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