v0.19.0 d7aa01a

resident_tools_test.go RawHistoryFolder

1package tools
2
3import (
4	"context"
5	"encoding/json"
6	"strings"
7	"testing"
8
9	"github.com/bnema/zut/packages/agent/subagents"
10	"github.com/bnema/zut/packages/core"
11)
12
13func TestResidentToolsUseManagerOnly(t *testing.T) {
14	runs := make(chan string, 2)
15	manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentTurnRunner, error) {
16		return func(_ context.Context, prompt string) error { runs <- prompt; return nil }, nil
17	})
18	t.Cleanup(func() { _ = manager.Close(context.Background()) })
19	spawn := &SubagentSpawnTool{
20		ResidentManager: manager,
21		Enabled:         func() bool { return true },
22		DefaultProvider: func() string { return "openai" },
23		DefaultModel:    func() string { return "gpt-5" },
24		BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
25			return subagents.ResidentChildSpec{ID: "resident-tool", SessionID: "child-session", Provider: request.Provider, Model: request.Model, Required: request.Required}, nil
26		},
27	}
28	result, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"initial"}`), nil)
29	if err != nil {
30		t.Fatal(err)
31	}
32	if len(result.Content) == 0 {
33		t.Fatal("spawn returned no result")
34	}
35	if got := <-runs; got != "initial" {
36		t.Fatalf("initial prompt = %q", got)
37	}
38	status := &SubagentStatusTool{ResidentManager: manager, Enabled: func() bool { return true }}
39	result, err = status.Execute(context.Background(), json.RawMessage(`{"agent_id":"resident-tool"}`), nil)
40	if err != nil {
41		t.Fatal(err)
42	}
43	if details, ok := result.Details.(subagentStatusResponse); !ok || details.Agent == nil || details.Agent.State == "" {
44		t.Fatalf("status details = %#v", result.Details)
45	}
46	resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
47	if _, err := resume.Execute(context.Background(), json.RawMessage(`{"agent_id":"resident-tool","prompt":"follow up"}`), nil); err != nil {
48		t.Fatal(err)
49	}
50	if got := <-runs; got != "follow up" {
51		t.Fatalf("follow-up prompt = %q", got)
52	}
53}
54
55func TestResidentSpawnRejectsInvalidInputBeforeCreatingChild(t *testing.T) {
56	manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentTurnRunner, error) {
57		return func(context.Context, string) error { return nil }, nil
58	})
59	t.Cleanup(func() { _ = manager.Close(context.Background()) })
60	built := 0
61	spawn := &SubagentSpawnTool{
62		ResidentManager: manager,
63		Enabled:         func() bool { return true },
64		BuildResidentSpec: func(context.Context, ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
65			built++
66			return subagents.ResidentChildSpec{ID: "should-not-exist", SessionID: "child"}, nil
67		},
68	}
69	for _, raw := range []string{
70		`{"task":"x","unexpected":true}`,
71		`{"task":"x"}{}`,
72		`{"task":"x","model":"gpt-5"}`,
73		`{"task":"x","isolation":"outside"}`,
74	} {
75		result, err := spawn.Execute(context.Background(), json.RawMessage(raw), nil)
76		if err != nil && !strings.Contains(err.Error(), "invalid args") {
77			t.Fatalf("Execute(%s) error = %v", raw, err)
78		}
79		if err == nil && !result.IsError {
80			t.Fatalf("Execute(%s) = %#v, want protocol error", raw, result)
81		}
82	}
83	if built != 0 || len(manager.Snapshot()) != 0 {
84		t.Fatalf("invalid input reached resident manager: built=%d snapshots=%#v", built, manager.Snapshot())
85	}
86}
87
88func TestResidentSpawnProfileAndExplicitOverridesReachFactory(t *testing.T) {
89	manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentTurnRunner, error) {
90		return func(context.Context, string) error { return nil }, nil
91	})
92	t.Cleanup(func() { _ = manager.Close(context.Background()) })
93	profileFast := false
94	var got ResidentSpawnRequest
95	spawn := &SubagentSpawnTool{
96		ResidentManager:  manager,
97		Enabled:          func() bool { return true },
98		DefaultModel:     func() string { return "host-model" },
99		DefaultProvider:  func() string { return "host-provider" },
100		DefaultReasoning: func() string { return "medium" },
101		ResolveSubagent: func(name string) (*subagents.Profile, error) {
102			return &subagents.Profile{Name: name, Model: "openai/gpt-5.6-sol", Thinking: "low", FastMode: &profileFast}, nil
103		},
104		BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
105			got = request
106			return subagents.ResidentChildSpec{ID: "profiled", SessionID: "child", Provider: request.Provider, Model: request.Model, Profile: request.Profile.Name}, nil
107		},
108	}
109	result, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"review","agent":"reviewer","reasoning":"high","fast_mode":true}`), nil)
110	if err != nil || result.IsError {
111		t.Fatalf("Execute = (%#v, %v)", result, err)
112	}
113	if got.Model != "gpt-5.6-sol" || got.Provider != "openai" || got.Reasoning != "high" || got.FastMode == nil || !*got.FastMode {
114		t.Fatalf("factory request = %#v", got)
115	}
116}
117
118func TestResidentToolsRejectUnknownFields(t *testing.T) {
119	for _, tool := range []interface {
120		Execute(context.Context, json.RawMessage, func(string)) (core.ToolResult, error)
121	}{
122		&SubagentStatusTool{ResidentManager: subagents.NewResidentManager(t.TempDir(), nil), Enabled: func() bool { return true }},
123		&SubagentStopTool{ResidentManager: subagents.NewResidentManager(t.TempDir(), nil), Enabled: func() bool { return true }},
124		&SubagentResumeTool{ResidentManager: subagents.NewResidentManager(t.TempDir(), nil), Enabled: func() bool { return true }},
125	} {
126		_, err := tool.Execute(context.Background(), json.RawMessage(`{"unknown":true}`), nil)
127		if err == nil || !strings.Contains(err.Error(), "unknown field") {
128			t.Fatalf("unknown field error = %v", err)
129		}
130	}
131}
132
133func TestFindResidentStatusSnapshotRejectsAmbiguousPrefix(t *testing.T) {
134	_, ok := findResidentStatusSnapshot([]subagents.ResidentSnapshot{{ID: "resident-abcd"}, {ID: "resident-abef"}}, "resident-ab")
135	if ok {
136		t.Fatal("ambiguous resident prefix resolved")
137	}
138}

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