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}