test(subagents): cover mode-specific fallbacks
- commit
- 825879476197de64a3dea533055af9f6bb020d3c
- author
- brice · · 2026-08-24 11:02 UTC
- parent
- bb1a57d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +16 -8
M README.md +1 -1
| @@ -439,7 +439,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 439 | 439 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | |
| 440 | 440 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | |
| 441 | 441 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zut: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`. | |
| 442 | - | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven through `[auto-subagents update]` for every worker; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | |
| 442 | + | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | |
| 443 | 443 | - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents. | |
| 444 | 444 | - **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | |
| 445 | 445 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | |
| @@ -439,7 +439,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 439 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | 439 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | ||
| 440 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | 440 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | ||
| 441 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zut: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`. | 441 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zut: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`. | ||
| 442 | - | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven through `[auto-subagents update]` for every worker; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | 442 | + | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. |
| 443 | - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents. | 443 | - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents. | ||
| 444 | - **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | 444 | - **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | ||
| 445 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | 445 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | ||
M packages/agent/settings_store_test.go +15 -7
| @@ -6,12 +6,16 @@ import ( | |||
| 6 | 6 | ) | |
| 7 | 7 | ||
| 8 | 8 | func TestSubagentsSystemAddendaListOnlyEnabledLifecycleTools(t *testing.T) { | |
| 9 | - | builders := map[string]func(bool, bool, bool) string{ | |
| 10 | - | "proactive": ProactiveSubagentsSystemAddendumFor, | |
| 11 | - | "strict": StrictOrchestratorSystemAddendumFor, | |
| 12 | - | } | |
| 13 | - | for name, build := range builders { | |
| 14 | - | t.Run(name, func(t *testing.T) { | |
| 9 | + | builders := []struct { | |
| 10 | + | name string | |
| 11 | + | build func(bool, bool, bool) string | |
| 12 | + | noSpawnFallback string | |
| 13 | + | }{ | |
| 14 | + | {name: "proactive", build: ProactiveSubagentsSystemAddendumFor, noSpawnFallback: "Continue non-delegated work locally."}, | |
| 15 | + | {name: "strict", build: StrictOrchestratorSystemAddendumFor, noSpawnFallback: "Do not implement, debug, test, or review directly."}, | |
| 16 | + | } | |
| 17 | + | for _, builder := range builders { | |
| 18 | + | t.Run(builder.name, func(t *testing.T) { | |
| 15 | 19 | for _, tc := range []struct { | |
| 16 | 20 | name string | |
| 17 | 21 | spawn bool | |
| @@ -28,9 +32,13 @@ func TestSubagentsSystemAddendaListOnlyEnabledLifecycleTools(t *testing.T) { | |||
| 28 | 32 | {name: "both", spawn: true, stop: true, resume: true, want: []string{"subagent_stop", "subagent_resume"}, available: true}, | |
| 29 | 33 | {name: "stop without spawn", stop: true, want: []string{"subagent_stop", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_resume"}}, | |
| 30 | 34 | {name: "resume without spawn", resume: true, want: []string{"subagent_resume", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_stop"}}, | |
| 35 | + | {name: "both without spawn", stop: true, resume: true, want: []string{"subagent_stop", "subagent_resume", "Spawning new workers is unavailable"}}, | |
| 31 | 36 | } { | |
| 32 | 37 | t.Run(tc.name, func(t *testing.T) { | |
| 33 | - | got := build(tc.spawn, tc.stop, tc.resume) | |
| 38 | + | got := builder.build(tc.spawn, tc.stop, tc.resume) | |
| 39 | + | if !tc.spawn && (tc.stop || tc.resume) && !strings.Contains(got, builder.noSpawnFallback) { | |
| 40 | + | t.Fatalf("addendum missing mode-specific no-spawn fallback %q:\n%s", builder.noSpawnFallback, got) | |
| 41 | + | } | |
| 34 | 42 | for _, want := range tc.want { | |
| 35 | 43 | if !strings.Contains(got, want) { | |
| 36 | 44 | t.Fatalf("addendum missing enabled lifecycle tool %q:\n%s", want, got) | |
| @@ -6,12 +6,16 @@ import ( | |||||
| 6 | ) | 6 | ) | ||
| 7 | 7 | ||||
| 8 | func TestSubagentsSystemAddendaListOnlyEnabledLifecycleTools(t *testing.T) { | 8 | func TestSubagentsSystemAddendaListOnlyEnabledLifecycleTools(t *testing.T) { | ||
| 9 | - | builders := map[string]func(bool, bool, bool) string{ | 9 | + | builders := []struct { |
| 10 | - | "proactive": ProactiveSubagentsSystemAddendumFor, | 10 | + | name string |
| 11 | - | "strict": StrictOrchestratorSystemAddendumFor, | 11 | + | build func(bool, bool, bool) string |
| 12 | - | } | 12 | + | noSpawnFallback string |
| 13 | - | for name, build := range builders { | 13 | + | }{ |
| 14 | - | t.Run(name, func(t *testing.T) { | 14 | + | {name: "proactive", build: ProactiveSubagentsSystemAddendumFor, noSpawnFallback: "Continue non-delegated work locally."}, |
| 15 | + | {name: "strict", build: StrictOrchestratorSystemAddendumFor, noSpawnFallback: "Do not implement, debug, test, or review directly."}, | |||
| 16 | + | } | |||
| 17 | + | for _, builder := range builders { | |||
| 18 | + | t.Run(builder.name, func(t *testing.T) { | |||
| 15 | for _, tc := range []struct { | 19 | for _, tc := range []struct { | ||
| 16 | name string | 20 | name string | ||
| 17 | spawn bool | 21 | spawn bool | ||
| @@ -28,9 +32,13 @@ func TestSubagentsSystemAddendaListOnlyEnabledLifecycleTools(t *testing.T) { | |||||
| 28 | {name: "both", spawn: true, stop: true, resume: true, want: []string{"subagent_stop", "subagent_resume"}, available: true}, | 32 | {name: "both", spawn: true, stop: true, resume: true, want: []string{"subagent_stop", "subagent_resume"}, available: true}, | ||
| 29 | {name: "stop without spawn", stop: true, want: []string{"subagent_stop", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_resume"}}, | 33 | {name: "stop without spawn", stop: true, want: []string{"subagent_stop", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_resume"}}, | ||
| 30 | {name: "resume without spawn", resume: true, want: []string{"subagent_resume", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_stop"}}, | 34 | {name: "resume without spawn", resume: true, want: []string{"subagent_resume", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_stop"}}, | ||
| 35 | + | {name: "both without spawn", stop: true, resume: true, want: []string{"subagent_stop", "subagent_resume", "Spawning new workers is unavailable"}}, | |||
| 31 | } { | 36 | } { | ||
| 32 | t.Run(tc.name, func(t *testing.T) { | 37 | t.Run(tc.name, func(t *testing.T) { | ||
| 33 | - | got := build(tc.spawn, tc.stop, tc.resume) | 38 | + | got := builder.build(tc.spawn, tc.stop, tc.resume) |
| 39 | + | if !tc.spawn && (tc.stop || tc.resume) && !strings.Contains(got, builder.noSpawnFallback) { | |||
| 40 | + | t.Fatalf("addendum missing mode-specific no-spawn fallback %q:\n%s", builder.noSpawnFallback, got) | |||
| 41 | + | } | |||
| 34 | for _, want := range tc.want { | 42 | for _, want := range tc.want { | ||
| 35 | if !strings.Contains(got, want) { | 43 | if !strings.Contains(got, want) { | ||
| 36 | t.Fatalf("addendum missing enabled lifecycle tool %q:\n%s", want, got) | 44 | t.Fatalf("addendum missing enabled lifecycle tool %q:\n%s", want, got) | ||