1package agent
2
3import (
4 "strings"
5 "testing"
6)
7
8func TestSubagentsSystemAddendaListOnlyEnabledLifecycleTools(t *testing.T) {
9 // Substrings that identify the lifecycle action guidance.
10 const (
11 stopAction = "use stop to request termination of a stuck worker, and to remove an idle isolation:\"worktree\" worker's temporary worktree"
12 resumeAction = "use resume with an agent id and follow-up prompt"
13 interruptAction = "use interrupt to cancel only a worker's running turn"
14 noSpawnNotice = "Spawning new workers is unavailable"
15 )
16 builders := []struct {
17 name string
18 build func(SubagentActions) string
19 noSpawnFallback string
20 }{
21 {name: "proactive", build: ProactiveSubagentsSystemAddendumFor, noSpawnFallback: "Continue non-delegated work locally."},
22 {name: "strict", build: StrictOrchestratorSystemAddendumFor, noSpawnFallback: "Do not implement, debug, test, or review directly."},
23 }
24 for _, builder := range builders {
25 t.Run(builder.name, func(t *testing.T) {
26 for _, tc := range []struct {
27 name string
28 spawn bool
29 stop bool
30 resume bool
31 interrupt bool
32 want []string
33 unwanted []string
34 available bool
35 }{
36 {name: "none", spawn: true, unwanted: []string{stopAction, resumeAction, interruptAction}, available: true},
37 {name: "fully unavailable", unwanted: []string{stopAction, resumeAction, interruptAction}},
38 {name: "stop", spawn: true, stop: true, want: []string{stopAction}, unwanted: []string{resumeAction, interruptAction}, available: true},
39 {name: "resume", spawn: true, resume: true, want: []string{resumeAction}, unwanted: []string{stopAction, interruptAction}, available: true},
40 {name: "interrupt", spawn: true, interrupt: true, want: []string{interruptAction}, unwanted: []string{stopAction, resumeAction}, available: true},
41 {name: "all", spawn: true, stop: true, resume: true, interrupt: true, want: []string{stopAction, resumeAction, interruptAction}, available: true},
42 {name: "stop without spawn", stop: true, want: []string{stopAction, noSpawnNotice}, unwanted: []string{resumeAction, interruptAction}},
43 {name: "resume without spawn", resume: true, want: []string{resumeAction, noSpawnNotice}, unwanted: []string{stopAction, interruptAction}},
44 {name: "interrupt without spawn", interrupt: true, want: []string{interruptAction, noSpawnNotice}, unwanted: []string{stopAction, resumeAction}},
45 {name: "all without spawn", stop: true, resume: true, interrupt: true, want: []string{stopAction, resumeAction, interruptAction, noSpawnNotice}},
46 } {
47 t.Run(tc.name, func(t *testing.T) {
48 got := builder.build(SubagentActions{Spawn: tc.spawn, Stop: tc.stop, Resume: tc.resume, Interrupt: tc.interrupt})
49 if !tc.spawn && (tc.stop || tc.resume || tc.interrupt) && !strings.Contains(got, builder.noSpawnFallback) {
50 t.Fatalf("addendum missing mode-specific no-spawn fallback %q:\n%s", builder.noSpawnFallback, got)
51 }
52 for _, want := range tc.want {
53 if !strings.Contains(got, want) {
54 t.Fatalf("addendum missing enabled lifecycle tool %q:\n%s", want, got)
55 }
56 }
57 for _, unwanted := range tc.unwanted {
58 if strings.Contains(got, unwanted) {
59 t.Fatalf("addendum mentions unavailable lifecycle tool %q:\n%s", unwanted, got)
60 }
61 }
62 if gotAvailable := !strings.Contains(got, "unavailable in this session") && !strings.Contains(got, "Spawning new workers is unavailable"); gotAvailable != tc.available {
63 t.Fatalf("delegation available = %t, want %t:\n%s", gotAvailable, tc.available, got)
64 }
65 })
66 }
67 })
68 }
69}
70
71func TestSubagentPoliciesSeparateProactiveAndStrictOwnership(t *testing.T) {
72 proactive := ProactiveSubagentsSystemAddendumFor(SubagentActions{Spawn: true, Stop: false, Resume: false})
73 for _, want := range []string{"delegate by default and keep work local by exception", "spawn it with required:true and end or yield your turn", "Edits of a few lines in one file", "Do not search, read, test, review, edit", "Map dependencies first", "base set to the feature branch", "Review each worker's result as a gate", "into to the feature branch and delete_branch to true", "no worktree you created is left behind"} {
74 if !strings.Contains(proactive, want) {
75 t.Fatalf("proactive addendum missing %q:\n%s", want, proactive)
76 }
77 }
78 if strings.Contains(proactive, "not an implementer") {
79 t.Fatalf("proactive addendum retained strict orchestrator contract:\n%s", proactive)
80 }
81 // These lines vetoed delegation whenever the next step depended on a
82 // result, which made the primary implement nearly every task itself.
83 for _, unwanted := range []string{"primary owner and implementer", "do that work yourself instead of spawning", "do not delegate"} {
84 if strings.Contains(proactive, unwanted) {
85 t.Fatalf("proactive addendum kept delegation veto %q:\n%s", unwanted, proactive)
86 }
87 }
88
89 strict := StrictOrchestratorSystemAddendumFor(SubagentActions{Spawn: true, Stop: false, Resume: false})
90 for _, want := range []string{"not an implementer", "non-overlapping worker scopes", "Once a worker is active", "Only coordinate workers"} {
91 if !strings.Contains(strict, want) {
92 t.Fatalf("strict addendum missing %q:\n%s", want, strict)
93 }
94 }
95}
96
97func TestProactiveAddendumRoutesBeforeWorkOnlyWhenSpawningIsAvailable(t *testing.T) {
98 proactive := ProactiveSubagentsSystemAddendumFor(SubagentActions{Spawn: true, Stop: false, Resume: false})
99 for _, want := range []string{
100 "Route every new task before you work:",
101 "Re-route each new request, even late in a long session",
102 "the [subagents_list] profile for each delegated part",
103 "do not narrate the tool calls that follow",
104 "Keep a task fully local only when it is small",
105 } {
106 if !strings.Contains(proactive, want) {
107 t.Fatalf("proactive addendum missing %q:\n%s", want, proactive)
108 }
109 }
110
111 // Without the spawn action there is nothing to route to, and the
112 // spawn-withheld branches in the same prompt tell the primary to stay
113 // local; naming profiles there would contradict them.
114 for _, tc := range []struct {
115 name string
116 stop, resume bool
117 }{
118 {name: "spawn withheld"},
119 {name: "stop only", stop: true},
120 {name: "resume only", resume: true},
121 {name: "stop and resume", stop: true, resume: true},
122 } {
123 t.Run(tc.name, func(t *testing.T) {
124 got := ProactiveSubagentsSystemAddendumFor(SubagentActions{Spawn: false, Stop: tc.stop, Resume: tc.resume})
125 for _, unwanted := range []string{"Route every new task before you work:", "[subagents_list]", "Spawn a clearly described general worker"} {
126 if strings.Contains(got, unwanted) {
127 t.Fatalf("spawn-withheld addendum kept %q:\n%s", unwanted, got)
128 }
129 }
130 if !strings.Contains(got, ProactiveSubagentsSystemAddendum) {
131 t.Fatalf("spawn-withheld addendum lost the ownership contract:\n%s", got)
132 }
133 // Fully unavailable delegation falls back to the dedicated notice;
134 // an enabled lifecycle action uses the shorter local-work line.
135 fallback := "Continue non-delegated work locally."
136 if !tc.stop && !tc.resume {
137 fallback = ProactiveSubagentsDelegationUnavailableAddendum
138 }
139 if !strings.Contains(got, fallback) {
140 t.Fatalf("spawn-withheld addendum lost its local-work fallback %q:\n%s", fallback, got)
141 }
142 })
143 }
144
145 // Routing is the interactive collaboration step; the headless strict
146 // contract divides scopes itself and must not inherit it.
147 strict := StrictOrchestratorSystemAddendumFor(SubagentActions{Spawn: true, Stop: false, Resume: false})
148 if strings.Contains(strict, "Route every new task before you work:") || strings.Contains(strict, "[subagents_list]") {
149 t.Fatalf("strict orchestrator inherited interactive routing guidance:\n%s", strict)
150 }
151}
152
153func TestSubagentPoliciesHandleUnavailableDelegationByMode(t *testing.T) {
154 proactive := ProactiveSubagentsSystemAddendumFor(SubagentActions{Spawn: false, Stop: false, Resume: false})
155 if !strings.Contains(proactive, "Continue the user's task locally") || strings.Contains(proactive, "report this limitation") {
156 t.Fatalf("proactive unavailable guidance blocks local work:\n%s", proactive)
157 }
158 strict := StrictOrchestratorSystemAddendumFor(SubagentActions{Spawn: false, Stop: false, Resume: false})
159 if !strings.Contains(strict, "report this limitation") || !strings.Contains(strict, "rather than implementing") {
160 t.Fatalf("strict unavailable guidance permits local implementation:\n%s", strict)
161 }
162}
163
164func TestConfigSettingsStorePersistsShowInstructionsAtStartup(t *testing.T) {
165 t.Setenv("ZUT_HOME", t.TempDir())
166 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
167 t.Fatal(err)
168 }
169
170 if err := (configSettingsStore{}).SetShowInstructionsAtStartup(true); err != nil {
171 t.Fatal(err)
172 }
173 cfg, err := LoadConfig()
174 if err != nil {
175 t.Fatal(err)
176 }
177 if cfg.ShowInstructionsAtStartup == nil || !*cfg.ShowInstructionsAtStartup {
178 t.Fatal("show_instructions_at_startup was not persisted as enabled")
179 }
180 if cfg.Theme != "dark" {
181 t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme)
182 }
183}
184
185func TestConfigSettingsStorePersistsTerminalAlerts(t *testing.T) {
186 t.Setenv("ZUT_HOME", t.TempDir())
187 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
188 t.Fatal(err)
189 }
190
191 if err := (configSettingsStore{}).SetTerminalAlertsEnabled(false); err != nil {
192 t.Fatal(err)
193 }
194 cfg, err := LoadConfig()
195 if err != nil {
196 t.Fatal(err)
197 }
198 if cfg.TerminalAlertsEnabled == nil || *cfg.TerminalAlertsEnabled {
199 t.Fatal("terminal_alerts_enabled was not persisted as disabled")
200 }
201 if cfg.Theme != "dark" {
202 t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme)
203 }
204}
205
206func TestConfigSettingsStorePersistsTerminalTitle(t *testing.T) {
207 t.Setenv("ZUT_HOME", t.TempDir())
208 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
209 t.Fatal(err)
210 }
211
212 if err := (configSettingsStore{}).SetTerminalTitleEnabled(false); err != nil {
213 t.Fatal(err)
214 }
215 cfg, err := LoadConfig()
216 if err != nil {
217 t.Fatal(err)
218 }
219 if cfg.TerminalTitleEnabled == nil || *cfg.TerminalTitleEnabled {
220 t.Fatal("terminal_title_enabled was not persisted as disabled")
221 }
222 if cfg.Theme != "dark" {
223 t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme)
224 }
225}
226
227func TestConfigSettingsStorePersistsJailByDefault(t *testing.T) {
228 t.Setenv("ZUT_HOME", t.TempDir())
229 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
230 t.Fatal(err)
231 }
232
233 if err := (configSettingsStore{}).SetJailByDefault(true); err != nil {
234 t.Fatal(err)
235 }
236 cfg, err := LoadConfig()
237 if err != nil {
238 t.Fatal(err)
239 }
240 if cfg.JailByDefault == nil || !*cfg.JailByDefault {
241 t.Fatal("jail_by_default was not persisted as enabled")
242 }
243 if cfg.Theme != "dark" {
244 t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme)
245 }
246}
247
248func TestConfigSettingsStorePersistsAutoCompactThreshold(t *testing.T) {
249 t.Setenv("ZUT_HOME", t.TempDir())
250 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
251 t.Fatal(err)
252 }
253
254 if err := (configSettingsStore{}).SetAutoCompactThreshold(70); err != nil {
255 t.Fatal(err)
256 }
257 cfg, err := LoadConfig()
258 if err != nil {
259 t.Fatal(err)
260 }
261 if cfg.AutoCompactThreshold == nil || *cfg.AutoCompactThreshold != 70 {
262 t.Fatalf("auto_compact_threshold = %v, want 70", cfg.AutoCompactThreshold)
263 }
264 if cfg.Theme != "dark" {
265 t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme)
266 }
267}
268
269func TestConfigSettingsStorePersistsLSPDefaults(t *testing.T) {
270 t.Setenv("ZUT_HOME", t.TempDir())
271 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
272 t.Fatal(err)
273 }
274
275 if err := (configSettingsStore{}).SetLSPEnabled(false); err != nil {
276 t.Fatal(err)
277 }
278 if err := (configSettingsStore{}).SetSubagentLSPEnabled(false); err != nil {
279 t.Fatal(err)
280 }
281 cfg, err := LoadConfig()
282 if err != nil {
283 t.Fatal(err)
284 }
285 if cfg.LSPEnabled == nil || *cfg.LSPEnabled {
286 t.Fatal("lsp_enabled was not persisted as disabled")
287 }
288 if cfg.SubagentLSPEnabled == nil || *cfg.SubagentLSPEnabled {
289 t.Fatal("subagent_lsp_enabled was not persisted as disabled")
290 }
291 if cfg.Theme != "dark" {
292 t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme)
293 }
294}
295
296func TestConfigLSPEnabledForDefaultsToTrue(t *testing.T) {
297 cfg := Config{}
298 if !cfg.LSPEnabledFor(false) {
299 t.Fatal("main-session LSP should default to enabled")
300 }
301 if !cfg.LSPEnabledFor(true) {
302 t.Fatal("sub-agent LSP should default to enabled")
303 }
304
305 noMain, noSub := false, false
306 cfg.LSPEnabled = &noMain
307 cfg.SubagentLSPEnabled = &noSub
308 if cfg.LSPEnabledFor(false) || cfg.LSPEnabledFor(true) {
309 t.Fatal("explicitly disabled LSP settings were ignored")
310 }
311 yesSub := true
312 cfg.SubagentLSPEnabled = &yesSub
313 write, edit := true, true
314 cfg.LSPDiagnosticsOnWrite = &write
315 cfg.LSPDiagnosticsOnEdit = &edit
316 if cfg.LSPDiagnosticsOnWriteEnabled(false) || cfg.LSPDiagnosticsOnEditEnabled(false) {
317 t.Fatal("diagnostics remained enabled with main-session LSP disabled")
318 }
319 if !cfg.LSPDiagnosticsOnWriteEnabled(true) || !cfg.LSPDiagnosticsOnEditEnabled(true) {
320 t.Fatal("sub-agent diagnostics did not follow sub-agent LSP settings")
321 }
322}
323
324func TestConfigSettingsStoreResetsRemovedInheritedThemeToAuto(t *testing.T) {
325 t.Setenv("ZUT_HOME", t.TempDir())
326 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
327 t.Fatal(err)
328 }
329
330 if err := (configSettingsStore{}).SetTheme("inherited"); err != nil {
331 t.Fatal(err)
332 }
333 cfg, err := LoadConfig()
334 if err != nil {
335 t.Fatal(err)
336 }
337 if cfg.Theme != "" {
338 t.Fatalf("theme = %q, want auto", cfg.Theme)
339 }
340}
341
342func TestConfigWebSearchEnabledForCLIDefaultsToTrue(t *testing.T) {
343 cfg := Config{}
344 if !cfg.WebSearchEnabledForCLI() {
345 t.Fatal("web search should default to enabled when the config field is absent")
346 }
347 disabled := false
348 cfg.WebSearchEnabled = &disabled
349 if cfg.WebSearchEnabledForCLI() {
350 t.Fatal("explicitly disabled web search setting was ignored")
351 }
352}
353
354func TestConfigSettingsStorePersistsWebSearch(t *testing.T) {
355 t.Setenv("ZUT_HOME", t.TempDir())
356 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
357 t.Fatal(err)
358 }
359
360 if err := (configSettingsStore{}).SetWebSearchEnabled(false); err != nil {
361 t.Fatal(err)
362 }
363 cfg, err := LoadConfig()
364 if err != nil {
365 t.Fatal(err)
366 }
367 if cfg.WebSearchEnabled == nil || *cfg.WebSearchEnabled {
368 t.Fatal("web_search_enabled was not persisted as disabled")
369 }
370 if cfg.Theme != "dark" {
371 t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme)
372 }
373}