1package modes
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/bnema/zut/packages/agent/subagents"
10 "github.com/bnema/zut/packages/core"
11 "github.com/bnema/zut/packages/provider"
12)
13
14func TestAutoSubagentsSettingsDisableUnavailablePolicy(t *testing.T) {
15 trueValue := true
16 falseValue := false
17
18 tests := []struct {
19 name string
20 supervisor *subagents.Supervisor
21 allowed *bool
22 wantHint string
23 }{
24 {
25 name: "supervisor unavailable",
26 allowed: &trueValue,
27 wantHint: "subagent supervisor not available in this mode",
28 },
29 {
30 name: "launch policy excludes tool",
31 supervisor: subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()}),
32 allowed: &falseValue,
33 wantHint: "launch-time tool policy excludes subagent_spawn",
34 },
35 }
36 for _, tt := range tests {
37 t.Run(tt.name, func(t *testing.T) {
38 if tt.supervisor != nil {
39 t.Cleanup(tt.supervisor.StopAll)
40 }
41 iv := &Interactive{
42 cfg: InteractiveConfig{
43 AutoSubagentsEnabled: &trueValue,
44 AutoSubagentsToolAllowed: tt.allowed,
45 Supervisor: tt.supervisor,
46 },
47 settingsDialog: newSettingsDialog(),
48 }
49 iv.openSettingsDialog()
50
51 var item settingsItem
52 for _, candidate := range iv.settingsDialog.items {
53 if candidate.key == "auto_subagents_enabled" {
54 item = candidate
55 break
56 }
57 }
58 if !item.disabled {
59 t.Fatal("auto-subagents setting is not disabled")
60 }
61 if item.value {
62 t.Fatal("disabled auto-subagents setting is shown as enabled")
63 }
64 if !strings.Contains(item.hint, tt.wantHint) {
65 t.Fatalf("hint = %q; want %q", item.hint, tt.wantHint)
66 }
67 })
68 }
69}
70
71func TestAutoSubagentsToolRegistrationHonorsLaunchPolicy(t *testing.T) {
72 allowed := false
73 enabled := false
74 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
75 t.Cleanup(supervisor.StopAll)
76 iv := &Interactive{
77 agent: &core.Agent{Tools: core.Registry{}},
78 cfg: InteractiveConfig{
79 AutoSubagentsEnabled: &enabled,
80 AutoSubagentsToolAllowed: &allowed,
81 Supervisor: supervisor,
82 },
83 dirty: make(chan struct{}, 1),
84 }
85
86 iv.applyAutoSubagentsTool(true)
87 if _, ok := iv.agent.Tools["subagent_spawn"]; ok {
88 t.Fatal("subagent_spawn registered despite launch-time policy")
89 }
90 if _, ok := iv.agent.Tools["subagent_status"]; ok {
91 t.Fatal("subagent_status registered despite launch-time policy")
92 }
93 iv.applySettingToggle("auto_subagents_enabled", true)
94 iv.mu.Lock()
95 statusErr := iv.statusErr
96 value := iv.cfg.AutoSubagentsEnabled != nil && *iv.cfg.AutoSubagentsEnabled
97 iv.mu.Unlock()
98 if value {
99 t.Fatal("auto-subagents toggle enabled despite launch-time policy")
100 }
101 if !strings.Contains(statusErr, "launch-time tool policy") {
102 t.Fatalf("toggle error = %q; want launch-time policy hint", statusErr)
103 }
104}
105
106func TestAutoSubagentsToolRegistrationHonorsSeparateLaunchPolicies(t *testing.T) {
107 for _, tc := range []struct {
108 name string
109 spawnAllowed bool
110 statusAllowed bool
111 wantSpawn bool
112 wantStatus bool
113 }{
114 {name: "status only", statusAllowed: true, wantStatus: true},
115 {name: "spawn only", spawnAllowed: true, wantSpawn: true},
116 {name: "both", spawnAllowed: true, statusAllowed: true, wantSpawn: true, wantStatus: true},
117 } {
118 t.Run(tc.name, func(t *testing.T) {
119 enabled := true
120 spawnAllowed := tc.spawnAllowed
121 statusAllowed := tc.statusAllowed
122 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
123 t.Cleanup(supervisor.StopAll)
124 iv := &Interactive{
125 agent: &core.Agent{Tools: core.Registry{}},
126 cfg: InteractiveConfig{
127 AutoSubagentsEnabled: &enabled,
128 AutoSubagentsToolAllowed: &spawnAllowed,
129 AutoSubagentsStatusToolAllowed: &statusAllowed,
130 Supervisor: supervisor,
131 },
132 dirty: make(chan struct{}, 1),
133 }
134
135 iv.applyAutoSubagentsTool(true)
136 _, gotSpawn := iv.agent.Tools["subagent_spawn"]
137 _, gotStatus := iv.agent.Tools["subagent_status"]
138 if gotSpawn != tc.wantSpawn || gotStatus != tc.wantStatus {
139 t.Fatalf("registered tools spawn=%v status=%v, want spawn=%v status=%v", gotSpawn, gotStatus, tc.wantSpawn, tc.wantStatus)
140 }
141 })
142 }
143}
144
145type serializedAutoSubagentsClient struct {
146 started chan struct{}
147 request chan provider.Request
148 release chan struct{}
149 finished chan struct{}
150}
151
152func (c *serializedAutoSubagentsClient) Name() string { return "serialized-auto-subagents-test" }
153
154func (c *serializedAutoSubagentsClient) Stream(ctx context.Context, req provider.Request) (<-chan provider.Event, error) {
155 c.request <- req
156 c.started <- struct{}{}
157 out := make(chan provider.Event, 1)
158 go func() {
159 defer close(out)
160 select {
161 case <-c.release:
162 out <- provider.EventDone{Stop: provider.StopEnd}
163 case <-ctx.Done():
164 out <- provider.EventDone{Stop: provider.StopAborted, Err: ctx.Err()}
165 }
166 close(c.finished)
167 }()
168 return out, nil
169}
170
171func TestAutoSubagentsPromptConfigurationIsSynchronized(t *testing.T) {
172 client := &serializedAutoSubagentsClient{
173 started: make(chan struct{}, 1),
174 request: make(chan provider.Request, 1),
175 release: make(chan struct{}),
176 finished: make(chan struct{}),
177 }
178 agent := core.NewAgent(client, "test-model", "base system", nil)
179 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
180 t.Cleanup(supervisor.StopAll)
181 allowed := true
182 iv := NewInteractive(InteractiveConfig{
183 Agent: agent,
184 Supervisor: supervisor,
185 AutoSubagentsToolAllowed: &allowed,
186 AutoSubagentsSystemAddendum: "delegation guidance",
187 })
188
189 iv.startTurn(context.Background(), "hello")
190 select {
191 case <-client.started:
192 case <-time.After(time.Second):
193 t.Fatal("prompt did not reach provider")
194 }
195
196 request := <-client.request
197 if request.System != "base system" {
198 t.Fatalf("provider request system = %q; want base system snapshot", request.System)
199 }
200 applied := make(chan struct{})
201 go func() {
202 iv.applyAutoSubagentsSystemPrompt(true)
203 close(applied)
204 }()
205 select {
206 case <-applied:
207 case <-time.After(time.Second):
208 t.Fatal("system-prompt update did not apply while provider was running")
209 }
210 if system, _ := agent.PromptConfig(); !strings.Contains(system, "delegation guidance") {
211 t.Fatalf("system prompt missing applied guidance: %q", system)
212 }
213
214 close(client.release)
215 select {
216 case <-client.finished:
217 case <-time.After(time.Second):
218 t.Fatal("prompt did not finish after provider release")
219 }
220}