* feat: add watch mode to gt status - Add --watch/-w flag for continuous status refresh - Add --interval/-n flag to set refresh interval (default 2s) - Clears screen and shows timestamp on each refresh - Graceful Ctrl+C handling to stop watch mode - Works with existing --fast and --json flags * fix(status): validate watch interval to prevent panic on zero/negative values * fix(status): harden watch mode with signal cleanup, TTY detection, and tests - Add defer signal.Stop() to prevent signal handler leak - Reject --json + --watch combination (produces invalid output) - Add TTY detection for ANSI escapes (safe when piped) - Use style.Dim for header when in TTY mode - Fix duplicate '(default 2)' in flag help - Add tests for interval validation and flag conflicts
161 lines
3.5 KiB
Go
161 lines
3.5 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/steveyegge/gastown/internal/beads"
|
|
"github.com/steveyegge/gastown/internal/rig"
|
|
)
|
|
|
|
func captureStdout(t *testing.T, fn func()) string {
|
|
t.Helper()
|
|
old := os.Stdout
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("create pipe: %v", err)
|
|
}
|
|
os.Stdout = w
|
|
|
|
fn()
|
|
|
|
_ = w.Close()
|
|
os.Stdout = old
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := io.Copy(&buf, r); err != nil {
|
|
t.Fatalf("read stdout: %v", err)
|
|
}
|
|
_ = r.Close()
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func TestDiscoverRigAgents_UsesRigPrefix(t *testing.T) {
|
|
townRoot := t.TempDir()
|
|
writeTestRoutes(t, townRoot, []beads.Route{
|
|
{Prefix: "bd-", Path: "beads/mayor/rig"},
|
|
})
|
|
|
|
r := &rig.Rig{
|
|
Name: "beads",
|
|
Path: filepath.Join(townRoot, "beads"),
|
|
HasWitness: true,
|
|
}
|
|
|
|
allAgentBeads := map[string]*beads.Issue{
|
|
"bd-beads-witness": {
|
|
ID: "bd-beads-witness",
|
|
AgentState: "running",
|
|
HookBead: "bd-hook",
|
|
},
|
|
}
|
|
allHookBeads := map[string]*beads.Issue{
|
|
"bd-hook": {ID: "bd-hook", Title: "Pinned"},
|
|
}
|
|
|
|
agents := discoverRigAgents(map[string]bool{}, r, nil, allAgentBeads, allHookBeads, nil, true)
|
|
if len(agents) != 1 {
|
|
t.Fatalf("discoverRigAgents() returned %d agents, want 1", len(agents))
|
|
}
|
|
|
|
if agents[0].State != "running" {
|
|
t.Fatalf("agent state = %q, want %q", agents[0].State, "running")
|
|
}
|
|
if !agents[0].HasWork {
|
|
t.Fatalf("agent HasWork = false, want true")
|
|
}
|
|
if agents[0].WorkTitle != "Pinned" {
|
|
t.Fatalf("agent WorkTitle = %q, want %q", agents[0].WorkTitle, "Pinned")
|
|
}
|
|
}
|
|
|
|
func TestRenderAgentDetails_UsesRigPrefix(t *testing.T) {
|
|
townRoot := t.TempDir()
|
|
writeTestRoutes(t, townRoot, []beads.Route{
|
|
{Prefix: "bd-", Path: "beads/mayor/rig"},
|
|
})
|
|
|
|
agent := AgentRuntime{
|
|
Name: "witness",
|
|
Address: "beads/witness",
|
|
Role: "witness",
|
|
Running: true,
|
|
}
|
|
|
|
output := captureStdout(t, func() {
|
|
renderAgentDetails(agent, "", nil, townRoot)
|
|
})
|
|
|
|
if !strings.Contains(output, "bd-beads-witness") {
|
|
t.Fatalf("output %q does not contain rig-prefixed bead ID", output)
|
|
}
|
|
}
|
|
|
|
func TestRunStatusWatch_RejectsZeroInterval(t *testing.T) {
|
|
oldInterval := statusInterval
|
|
oldWatch := statusWatch
|
|
defer func() {
|
|
statusInterval = oldInterval
|
|
statusWatch = oldWatch
|
|
}()
|
|
|
|
statusInterval = 0
|
|
statusWatch = true
|
|
|
|
err := runStatusWatch(nil, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for zero interval, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "positive") {
|
|
t.Errorf("error %q should mention 'positive'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRunStatusWatch_RejectsNegativeInterval(t *testing.T) {
|
|
oldInterval := statusInterval
|
|
oldWatch := statusWatch
|
|
defer func() {
|
|
statusInterval = oldInterval
|
|
statusWatch = oldWatch
|
|
}()
|
|
|
|
statusInterval = -5
|
|
statusWatch = true
|
|
|
|
err := runStatusWatch(nil, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for negative interval, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "positive") {
|
|
t.Errorf("error %q should mention 'positive'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRunStatusWatch_RejectsJSONCombo(t *testing.T) {
|
|
oldJSON := statusJSON
|
|
oldWatch := statusWatch
|
|
oldInterval := statusInterval
|
|
defer func() {
|
|
statusJSON = oldJSON
|
|
statusWatch = oldWatch
|
|
statusInterval = oldInterval
|
|
}()
|
|
|
|
statusJSON = true
|
|
statusWatch = true
|
|
statusInterval = 2
|
|
|
|
err := runStatusWatch(nil, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for --json + --watch, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot be used together") {
|
|
t.Errorf("error %q should mention 'cannot be used together'", err.Error())
|
|
}
|
|
}
|