1package subagents
2
3import (
4 "context"
5 "errors"
6 "reflect"
7 "strings"
8 "testing"
9)
10
11func TestResidentCompletionProjection(t *testing.T) {
12 for _, tc := range []struct {
13 err error
14 status string
15 }{
16 {nil, "completed"},
17 {errors.New("failure"), "failed"},
18 {context.Canceled, "interrupted"},
19 {errors.New("capture failed"), "failed"},
20 {errors.Join(context.Canceled, errors.New("capture failed")), "interrupted"},
21 } {
22 t.Run(tc.status, func(t *testing.T) {
23 got := (ResidentCompletion{ChildID: "child", TurnID: "turn", Task: "task", Summary: "saved progress", Err: tc.err}).Completion()
24 if got.Status != tc.status || got.AgentID != "child" || got.TurnID != "turn" || got.Task != "task" || got.Summary != "saved progress" {
25 t.Fatalf("completion = %#v", got)
26 }
27 if tc.err != nil && got.Error != tc.err.Error() {
28 t.Fatalf("error = %q, want %q", got.Error, tc.err.Error())
29 }
30 text := FormatCompletionUpdate([]Completion{got}, "")
31 label := "partial: "
32 if tc.err == nil {
33 label = "final: "
34 }
35 if !strings.Contains(text, label+"saved progress") {
36 t.Fatalf("update = %q", text)
37 }
38 })
39 }
40}
41
42// A dropped follow-up hides expected cancellation but keeps any other cause.
43func TestDroppedCompletionKeepsNonCancellationError(t *testing.T) {
44 canceled := (ResidentCompletion{ChildID: "c", TurnID: "t", Task: "next", Err: context.Canceled, NotStarted: true}).Completion()
45 if canceled.Status != CompletionDropped || canceled.Error != "" {
46 t.Fatalf("canceled drop = %#v", canceled)
47 }
48 failed := (ResidentCompletion{ChildID: "c", TurnID: "t", Task: "next", Err: errors.New("journal full"), NotStarted: true}).Completion()
49 if failed.Status != CompletionDropped || failed.Error != "journal full" {
50 t.Fatalf("failed drop = %#v", failed)
51 }
52 if text := FormatCompletionUpdate([]Completion{failed}, ""); !strings.Contains(text, "dropped (queued follow-up never started: next) (journal full)") {
53 t.Fatalf("update = %q", text)
54 }
55}
56
57func TestFormatCompletionUpdateIncludesFinalSummary(t *testing.T) {
58 got := FormatCompletionUpdate([]Completion{{AgentID: "child", Status: "completed", Task: "review", Summary: "found the regression"}}, "")
59 if !strings.Contains(got, "final: found the regression") {
60 t.Fatalf("completion update = %q", got)
61 }
62}
63
64func TestFormatCompletionUpdateIdentifiesActiveSiblings(t *testing.T) {
65 got := FormatCompletionUpdateWithActive(
66 []Completion{{AgentID: "finished", Status: "completed"}},
67 []string{"worker-a", "worker-b"},
68 "continue",
69 )
70 for _, want := range []string{"finished: completed", "Still running: worker-a worker-b", "continue"} {
71 if !strings.Contains(got, want) {
72 t.Fatalf("completion update %q missing %q", got, want)
73 }
74 }
75}
76
77func TestCompletionTrackerDropsReportsAfterCancellationReset(t *testing.T) {
78 tracker := NewCompletionTracker()
79 if !tracker.TrackResident("child", "turn-1") {
80 t.Fatal("TrackResident returned false")
81 }
82 tracker.Reset()
83 if tracker.Report(Completion{AgentID: "child", TurnID: "turn-1"}) {
84 t.Fatal("Report returned true after reset")
85 }
86 got, err := tracker.WaitIdle(context.Background())
87 if err != nil {
88 t.Fatal(err)
89 }
90 if len(got) != 0 {
91 t.Fatalf("stale completions = %#v", got)
92 }
93}
94
95func TestCompletionTrackerWaitReadyDoesNotWaitForSiblings(t *testing.T) {
96 tracker := NewCompletionTracker()
97 tracker.TrackResident("a", "turn-a")
98 tracker.TrackResident("b", "turn-b")
99 want := Completion{AgentID: "a", TurnID: "turn-a", Summary: "result"}
100 tracker.Report(want)
101 ctx, cancel := context.WithCancel(context.Background())
102 cancel()
103 got, err := tracker.WaitReady(ctx)
104 if err != nil || len(got) != 1 || !reflect.DeepEqual(got[0], want) {
105 t.Fatalf("WaitReady = %#v, %v, want ready result despite pending sibling", got, err)
106 }
107 if _, err := tracker.WaitReady(ctx); !errors.Is(err, context.Canceled) {
108 t.Fatalf("second WaitReady error = %v, want cancellation with no duplicate result", err)
109 }
110 tracker.Report(Completion{AgentID: "b", TurnID: "turn-b"})
111 got, err = tracker.WaitIdle(context.Background())
112 if err != nil || len(got) != 1 || got[0].AgentID != "b" {
113 t.Fatalf("WaitIdle = %#v, %v, want only remaining sibling", got, err)
114 }
115 got, err = tracker.WaitReady(context.Background())
116 if err != nil || len(got) != 0 {
117 t.Fatalf("idle WaitReady = %#v, %v", got, err)
118 }
119}
120
121func TestCompletionTrackerMatchesTerminalReportsToAcceptedTurns(t *testing.T) {
122 tracker := NewCompletionTracker()
123 if !tracker.TrackResident("child", "turn-1") || !tracker.TrackResident("child", "turn-2") {
124 t.Fatal("TrackResident returned false")
125 }
126 if tracker.Report(Completion{AgentID: "child", TurnID: "other"}) {
127 t.Fatal("Report accepted an unknown turn")
128 }
129 if !tracker.Report(Completion{AgentID: "child", TurnID: "turn-1"}) {
130 t.Fatal("Report rejected an accepted turn")
131 }
132 if tracker.Report(Completion{AgentID: "child", TurnID: "turn-1"}) {
133 t.Fatal("Report accepted a duplicate terminal result")
134 }
135 if got := tracker.Pending(); got != 1 {
136 t.Fatalf("Pending = %d, want 1", got)
137 }
138 if !tracker.Report(Completion{AgentID: "child", TurnID: "turn-2"}) {
139 t.Fatal("Report rejected second accepted turn")
140 }
141 got, err := tracker.WaitIdle(context.Background())
142 if err != nil {
143 t.Fatal(err)
144 }
145 if len(got) != 2 {
146 t.Fatalf("completions = %#v, want 2", got)
147 }
148}