v0.51.0 a9d674e

settings_store_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close