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_spawn",
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(true)
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 iv.applySettingToggle("auto_subagents_enabled", true)
166 iv.mu.Lock()
167 statusErr := iv.statusErr
168 value := iv.cfg.AutoSubagentsEnabled != nil && *iv.cfg.AutoSubagentsEnabled
169 iv.mu.Unlock()
170 if value {
171 t.Fatal("auto-subagents toggle enabled despite launch-time policy")
172 }
173 if !strings.Contains(statusErr, "launch-time tool policy") {
174 t.Fatalf("toggle error = %q; want launch-time policy hint", statusErr)
175 }
176}
177
178func TestAutoSubagentsToolRegistrationHonorsSeparateLaunchPolicies(t *testing.T) {
179 for _, tc := range []struct {
180 name string
181 spawnAllowed bool
182 statusAllowed bool
183 wantSpawn bool
184 wantStatus bool
185 }{
186 {name: "status only", statusAllowed: true, wantStatus: true},
187 {name: "spawn only", spawnAllowed: true, wantSpawn: true},
188 {name: "both", spawnAllowed: true, statusAllowed: true, wantSpawn: true, wantStatus: true},
189 } {
190 t.Run(tc.name, func(t *testing.T) {
191 enabled := true
192 spawnAllowed := tc.spawnAllowed
193 statusAllowed := tc.statusAllowed
194 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
195 t.Cleanup(supervisor.StopAll)
196 iv := &Interactive{
197 agent: &core.Agent{Tools: core.Registry{}},
198 cfg: InteractiveConfig{
199 AutoSubagentsEnabled: &enabled,
200 AutoSubagentsToolAllowed: &spawnAllowed,
201 AutoSubagentsStatusToolAllowed: &statusAllowed,
202 Supervisor: supervisor,
203 },
204 dirty: make(chan struct{}, 1),
205 }
206
207 iv.applyAutoSubagentsTool(true)
208 _, gotSpawn := iv.agent.Tools["subagent_spawn"]
209 _, gotStatus := iv.agent.Tools["subagent_status"]
210 if gotSpawn != tc.wantSpawn || gotStatus != tc.wantStatus {
211 t.Fatalf("registered tools spawn=%v status=%v, want spawn=%v status=%v", gotSpawn, gotStatus, tc.wantSpawn, tc.wantStatus)
212 }
213 })
214 }
215}
216
217type serializedAutoSubagentsClient struct {
218 started chan struct{}
219 request chan provider.Request
220 release chan struct{}
221 finished chan struct{}
222}
223
224func (c *serializedAutoSubagentsClient) Name() string { return "serialized-auto-subagents-test" }
225
226func (c *serializedAutoSubagentsClient) Stream(ctx context.Context, req provider.Request) (<-chan provider.Event, error) {
227 c.request <- req
228 c.started <- struct{}{}
229 out := make(chan provider.Event, 1)
230 go func() {
231 defer close(out)
232 select {
233 case <-c.release:
234 out <- provider.EventDone{Stop: provider.StopEnd}
235 case <-ctx.Done():
236 out <- provider.EventDone{Stop: provider.StopAborted, Err: ctx.Err()}
237 }
238 close(c.finished)
239 }()
240 return out, nil
241}
242
243func TestAutoSubagentsPromptConfigurationIsSynchronized(t *testing.T) {
244 client := &serializedAutoSubagentsClient{
245 started: make(chan struct{}, 1),
246 request: make(chan provider.Request, 1),
247 release: make(chan struct{}),
248 finished: make(chan struct{}),
249 }
250 agent := core.NewAgent(client, "test-model", "base system", nil)
251 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
252 t.Cleanup(supervisor.StopAll)
253 allowed := true
254 iv := NewInteractive(InteractiveConfig{
255 Agent: agent,
256 Supervisor: supervisor,
257 AutoSubagentsToolAllowed: &allowed,
258 AutoSubagentsSystemAddendum: "delegation guidance",
259 })
260
261 iv.startTurn(context.Background(), "hello")
262 select {
263 case <-client.started:
264 case <-time.After(time.Second):
265 t.Fatal("prompt did not reach provider")
266 }
267
268 request := <-client.request
269 if request.System != "base system" {
270 t.Fatalf("provider request system = %q; want base system snapshot", request.System)
271 }
272 applied := make(chan struct{})
273 go func() {
274 iv.applyAutoSubagentsSystemPrompt(true)
275 close(applied)
276 }()
277 select {
278 case <-applied:
279 case <-time.After(time.Second):
280 t.Fatal("system-prompt update did not apply while provider was running")
281 }
282 if system, _ := agent.PromptConfig(); !strings.Contains(system, "delegation guidance") {
283 t.Fatalf("system prompt missing applied guidance: %q", system)
284 }
285
286 close(client.release)
287 select {
288 case <-client.finished:
289 case <-time.After(time.Second):
290 t.Fatal("prompt did not finish after provider release")
291 }
292}