v0.44.0 efd47b3

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 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}

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