v0.50.0 c4ec31f

completion_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close