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