v0.2.0 42a5079

auto_subagents_policy_test.go RawHistoryFolder

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}

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 477

↑↓ moveenter openesc close