1package modes
2
3import (
4 "context"
5 "errors"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/bnema/zut/packages/agent/subagents"
11 "github.com/bnema/zut/packages/agent/tools"
12 "github.com/bnema/zut/packages/core"
13 "github.com/bnema/zut/packages/provider"
14)
15
16type failingAutoSubagentsSettingsStore struct {
17 SettingsStore
18 err error
19 calls int
20}
21
22func (s *failingAutoSubagentsSettingsStore) SetAutoSubagents(bool) error {
23 s.calls++
24 return s.err
25}
26
27func TestAutoSubagentsToggleRollsBackInMemoryStateWhenPersistenceFails(t *testing.T) {
28 previous := true
29 allowed := true
30 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
31 t.Cleanup(supervisor.StopAll)
32 store := &failingAutoSubagentsSettingsStore{err: errors.New("disk full")}
33 agent := core.NewAgent(nil, "test-model", "base system\n\nauto-subagents guidance", core.Registry{
34 "subagent_spawn": &tools.SubagentSpawnTool{Supervisor: supervisor},
35 "subagent_status": &tools.SubagentStatusTool{Supervisor: supervisor},
36 })
37 interactive := NewInteractive(InteractiveConfig{
38 Agent: agent,
39 Supervisor: supervisor,
40 AutoSubagentsEnabled: &previous,
41 AutoSubagentsToolAllowed: &allowed,
42 AutoSubagentsStatusToolAllowed: &allowed,
43 AutoSubagentsSystemAddendum: "auto-subagents guidance",
44 SettingsStore: store,
45 })
46 interactive.settingsDialog = newSettingsDialog()
47 interactive.rend = nil
48 interactive.openSettingsDialog()
49 beforeSystem, _ := agent.PromptConfig()
50 beforeTools := agent.ToolsSnapshot()
51
52 interactive.applySettingToggle("auto_subagents_enabled", false)
53
54 interactive.mu.Lock()
55 gotEnabled := interactive.cfg.AutoSubagentsEnabled != nil && *interactive.cfg.AutoSubagentsEnabled
56 statusErr := interactive.statusErr
57 interactive.mu.Unlock()
58 if !gotEnabled {
59 t.Fatal("auto-subagents flag stayed disabled after persistence failure")
60 }
61 if store.calls != 1 {
62 t.Fatalf("SetAutoSubagents calls = %d, want 1", store.calls)
63 }
64 if !strings.Contains(statusErr, "disk full") {
65 t.Fatalf("status error = %q, want persistence failure", statusErr)
66 }
67 afterSystem, _ := agent.PromptConfig()
68 if afterSystem != beforeSystem {
69 t.Fatalf("system prompt changed after persistence failure: %q -> %q", beforeSystem, afterSystem)
70 }
71 afterTools := agent.ToolsSnapshot()
72 if len(afterTools) != len(beforeTools) {
73 t.Fatalf("tool registry size = %d, want unchanged %d", len(afterTools), len(beforeTools))
74 }
75 for name, before := range beforeTools {
76 if afterTools[name] != before {
77 t.Fatalf("tool %q changed after persistence failure", name)
78 }
79 }
80 item := findSettingsItem(interactive.settingsDialog.items, "auto_subagents_enabled")
81 if item == nil || !item.value {
82 t.Fatalf("settings row = %#v, want restored enabled value", item)
83 }
84}
85
86func TestAutoSubagentsSettingsDisableUnavailablePolicy(t *testing.T) {
87 trueValue := true
88 falseValue := false
89
90 tests := []struct {
91 name string
92 supervisor *subagents.Supervisor
93 allowed *bool
94 wantHint string
95 }{
96 {
97 name: "supervisor unavailable",
98 allowed: &trueValue,
99 wantHint: "subagent supervisor not available in this mode",
100 },
101 {
102 name: "launch policy excludes tool",
103 supervisor: subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()}),
104 allowed: &falseValue,
105 wantHint: "launch-time tool policy excludes subagent manager tools",
106 },
107 }
108 for _, tt := range tests {
109 t.Run(tt.name, func(t *testing.T) {
110 if tt.supervisor != nil {
111 t.Cleanup(tt.supervisor.StopAll)
112 }
113 iv := &Interactive{
114 cfg: InteractiveConfig{
115 AutoSubagentsEnabled: &trueValue,
116 AutoSubagentsToolAllowed: tt.allowed,
117 Supervisor: tt.supervisor,
118 },
119 settingsDialog: newSettingsDialog(),
120 }
121 iv.openSettingsDialog()
122
123 var item settingsItem
124 for _, candidate := range iv.settingsDialog.items {
125 if candidate.key == "auto_subagents_enabled" {
126 item = candidate
127 break
128 }
129 }
130 if !item.disabled {
131 t.Fatal("auto-subagents setting is not disabled")
132 }
133 if item.value {
134 t.Fatal("disabled auto-subagents setting is shown as enabled")
135 }
136 if !strings.Contains(item.hint, tt.wantHint) {
137 t.Fatalf("hint = %q; want %q", item.hint, tt.wantHint)
138 }
139 })
140 }
141}
142
143func TestAutoSubagentsToolRegistrationHonorsLaunchPolicy(t *testing.T) {
144 allowed := false
145 enabled := false
146 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
147 t.Cleanup(supervisor.StopAll)
148 iv := &Interactive{
149 agent: &core.Agent{Tools: core.Registry{}},
150 cfg: InteractiveConfig{
151 AutoSubagentsEnabled: &enabled,
152 AutoSubagentsToolAllowed: &allowed,
153 Supervisor: supervisor,
154 },
155 dirty: make(chan struct{}, 1),
156 }
157
158 iv.applyAutoSubagentsTool()
159 if _, ok := iv.agent.Tools["subagent_spawn"]; ok {
160 t.Fatal("subagent_spawn registered despite launch-time policy")
161 }
162 if _, ok := iv.agent.Tools["subagent_status"]; ok {
163 t.Fatal("subagent_status registered despite launch-time policy")
164 }
165 if _, ok := iv.agent.Tools["subagent_stop"]; ok {
166 t.Fatal("subagent_stop registered despite launch-time policy")
167 }
168 if _, ok := iv.agent.Tools["subagent_resume"]; ok {
169 t.Fatal("subagent_resume registered despite launch-time policy")
170 }
171 iv.applySettingToggle("auto_subagents_enabled", true)
172 iv.mu.Lock()
173 statusErr := iv.statusErr
174 value := iv.cfg.AutoSubagentsEnabled != nil && *iv.cfg.AutoSubagentsEnabled
175 iv.mu.Unlock()
176 if value {
177 t.Fatal("auto-subagents toggle enabled despite launch-time policy")
178 }
179 if !strings.Contains(statusErr, "launch-time tool policy") {
180 t.Fatalf("toggle error = %q; want launch-time policy hint", statusErr)
181 }
182}
183
184func TestAutoSubagentsToolRegistrationHonorsSeparateLaunchPolicies(t *testing.T) {
185 for _, tc := range []struct {
186 name string
187 spawnAllowed bool
188 statusAllowed bool
189 stopAllowed bool
190 resumeAllowed bool
191 wantSpawn bool
192 wantStatus bool
193 wantStop bool
194 wantResume bool
195 }{
196 {name: "status only", statusAllowed: true, wantStatus: true},
197 {name: "spawn only", spawnAllowed: true, wantSpawn: true},
198 {name: "stop only", stopAllowed: true, wantStop: true},
199 {name: "resume only", resumeAllowed: true, wantResume: true},
200 {name: "all", spawnAllowed: true, statusAllowed: true, stopAllowed: true, resumeAllowed: true, wantSpawn: true, wantStatus: true, wantStop: true, wantResume: true},
201 } {
202 t.Run(tc.name, func(t *testing.T) {
203 enabled := true
204 spawnAllowed := tc.spawnAllowed
205 statusAllowed := tc.statusAllowed
206 stopAllowed := tc.stopAllowed
207 resumeAllowed := tc.resumeAllowed
208 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
209 t.Cleanup(supervisor.StopAll)
210 iv := &Interactive{
211 agent: &core.Agent{Tools: core.Registry{}},
212 cfg: InteractiveConfig{
213 AutoSubagentsEnabled: &enabled,
214 AutoSubagentsToolAllowed: &spawnAllowed,
215 AutoSubagentsStatusToolAllowed: &statusAllowed,
216 AutoSubagentsStopToolAllowed: &stopAllowed,
217 AutoSubagentsResumeToolAllowed: &resumeAllowed,
218 Supervisor: supervisor,
219 },
220 dirty: make(chan struct{}, 1),
221 }
222
223 iv.applyAutoSubagentsTool()
224 _, gotSpawn := iv.agent.Tools["subagent_spawn"]
225 _, gotStatus := iv.agent.Tools["subagent_status"]
226 _, gotStop := iv.agent.Tools["subagent_stop"]
227 _, gotResume := iv.agent.Tools["subagent_resume"]
228 if gotSpawn != tc.wantSpawn || gotStatus != tc.wantStatus || gotStop != tc.wantStop || gotResume != tc.wantResume {
229 t.Fatalf("registered tools spawn=%v status=%v stop=%v resume=%v, want spawn=%v status=%v stop=%v resume=%v", gotSpawn, gotStatus, gotStop, gotResume, tc.wantSpawn, tc.wantStatus, tc.wantStop, tc.wantResume)
230 }
231 })
232 }
233}
234
235func TestAutoSubagentsDisabledKeepsToolsForExplicitUserRequests(t *testing.T) {
236 enabled := false
237 allowed := true
238 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
239 t.Cleanup(supervisor.StopAll)
240 iv := NewInteractive(InteractiveConfig{
241 Agent: &core.Agent{Tools: core.Registry{}},
242 AutoSubagentsEnabled: &enabled,
243 AutoSubagentsToolAllowed: &allowed,
244 AutoSubagentsSystemAddendum: "primary-agent orchestrator",
245 Supervisor: supervisor,
246 })
247
248 spawn, ok := iv.agent.ToolsSnapshot()["subagent_spawn"].(*tools.SubagentSpawnTool)
249 if !ok {
250 t.Fatal("subagent_spawn is unavailable when automatic subagents are disabled")
251 }
252 if spawn.Enabled == nil || !spawn.Enabled() {
253 t.Fatal("subagent_spawn is disabled when a user explicitly requests delegation")
254 }
255 system, _ := iv.agent.PromptConfig()
256 if strings.Contains(system, "primary-agent orchestrator") {
257 t.Fatalf("disabled automatic subagents retained the orchestrator contract: %q", system)
258 }
259}
260
261type serializedAutoSubagentsClient struct {
262 started chan struct{}
263 request chan provider.Request
264 release chan struct{}
265 finished chan struct{}
266}
267
268func (c *serializedAutoSubagentsClient) Name() string { return "serialized-auto-subagents-test" }
269
270func (c *serializedAutoSubagentsClient) Stream(ctx context.Context, req provider.Request) (<-chan provider.Event, error) {
271 c.request <- req
272 c.started <- struct{}{}
273 out := make(chan provider.Event, 1)
274 go func() {
275 defer close(out)
276 select {
277 case <-c.release:
278 out <- provider.EventDone{Stop: provider.StopEnd}
279 case <-ctx.Done():
280 out <- provider.EventDone{Stop: provider.StopAborted, Err: ctx.Err()}
281 }
282 close(c.finished)
283 }()
284 return out, nil
285}
286
287func TestAutoSubagentsPromptConfigurationIsSynchronized(t *testing.T) {
288 client := &serializedAutoSubagentsClient{
289 started: make(chan struct{}, 1),
290 request: make(chan provider.Request, 1),
291 release: make(chan struct{}),
292 finished: make(chan struct{}),
293 }
294 agent := core.NewAgent(client, "test-model", "base system", nil)
295 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
296 t.Cleanup(supervisor.StopAll)
297 allowed := true
298 iv := NewInteractive(InteractiveConfig{
299 Agent: agent,
300 Supervisor: supervisor,
301 AutoSubagentsToolAllowed: &allowed,
302 AutoSubagentsSystemAddendum: "delegation guidance",
303 })
304
305 iv.startTurn(context.Background(), "hello")
306 select {
307 case <-client.started:
308 case <-time.After(time.Second):
309 t.Fatal("prompt did not reach provider")
310 }
311
312 request := <-client.request
313 if request.System != "base system" {
314 t.Fatalf("provider request system = %q; want base system snapshot", request.System)
315 }
316 applied := make(chan struct{})
317 go func() {
318 iv.applyAutoSubagentsSystemPrompt(true)
319 close(applied)
320 }()
321 select {
322 case <-applied:
323 case <-time.After(time.Second):
324 t.Fatal("system-prompt update did not apply while provider was running")
325 }
326 if system, _ := agent.PromptConfig(); !strings.Contains(system, "delegation guidance") {
327 t.Fatalf("system prompt missing applied guidance: %q", system)
328 }
329
330 close(client.release)
331 select {
332 case <-client.finished:
333 case <-time.After(time.Second):
334 t.Fatal("prompt did not finish after provider release")
335 }
336}