package agents import ( "fmt" "strings" "testing" "time" "g.fc.run/misc/byre/internal/api" ) func TestAssessStuck(t *testing.T) { now := time.Now() procs := []procInfo{ {sessionID: "a", pid: 10, state: api.AgentSessionStateRunning, lastLine: now.Add(-StuckAfter - time.Second)}, {sessionID: "b", pid: 11, state: api.AgentSessionStateRunning, lastLine: now.Add(-StuckAfter + time.Second)}, {sessionID: "c", pid: 12, state: api.AgentSessionStateIdle, lastLine: now.Add(-time.Hour)}, } running := []api.AgentSession{{Id: "a"}, {Id: "b"}} h := assess(now, procs, running, []int{10, 11, 12}) want := map[string]api.AgentProcessHealth{"a": api.HealthStuck, "b": api.HealthOk, "c": api.HealthOk} for _, p := range h.Processes { if p.Health != want[p.SessionId] { t.Errorf("session %s health = %s, want %s", p.SessionId, p.Health, want[p.SessionId]) } } if len(h.Zombies) != 0 { t.Errorf("zombies = %+v, want none", h.Zombies) } } func TestAssessZombies(t *testing.T) { now := time.Now() procs := []procInfo{ {sessionID: "exited", pid: 20, state: api.AgentSessionStateRunning, lastLine: now, exited: true}, {sessionID: "done", pid: 21, state: api.AgentSessionStateCompleted, lastLine: now}, {sessionID: "idle", pid: 22, state: api.AgentSessionStateIdle, lastLine: now}, } running := []api.AgentSession{{Id: "exited", TaskId: "t1"}, {Id: "gone", TaskId: "t2"}} h := assess(now, procs, running, []int{21, 22, 99}) got := []string{} for _, z := range h.Zombies { key := string(z.Kind) if z.SessionId != nil { key += " " + *z.SessionId } if z.Pid != nil { key += fmt.Sprintf(" %d", *z.Pid) } got = append(got, key) } want := []string{ "process_without_session done 21", "running_without_process exited", "running_without_process gone", "process_without_session 99", } if strings.Join(got, "\n") != strings.Join(want, "\n") { t.Errorf("zombies:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(want, "\n")) } for _, p := range h.Processes { if p.SessionId == "exited" && p.Health != api.HealthExited { t.Errorf("exited process health = %s", p.Health) } } } func TestBadLinesRing(t *testing.T) { b := newBadLines() at := time.Now() for i := range keptBadLines + 5 { b.add("claude", "s", fmt.Sprintf("reason %d", i), []byte(fmt.Sprintf("line %d", i)), at) } b.add("other", "s", "big", []byte(strings.Repeat("x", badLineBytes*2)), at) snap := b.snapshot() if snap.Counts["claude"] != keptBadLines+5 || snap.Counts["other"] != 1 { t.Errorf("counts = %v", snap.Counts) } if len(snap.Recent) != keptBadLines { t.Fatalf("kept %d, want %d", len(snap.Recent), keptBadLines) } if snap.Recent[0].Reason != "big" || len(snap.Recent[0].Raw) != badLineBytes || snap.Recent[0].Size != badLineBytes*2 { t.Errorf("newest = %q, %d raw bytes, size %d", snap.Recent[0].Reason, len(snap.Recent[0].Raw), snap.Recent[0].Size) } if snap.Recent[1].Raw != fmt.Sprintf("line %d", keptBadLines+4) { t.Errorf("second newest = %q", snap.Recent[1].Raw) } oldest := snap.Recent[len(snap.Recent)-1].Raw if oldest != "line 6" { t.Errorf("oldest = %q, want line 6", oldest) } } func TestParseChildren(t *testing.T) { out := []byte(" 100 1 launchd\n 200 100 claude\n 201 100 /Users/x/.local/bin/claude\n 202 100 git\n 203 999 claude\n") got := parseChildren(out, 100) if fmt.Sprint(got) != "[200 201]" { t.Errorf("children = %v", got) } }