1package agent
2
3import (
4 "os"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/bnema/zut/packages/agent/subagents"
10 "github.com/bnema/zut/packages/agent/tools"
11 "github.com/google/uuid"
12)
13
14func TestParseArgsOrchestrate(t *testing.T) {
15 for _, mode := range []string{"--print", "--stream", "--json"} {
16 args, err := ParseArgs([]string{mode, "--orchestrate", "task"})
17 if err != nil {
18 t.Fatalf("ParseArgs(%q): %v", mode, err)
19 }
20 if !args.Orchestrate {
21 t.Fatalf("ParseArgs(%q): Orchestrate = false", mode)
22 }
23 }
24}
25
26func TestValidateOrchestrationArgs(t *testing.T) {
27 valid := Args{Mode: ModePrint, Orchestrate: true}
28 if err := validateOrchestrationArgs(valid); err != nil {
29 t.Fatalf("valid orchestration args: %v", err)
30 }
31 for _, tc := range []struct {
32 name string
33 args Args
34 want string
35 }{
36 {name: "interactive", args: Args{Mode: ModeInteractive, Orchestrate: true}, want: "print, stream, or JSON"},
37 {name: "rpc", args: Args{Mode: ModeRPC, Orchestrate: true}, want: "RPC"},
38 {name: "worker", args: Args{Mode: ModeSubagentWorker, Orchestrate: true}, want: "subagent-worker"},
39 {name: "profile", args: Args{Mode: ModePrint, Orchestrate: true, Subagent: "reviewer"}, want: "--subagent profiles"},
40 {name: "stats", args: Args{Mode: ModePrint, Orchestrate: true, StatsPath: "stats.json"}, want: "--stats"},
41 {name: "packaged", args: Args{Mode: ModePrint, Orchestrate: true, PermissionSet: &tools.PermissionSet{}}, want: "packaged"},
42 {name: "packaged agent name", args: Args{Mode: ModePrint, Orchestrate: true, AgentName: "bundled"}, want: "packaged"},
43 {name: "packaged agent data dir", args: Args{Mode: ModePrint, Orchestrate: true, AgentDataDir: "/tmp/agent-data"}, want: "packaged"},
44 {name: "missing spawn", args: Args{Mode: ModePrint, Orchestrate: true, ToolsSet: true, Tools: []string{"read"}}, want: "subagent_spawn"},
45 } {
46 t.Run(tc.name, func(t *testing.T) {
47 err := validateOrchestrationArgs(tc.args)
48 if err == nil || !strings.Contains(err.Error(), tc.want) {
49 t.Fatalf("error = %v, want substring %q", err, tc.want)
50 }
51 })
52 }
53}
54
55func TestInvalidOrchestrationRejectsBeforeRuntimeCatalogSideEffects(t *testing.T) {
56 home := t.TempDir()
57 t.Setenv("ZUT_HOME", home)
58
59 err := runWithArgsRaw([]string{"--orchestrate", "task"}, "test")
60 if err == nil || !strings.Contains(err.Error(), "requires print, stream, or JSON") {
61 t.Fatalf("run error = %v, want mode validation error", err)
62 }
63 entries, readErr := os.ReadDir(home)
64 if readErr != nil {
65 t.Fatal(readErr)
66 }
67 if len(entries) != 0 {
68 t.Fatalf("invalid orchestration created runtime state: %v", entries)
69 }
70}
71
72func TestParseArgsSubagentAndReasoning(t *testing.T) {
73 args, err := ParseArgs([]string{"--subagent-worker", "/tmp/in.sock", "--subagent", "reviewer", "--reasoning", "high", "task"})
74 if err != nil {
75 t.Fatal(err)
76 }
77 if args.Mode != ModeSubagentWorker || args.Subagent != "reviewer" || args.Reasoning != "high" || args.Prompt != "task" {
78 t.Fatalf("parsed args = mode=%q subagent=%q reasoning=%q prompt=%q", args.Mode, args.Subagent, args.Reasoning, args.Prompt)
79 }
80}
81
82func TestParseArgsSubagentTurnTimeout(t *testing.T) {
83 args, err := ParseArgs([]string{"--subagent-worker", "/tmp/in.sock", "--subagent-turn-timeout", "15m"})
84 if err != nil {
85 t.Fatal(err)
86 }
87 if args.SubagentTurnTimeout != 15*time.Minute {
88 t.Fatalf("turn timeout = %s, want %s", args.SubagentTurnTimeout, 15*time.Minute)
89 }
90
91 for _, value := range []string{"0s", "-1s", "invalid"} {
92 if _, err := ParseArgs([]string{"--subagent-worker", "/tmp/in.sock", "--subagent-turn-timeout", value}); err == nil {
93 t.Errorf("timeout %q unexpectedly accepted", value)
94 }
95 }
96}
97
98func TestParseArgsNoLSP(t *testing.T) {
99 args, err := ParseArgs([]string{"--no-lsp"})
100 if err != nil {
101 t.Fatal(err)
102 }
103 if !args.NoLSP {
104 t.Fatal("--no-lsp did not disable LSP")
105 }
106}
107
108func TestParseArgsResumeOptionalSessionID(t *testing.T) {
109 id := uuid.New()
110
111 picker, err := ParseArgs([]string{"--resume"})
112 if err != nil {
113 t.Fatal(err)
114 }
115 if !picker.Resume || picker.ResumeSessionID != "" || picker.Prompt != "" {
116 t.Fatalf("no-argument resume = %#v, want picker behavior", picker)
117 }
118
119 args, err := ParseArgs([]string{"--resume", id.String(), "continue this"})
120 if err != nil {
121 t.Fatal(err)
122 }
123 if !args.Resume || args.ResumeSessionID != id.String() || args.Prompt != "continue this" {
124 t.Fatalf("resume UUID args = %#v", args)
125 }
126
127 prompt, err := ParseArgs([]string{"--resume", "not-a-session-id"})
128 if err != nil {
129 t.Fatal(err)
130 }
131 if prompt.ResumeSessionID != "" || prompt.Prompt != "not-a-session-id" {
132 t.Fatalf("non-UUID resume argument = %#v, want existing prompt behavior", prompt)
133 }
134}
135
136func TestParseArgsAllowsLeadingDashPromptAfterTerminator(t *testing.T) {
137 args, err := ParseArgs([]string{"--subagent-worker", "/tmp/in.sock", "--", "--inspect", "auth flow"})
138 if err != nil {
139 t.Fatal(err)
140 }
141 if args.Prompt != "--inspect auth flow" {
142 t.Fatalf("prompt = %q, want leading-dash task preserved", args.Prompt)
143 }
144}
145
146func TestParseArgsTemperatureAllowsZero(t *testing.T) {
147 args, err := ParseArgs([]string{"--temperature", "0"})
148 if err != nil {
149 t.Fatalf("ParseArgs returned %v", err)
150 }
151 if args.Temperature == nil || *args.Temperature != 0 {
152 t.Fatalf("Temperature = %v; want 0", args.Temperature)
153 }
154}
155
156func TestParseArgsTemperatureRejectsOutOfRange(t *testing.T) {
157 if _, err := ParseArgs([]string{"--temperature", "2.1"}); err == nil {
158 t.Fatal("ParseArgs accepted out-of-range temperature")
159 }
160}
161
162func TestParseArgsYes(t *testing.T) {
163 for _, flag := range []string{"-y", "--yes"} {
164 args, err := ParseArgs([]string{flag, "--print", "hi"})
165 if err != nil {
166 t.Fatalf("ParseArgs(%q): %v", flag, err)
167 }
168 if !args.Yes {
169 t.Fatalf("ParseArgs(%q): Yes = false", flag)
170 }
171 if args.Mode != ModePrint || args.Prompt != "hi" {
172 t.Fatalf("ParseArgs(%q): Mode=%q Prompt=%q", flag, args.Mode, args.Prompt)
173 }
174 }
175}
176
177func TestParseArgsStatsRequiresPrintMode(t *testing.T) {
178 args, err := ParseArgs([]string{"-p", "--stats", "stats.json", "hi"})
179 if err != nil {
180 t.Fatal(err)
181 }
182 if args.StatsPath != "stats.json" || args.Mode != ModePrint {
183 t.Fatalf("StatsPath=%q Mode=%q", args.StatsPath, args.Mode)
184 }
185
186 if _, err := ParseArgs([]string{"--stats", "stats.json", "hi"}); err == nil {
187 t.Fatal("ParseArgs accepted --stats without print mode")
188 }
189}
190
191func TestParseArgsStream(t *testing.T) {
192 args, err := ParseArgs([]string{"--stream", "hi"})
193 if err != nil {
194 t.Fatal(err)
195 }
196 if args.Mode != ModeStream || args.Prompt != "hi" {
197 t.Fatalf("Mode=%q Prompt=%q", args.Mode, args.Prompt)
198 }
199}
200
201func TestParseArgsWebSearchPolicyIsWorkerOnlyRegardlessOfOrder(t *testing.T) {
202 for _, argv := range [][]string{
203 {"--web-search-policy", "allow"},
204 {"--web-search-policy", "allow", "--subagent-worker", "/tmp/in.sock", "--print"},
205 {"--subagent-worker", "/tmp/in.sock", "--web-search-policy", "deny", "--json"},
206 } {
207 if _, err := ParseArgs(argv); err == nil {
208 t.Fatalf("ordinary CLI mode accepted internal policy from %q", argv)
209 }
210 }
211
212 for _, tc := range []struct {
213 name string
214 argv []string
215 policy subagents.WebSearchPolicy
216 }{
217 {
218 name: "policy before worker",
219 argv: []string{"--web-search-policy", "allow", "--subagent-worker", "/tmp/in.sock", "--tools", "web_search"},
220 policy: subagents.WebSearchAllow,
221 },
222 {
223 name: "policy after worker",
224 argv: []string{"--print", "--subagent-worker", "/tmp/in.sock", "--web-search-policy", "deny"},
225 policy: subagents.WebSearchDeny,
226 },
227 } {
228 t.Run(tc.name, func(t *testing.T) {
229 worker, err := ParseArgs(tc.argv)
230 if err != nil {
231 t.Fatal(err)
232 }
233 if worker.Mode != ModeSubagentWorker || worker.WebSearchPolicy != tc.policy {
234 t.Fatalf("worker policy args = %#v", worker)
235 }
236 })
237 }
238}
239
240func TestParseArgsTracksExplicitToolListProvenance(t *testing.T) {
241 absent, err := ParseArgs(nil)
242 if err != nil {
243 t.Fatal(err)
244 }
245 if absent.ToolsSet || len(absent.Tools) != 0 {
246 t.Fatalf("absent --tools = %#v", absent)
247 }
248
249 empty, err := ParseArgs([]string{"--tools", ""})
250 if err != nil {
251 t.Fatal(err)
252 }
253 if !empty.ToolsSet || len(empty.Tools) != 0 {
254 t.Fatalf("empty --tools = %#v", empty)
255 }
256
257 selected, err := ParseArgs([]string{"--tools", "read, web_search"})
258 if err != nil {
259 t.Fatal(err)
260 }
261 if !selected.ToolsSet || len(selected.Tools) != 2 || selected.Tools[1] != "web_search" {
262 t.Fatalf("selected --tools = %#v", selected)
263 }
264}