v0.10.0 8a8e2b3

auto_subagents_test.go RawHistoryFolder

1package modes
2
3import (
4	"context"
5	"errors"
6	"strings"
7	"sync"
8	"testing"
9	"time"
10
11	"github.com/bnema/zut/packages/agent/subagents"
12	"github.com/bnema/zut/packages/core"
13	"github.com/bnema/zut/packages/provider"
14)
15
16func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) {
17	iv := &Interactive{
18		agent: &core.Agent{System: "base system"},
19		cfg: InteractiveConfig{
20			AutoSubagentsSystemAddendum: "auto-subagents guidance",
21			SubagentsSystemAddendum:     "[subagents_list]\n- reviewer\n[/subagents_list]",
22			Supervisor:                  &subagents.Supervisor{},
23		},
24	}
25
26	iv.applyAutoSubagentsSystemPrompt(true)
27	if !strings.Contains(iv.agent.System, "[subagents_list]") || !strings.Contains(iv.agent.System, "auto-subagents guidance") {
28		t.Fatalf("enabled system prompt missing subagent blocks: %q", iv.agent.System)
29	}
30	before := iv.agent.System
31	iv.applyAutoSubagentsSystemPrompt(true)
32	if iv.agent.System != before {
33		t.Fatalf("enabling twice changed system prompt: %q -> %q", before, iv.agent.System)
34	}
35
36	iv.applyAutoSubagentsSystemPrompt(false)
37	if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-subagents guidance") {
38		t.Fatalf("disabled system prompt retained subagent blocks: %q", iv.agent.System)
39	}
40}
41
42func TestAutoSubagentsTogglePreservesMatchingBasePromptBlocks(t *testing.T) {
43	manifest := "[subagents_list]\n- reviewer\n[/subagents_list]"
44	guidance := "auto-subagents guidance"
45	base := "base system\n\n" + manifest + "\n\n" + guidance
46	iv := &Interactive{
47		agent: &core.Agent{System: base},
48		cfg: InteractiveConfig{
49			AutoSubagentsSystemAddendum: guidance,
50			SubagentsSystemAddendum:     manifest,
51			Supervisor:                  &subagents.Supervisor{},
52		},
53	}
54
55	iv.applyAutoSubagentsSystemPrompt(true)
56	if strings.Count(iv.agent.System, manifest) != 2 || strings.Count(iv.agent.System, guidance) != 2 {
57		t.Fatalf("enabling should append owned duplicate blocks: %q", iv.agent.System)
58	}
59	iv.applyAutoSubagentsSystemPrompt(false)
60	if got := strings.TrimSpace(iv.agent.System); got != base {
61		t.Fatalf("disabling changed matching base prompt: %q; want %q", got, base)
62	}
63}
64
65func TestAutoSubagentsSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T) {
66	response := "first response line\n" + strings.Repeat("result ", 150) + "final marker"
67	mgr := subagents.New(subagents.Config{
68		Root:     t.TempDir(),
69		RepoRoot: t.TempDir(),
70		NewRunner: func(*subagents.Agent) subagents.Runner {
71			return subagents.RunnerFunc(func(_ context.Context, sink subagents.Sink) error {
72				sink.Transcript(response)
73				return nil
74			})
75		},
76	})
77	a, err := mgr.Spawn(context.Background(), strings.Repeat("long task ", 150))
78	if err != nil {
79		t.Fatal(err)
80	}
81
82	iv := newQueuedAutoSubagentsInteractive()
83	iv.TrackSubagentWorker(a, a.Task)
84	update := waitForQueuedPrompt(t, iv)
85	if !strings.Contains(update, "final response:\n"+response) {
86		t.Fatalf("update missing complete final response: %q", update)
87	}
88}
89
90func TestTrackSubagentWorkerReportsStartupFailure(t *testing.T) {
91	mgr := subagents.New(subagents.Config{
92		Root:     t.TempDir(),
93		RepoRoot: t.TempDir(),
94		NewRunner: func(*subagents.Agent) subagents.Runner {
95			return subagents.RunnerFunc(func(context.Context, subagents.Sink) error {
96				return errors.New("listener startup failed")
97			})
98		},
99	})
100	a, err := mgr.Spawn(context.Background(), "report the date")
101	if err != nil {
102		t.Fatal(err)
103	}
104
105	iv := newQueuedAutoSubagentsInteractive()
106	iv.TrackSubagentWorker(a, a.Task)
107
108	update := waitForQueuedPrompt(t, iv)
109	if !strings.Contains(update, "status: failed") {
110		t.Fatalf("update missing failed status: %q", update)
111	}
112	if !strings.Contains(update, "listener startup failed") {
113		t.Fatalf("update missing startup error: %q", update)
114	}
115}
116
117func TestCompleteSupervisorWatchReportsTurnOutcomeOnce(t *testing.T) {
118	started := make(chan struct{})
119	mgr := subagents.New(subagents.Config{
120		Root:     t.TempDir(),
121		RepoRoot: t.TempDir(),
122		NewRunner: func(*subagents.Agent) subagents.Runner {
123			return subagents.RunnerFunc(func(ctx context.Context, _ subagents.Sink) error {
124				close(started)
125				<-ctx.Done()
126				return ctx.Err()
127			})
128		},
129	})
130	a, err := mgr.Spawn(context.Background(), "report the date")
131	if err != nil {
132		t.Fatal(err)
133	}
134	<-started
135	t.Cleanup(func() {
136		_ = mgr.Stop(a.ID)
137		a.Wait()
138	})
139
140	iv := newQueuedAutoSubagentsInteractive()
141	entry := &subagentWatchEntry{agent: a, task: a.Task}
142	iv.subagentWatch = []*subagentWatchEntry{entry}
143	iv.completeSupervisorWatchEntry(entry, "completed", "")
144	iv.completeSupervisorWatchEntry(entry, "failed", "late error")
145
146	update := waitForQueuedPrompt(t, iv)
147	if !strings.Contains(update, "status: completed") {
148		t.Fatalf("update uses daemon status instead of turn outcome: %q", update)
149	}
150	iv.mu.Lock()
151	queued := len(iv.queued)
152	iv.mu.Unlock()
153	if queued != 1 {
154		t.Fatalf("queued updates = %d; want exactly one", queued)
155	}
156}
157
158type autoSubagentsQueueClient struct {
159	requests chan provider.Request
160}
161
162func (c *autoSubagentsQueueClient) Name() string { return "auto-subagents-queue-test" }
163
164func (c *autoSubagentsQueueClient) Stream(_ context.Context, req provider.Request) (<-chan provider.Event, error) {
165	c.requests <- req
166	out := make(chan provider.Event, 1)
167	go func() {
168		defer close(out)
169		out <- provider.EventDone{
170			Stop: provider.StopEnd,
171			Message: provider.Message{
172				Role:    provider.RoleAssistant,
173				Content: []provider.Content{provider.TextBlock{Text: "done"}},
174			},
175		}
176	}()
177	return out, nil
178}
179
180func TestAutoSubagentsUpdateQueuedAtTurnEndStartsFollowUp(t *testing.T) {
181	client := &autoSubagentsQueueClient{requests: make(chan provider.Request, 2)}
182	agent := core.NewAgent(client, "test-model", "", nil)
183	interactive := NewInteractive(InteractiveConfig{Agent: agent})
184	interactive.runCtx = context.Background()
185
186	var once sync.Once
187	agent.OnEvent = func(ev core.AgentEvent) {
188		if _, ok := ev.(core.EvDone); !ok {
189			return
190		}
191		once.Do(func() {
192			interactive.flushSupervisorSummary([]*subagentWatchEntry{{
193				agent: &subagents.Agent{},
194				task:  "smoke test",
195			}})
196		})
197	}
198
199	interactive.startTurn(context.Background(), "initial")
200	select {
201	case <-client.requests:
202	case <-time.After(2 * time.Second):
203		t.Fatal("initial request did not start")
204	}
205
206	var followUp provider.Request
207	select {
208	case followUp = <-client.requests:
209	case <-time.After(2 * time.Second):
210		t.Fatal("queued auto-subagents update did not start a follow-up request")
211	}
212	foundUpdate := false
213	for _, message := range followUp.Messages {
214		for _, content := range message.Content {
215			if text, ok := content.(provider.TextBlock); ok && strings.Contains(text.Text, "[auto-subagents update]") {
216				foundUpdate = true
217			}
218		}
219	}
220	if !foundUpdate {
221		t.Fatalf("follow-up request did not contain the auto-subagents update: %#v", followUp.Messages)
222	}
223
224	deadline := time.NewTimer(2 * time.Second)
225	defer deadline.Stop()
226	for {
227		interactive.mu.Lock()
228		busy := interactive.busy
229		interactive.mu.Unlock()
230		if !busy && agent.QueuedMessageCount() == 0 {
231			return
232		}
233		select {
234		case <-deadline.C:
235			t.Fatal("interactive turn remained busy or retained a queued update")
236		case <-time.After(time.Millisecond):
237		}
238	}
239}
240
241func newQueuedAutoSubagentsInteractive() *Interactive {
242	return &Interactive{
243		agent:      &core.Agent{},
244		busy:       true,
245		compacting: true,
246		dirty:      make(chan struct{}, 1),
247	}
248}
249
250func waitForQueuedPrompt(t *testing.T, iv *Interactive) string {
251	t.Helper()
252	deadline := time.NewTimer(time.Second)
253	defer deadline.Stop()
254	ticker := time.NewTicker(time.Millisecond)
255	defer ticker.Stop()
256	for {
257		iv.mu.Lock()
258		if len(iv.queued) > 0 {
259			prompt := iv.queued[0]
260			iv.mu.Unlock()
261			return prompt
262		}
263		iv.mu.Unlock()
264		select {
265		case <-deadline.C:
266			t.Fatal("timed out waiting for auto-subagents update")
267		case <-ticker.C:
268		}
269	}
270}

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