v0.50.0 c4ec31f

orchestration_loop_test.go RawHistoryFolder

1package agent
2
3import (
4	"context"
5	"errors"
6	"strings"
7	"testing"
8
9	"github.com/bnema/zut/packages/agent/subagents"
10)
11
12type fakeCompletionBatches struct {
13	batches [][]subagents.Completion
14	errors  []error
15	waits   int
16}
17
18type endlessCompletionBatches struct{ waits int }
19
20func (f *endlessCompletionBatches) WaitIdle(context.Context) ([]subagents.Completion, error) {
21	f.waits++
22	return []subagents.Completion{{AgentID: "worker", Status: "completed"}}, nil
23}
24
25func (f *fakeCompletionBatches) WaitIdle(context.Context) ([]subagents.Completion, error) {
26	index := f.waits
27	f.waits++
28	if index < len(f.errors) && f.errors[index] != nil {
29		return nil, f.errors[index]
30	}
31	if index >= len(f.batches) {
32		return nil, nil
33	}
34	return f.batches[index], nil
35}
36
37func TestRunHeadlessContinuationNoWorkerPath(t *testing.T) {
38	tracker := &fakeCompletionBatches{batches: [][]subagents.Completion{{}}}
39	called := false
40	got, err := runHeadlessContinuation(context.Background(), tracker, "initial synthesis", func(context.Context, string) (string, error) {
41		called = true
42		return "unexpected", nil
43	})
44	if err != nil {
45		t.Fatal(err)
46	}
47	if called || got != "initial synthesis" {
48		t.Fatalf("no-worker result = %q, called=%v", got, called)
49	}
50}
51
52func TestRunHeadlessContinuationBatchesFailuresAndSecondWave(t *testing.T) {
53	tracker := &fakeCompletionBatches{batches: [][]subagents.Completion{
54		{{AgentID: "worker-a", Status: "completed", Task: "inspect"}, {AgentID: "worker-b", Status: "failed", Task: "test", Error: "provider failed"}},
55		{{AgentID: "worker-c", Status: "completed", Task: "dependent"}},
56		{},
57	}}
58	var prompts []string
59	got, err := runHeadlessContinuation(context.Background(), tracker, "initial", func(_ context.Context, prompt string) (string, error) {
60		prompts = append(prompts, prompt)
61		return "synthesis " + string(rune('0'+len(prompts))), nil
62	})
63	if err != nil {
64		t.Fatal(err)
65	}
66	if got != "synthesis 2" || len(prompts) != 2 {
67		t.Fatalf("result=%q prompts=%d, want second-wave synthesis", got, len(prompts))
68	}
69	if !strings.Contains(prompts[0], "worker-b") || !strings.Contains(prompts[0], "provider failed") {
70		t.Fatalf("first completion batch omitted failed-worker evidence: %q", prompts[0])
71	}
72	if !strings.Contains(prompts[1], "worker-c") || strings.Contains(prompts[1], "worker-a") {
73		t.Fatalf("second completion batch was not isolated: %q", prompts[1])
74	}
75	if tracker.waits != 3 {
76		t.Fatalf("wait count=%d, want completion batches followed by empty terminal batch", tracker.waits)
77	}
78}
79
80func TestRunHeadlessContinuationStopsAtWaveLimit(t *testing.T) {
81	tracker := &endlessCompletionBatches{}
82	calls := 0
83	got, err := runHeadlessContinuation(context.Background(), tracker, "initial", func(context.Context, string) (string, error) {
84		calls++
85		return "synthesis", nil
86	})
87	if err == nil || !strings.Contains(err.Error(), "maximum of 32 completion waves") {
88		t.Fatalf("error = %v, want wave-limit error", err)
89	}
90	if got != "synthesis" || calls != maxHeadlessCompletionWaves || tracker.waits != maxHeadlessCompletionWaves+1 {
91		t.Fatalf("result=%q calls=%d waits=%d", got, calls, tracker.waits)
92	}
93}
94
95func TestRunHeadlessContinuationCancellation(t *testing.T) {
96	ctx, cancel := context.WithCancel(context.Background())
97	cancel()
98	want := errors.New("canceled")
99	tracker := &fakeCompletionBatches{errors: []error{want}}
100	got, err := runHeadlessContinuation(ctx, tracker, "initial", func(context.Context, string) (string, error) {
101		t.Fatal("parent should not run after cancellation")
102		return "", nil
103	})
104	var waitErr *headlessCompletionWaitError
105	if !errors.Is(err, want) || !errors.As(err, &waitErr) || got != "initial" {
106		t.Fatalf("result=(%q,%v), wait error=%v, want initial and cancellation wait error", got, err, waitErr)
107	}
108}
109
110func TestRunHeadlessContinuationParentErrorDoesNotRetry(t *testing.T) {
111	tracker := &fakeCompletionBatches{batches: [][]subagents.Completion{{{AgentID: "worker", Status: "completed"}}}}
112	want := errors.New("parent failed")
113	calls := 0
114	got, err := runHeadlessContinuation(context.Background(), tracker, "initial", func(context.Context, string) (string, error) {
115		calls++
116		return "partial", want
117	})
118	if !errors.Is(err, want) || got != "partial" || calls != 1 {
119		t.Fatalf("result=(%q,%v), calls=%d, want one non-retried parent failure", got, err, calls)
120	}
121}

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 476

↑↓ moveenter openesc close