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 TestParseArgsNoContextFiles(t *testing.T) {
192 for _, flag := range []string{"--no-context-files", "-nc"} {
193 args, err := ParseArgs([]string{flag})
194 if err != nil {
195 t.Fatalf("ParseArgs(%q): %v", flag, err)
196 }
197 if !args.NoContextFiles {
198 t.Fatalf("ParseArgs(%q): NoContextFiles = false", flag)
199 }
200 }
201}
202
203func TestParseArgsStream(t *testing.T) {
204 args, err := ParseArgs([]string{"--stream", "hi"})
205 if err != nil {
206 t.Fatal(err)
207 }
208 if args.Mode != ModeStream || args.Prompt != "hi" {
209 t.Fatalf("Mode=%q Prompt=%q", args.Mode, args.Prompt)
210 }
211}
212
213func TestParseArgsWebSearchPolicyIsWorkerOnlyRegardlessOfOrder(t *testing.T) {
214 for _, argv := range [][]string{
215 {"--web-search-policy", "allow"},
216 {"--web-search-policy", "allow", "--subagent-worker", "/tmp/in.sock", "--print"},
217 {"--subagent-worker", "/tmp/in.sock", "--web-search-policy", "deny", "--json"},
218 } {
219 if _, err := ParseArgs(argv); err == nil {
220 t.Fatalf("ordinary CLI mode accepted internal policy from %q", argv)
221 }
222 }
223
224 for _, tc := range []struct {
225 name string
226 argv []string
227 policy subagents.WebSearchPolicy
228 }{
229 {
230 name: "policy before worker",
231 argv: []string{"--web-search-policy", "allow", "--subagent-worker", "/tmp/in.sock", "--tools", "web_search"},
232 policy: subagents.WebSearchAllow,
233 },
234 {
235 name: "policy after worker",
236 argv: []string{"--print", "--subagent-worker", "/tmp/in.sock", "--web-search-policy", "deny"},
237 policy: subagents.WebSearchDeny,
238 },
239 } {
240 t.Run(tc.name, func(t *testing.T) {
241 worker, err := ParseArgs(tc.argv)
242 if err != nil {
243 t.Fatal(err)
244 }
245 if worker.Mode != ModeSubagentWorker || worker.WebSearchPolicy != tc.policy {
246 t.Fatalf("worker policy args = %#v", worker)
247 }
248 })
249 }
250}
251
252func TestParseArgsTracksExplicitToolListProvenance(t *testing.T) {
253 absent, err := ParseArgs(nil)
254 if err != nil {
255 t.Fatal(err)
256 }
257 if absent.ToolsSet || len(absent.Tools) != 0 {
258 t.Fatalf("absent --tools = %#v", absent)
259 }
260
261 empty, err := ParseArgs([]string{"--tools", ""})
262 if err != nil {
263 t.Fatal(err)
264 }
265 if !empty.ToolsSet || len(empty.Tools) != 0 {
266 t.Fatalf("empty --tools = %#v", empty)
267 }
268
269 selected, err := ParseArgs([]string{"--tools", "read, web_search"})
270 if err != nil {
271 t.Fatal(err)
272 }
273 if !selected.ToolsSet || len(selected.Tools) != 2 || selected.Tools[1] != "web_search" {
274 t.Fatalf("selected --tools = %#v", selected)
275 }
276}