blob: 6904447fd87b846200a2422e3cb121d777545c9a [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001package claudetool
2
3import (
4 "context"
5 "encoding/json"
Philip Zeyligerb60f0f22025-04-23 18:19:32 +00006 "os"
7 "path/filepath"
Earl Lee2e463fb2025-04-17 11:22:22 -07008 "strings"
Philip Zeyligerb60f0f22025-04-23 18:19:32 +00009 "syscall"
Earl Lee2e463fb2025-04-17 11:22:22 -070010 "testing"
11 "time"
12)
13
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000014func TestBashTool(t *testing.T) {
Earl Lee2e463fb2025-04-17 11:22:22 -070015 // Test basic functionality
16 t.Run("Basic Command", func(t *testing.T) {
17 input := json.RawMessage(`{"command":"echo 'Hello, world!'"}`)
18
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000019 result, err := Bash.Run(context.Background(), input)
Earl Lee2e463fb2025-04-17 11:22:22 -070020 if err != nil {
21 t.Fatalf("Unexpected error: %v", err)
22 }
23
24 expected := "Hello, world!\n"
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000025 if len(result) == 0 || result[0].Text != expected {
26 t.Errorf("Expected %q, got %q", expected, result[0].Text)
Earl Lee2e463fb2025-04-17 11:22:22 -070027 }
28 })
29
30 // Test with arguments
31 t.Run("Command With Arguments", func(t *testing.T) {
32 input := json.RawMessage(`{"command":"echo -n foo && echo -n bar"}`)
33
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000034 result, err := Bash.Run(context.Background(), input)
Earl Lee2e463fb2025-04-17 11:22:22 -070035 if err != nil {
36 t.Fatalf("Unexpected error: %v", err)
37 }
38
39 expected := "foobar"
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000040 if len(result) == 0 || result[0].Text != expected {
41 t.Errorf("Expected %q, got %q", expected, result[0].Text)
Earl Lee2e463fb2025-04-17 11:22:22 -070042 }
43 })
44
45 // Test with timeout parameter
46 t.Run("With Timeout", func(t *testing.T) {
47 inputObj := struct {
48 Command string `json:"command"`
49 Timeout string `json:"timeout"`
50 }{
51 Command: "sleep 0.1 && echo 'Completed'",
52 Timeout: "5s",
53 }
54 inputJSON, err := json.Marshal(inputObj)
55 if err != nil {
56 t.Fatalf("Failed to marshal input: %v", err)
57 }
58
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000059 result, err := Bash.Run(context.Background(), inputJSON)
Earl Lee2e463fb2025-04-17 11:22:22 -070060 if err != nil {
61 t.Fatalf("Unexpected error: %v", err)
62 }
63
64 expected := "Completed\n"
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000065 if len(result) == 0 || result[0].Text != expected {
66 t.Errorf("Expected %q, got %q", expected, result[0].Text)
Earl Lee2e463fb2025-04-17 11:22:22 -070067 }
68 })
69
70 // Test command timeout
71 t.Run("Command Timeout", func(t *testing.T) {
72 inputObj := struct {
73 Command string `json:"command"`
74 Timeout string `json:"timeout"`
75 }{
76 Command: "sleep 0.5 && echo 'Should not see this'",
77 Timeout: "100ms",
78 }
79 inputJSON, err := json.Marshal(inputObj)
80 if err != nil {
81 t.Fatalf("Failed to marshal input: %v", err)
82 }
83
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000084 _, err = Bash.Run(context.Background(), inputJSON)
Earl Lee2e463fb2025-04-17 11:22:22 -070085 if err == nil {
86 t.Errorf("Expected timeout error, got none")
87 } else if !strings.Contains(err.Error(), "timed out") {
88 t.Errorf("Expected timeout error, got: %v", err)
89 }
90 })
91
92 // Test command that fails
93 t.Run("Failed Command", func(t *testing.T) {
94 input := json.RawMessage(`{"command":"exit 1"}`)
95
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000096 _, err := Bash.Run(context.Background(), input)
Earl Lee2e463fb2025-04-17 11:22:22 -070097 if err == nil {
98 t.Errorf("Expected error for failed command, got none")
99 }
100 })
101
102 // Test invalid input
103 t.Run("Invalid JSON Input", func(t *testing.T) {
104 input := json.RawMessage(`{"command":123}`) // Invalid JSON (command must be string)
105
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000106 _, err := Bash.Run(context.Background(), input)
Earl Lee2e463fb2025-04-17 11:22:22 -0700107 if err == nil {
108 t.Errorf("Expected error for invalid input, got none")
109 }
110 })
111}
112
113func TestExecuteBash(t *testing.T) {
114 ctx := context.Background()
115
116 // Test successful command
117 t.Run("Successful Command", func(t *testing.T) {
118 req := bashInput{
119 Command: "echo 'Success'",
120 Timeout: "5s",
121 }
122
123 output, err := executeBash(ctx, req)
124 if err != nil {
125 t.Fatalf("Unexpected error: %v", err)
126 }
127
128 want := "Success\n"
129 if output != want {
130 t.Errorf("Expected %q, got %q", want, output)
131 }
132 })
133
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100134 // Test SKETCH=1 environment variable is set
135 t.Run("SKETCH Environment Variable", func(t *testing.T) {
136 req := bashInput{
137 Command: "echo $SKETCH",
138 Timeout: "5s",
139 }
140
141 output, err := executeBash(ctx, req)
142 if err != nil {
143 t.Fatalf("Unexpected error: %v", err)
144 }
145
146 want := "1\n"
147 if output != want {
148 t.Errorf("Expected SKETCH=1, got %q", output)
149 }
150 })
151
Earl Lee2e463fb2025-04-17 11:22:22 -0700152 // Test command with output to stderr
153 t.Run("Command with stderr", func(t *testing.T) {
154 req := bashInput{
155 Command: "echo 'Error message' >&2 && echo 'Success'",
156 Timeout: "5s",
157 }
158
159 output, err := executeBash(ctx, req)
160 if err != nil {
161 t.Fatalf("Unexpected error: %v", err)
162 }
163
164 want := "Error message\nSuccess\n"
165 if output != want {
166 t.Errorf("Expected %q, got %q", want, output)
167 }
168 })
169
170 // Test command that fails with stderr
171 t.Run("Failed Command with stderr", func(t *testing.T) {
172 req := bashInput{
173 Command: "echo 'Error message' >&2 && exit 1",
174 Timeout: "5s",
175 }
176
177 _, err := executeBash(ctx, req)
178 if err == nil {
179 t.Errorf("Expected error for failed command, got none")
180 } else if !strings.Contains(err.Error(), "Error message") {
181 t.Errorf("Expected stderr in error message, got: %v", err)
182 }
183 })
184
185 // Test timeout
186 t.Run("Command Timeout", func(t *testing.T) {
187 req := bashInput{
188 Command: "sleep 1 && echo 'Should not see this'",
189 Timeout: "100ms",
190 }
191
192 start := time.Now()
193 _, err := executeBash(ctx, req)
194 elapsed := time.Since(start)
195
196 // Command should time out after ~100ms, not wait for full 1 second
197 if elapsed >= 1*time.Second {
198 t.Errorf("Command did not respect timeout, took %v", elapsed)
199 }
200
201 if err == nil {
202 t.Errorf("Expected timeout error, got none")
203 } else if !strings.Contains(err.Error(), "timed out") {
204 t.Errorf("Expected timeout error, got: %v", err)
205 }
206 })
207}
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000208
209func TestBackgroundBash(t *testing.T) {
210 // Test basic background execution
211 t.Run("Basic Background Command", func(t *testing.T) {
212 inputObj := struct {
213 Command string `json:"command"`
214 Background bool `json:"background"`
215 }{
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100216 Command: "echo 'Hello from background' $SKETCH",
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000217 Background: true,
218 }
219 inputJSON, err := json.Marshal(inputObj)
220 if err != nil {
221 t.Fatalf("Failed to marshal input: %v", err)
222 }
223
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000224 result, err := Bash.Run(context.Background(), inputJSON)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000225 if err != nil {
226 t.Fatalf("Unexpected error: %v", err)
227 }
228
229 // Parse the returned JSON
230 var bgResult BackgroundResult
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000231 resultStr := result[0].Text
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700232 if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000233 t.Fatalf("Failed to unmarshal background result: %v", err)
234 }
235
236 // Verify we got a valid PID
237 if bgResult.PID <= 0 {
238 t.Errorf("Invalid PID returned: %d", bgResult.PID)
239 }
240
241 // Verify output files exist
242 if _, err := os.Stat(bgResult.StdoutFile); os.IsNotExist(err) {
243 t.Errorf("Stdout file doesn't exist: %s", bgResult.StdoutFile)
244 }
245 if _, err := os.Stat(bgResult.StderrFile); os.IsNotExist(err) {
246 t.Errorf("Stderr file doesn't exist: %s", bgResult.StderrFile)
247 }
248
249 // Wait for the command output to be written to file
250 waitForFile(t, bgResult.StdoutFile)
251
252 // Check file contents
253 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
254 if err != nil {
255 t.Fatalf("Failed to read stdout file: %v", err)
256 }
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100257 expected := "Hello from background 1\n"
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000258 if string(stdoutContent) != expected {
259 t.Errorf("Expected stdout content %q, got %q", expected, string(stdoutContent))
260 }
261
262 // Clean up
263 os.Remove(bgResult.StdoutFile)
264 os.Remove(bgResult.StderrFile)
265 os.Remove(filepath.Dir(bgResult.StdoutFile))
266 })
267
268 // Test background command with stderr output
269 t.Run("Background Command with stderr", func(t *testing.T) {
270 inputObj := struct {
271 Command string `json:"command"`
272 Background bool `json:"background"`
273 }{
274 Command: "echo 'Output to stdout' && echo 'Output to stderr' >&2",
275 Background: true,
276 }
277 inputJSON, err := json.Marshal(inputObj)
278 if err != nil {
279 t.Fatalf("Failed to marshal input: %v", err)
280 }
281
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000282 result, err := Bash.Run(context.Background(), inputJSON)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000283 if err != nil {
284 t.Fatalf("Unexpected error: %v", err)
285 }
286
287 // Parse the returned JSON
288 var bgResult BackgroundResult
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000289 resultStr := result[0].Text
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700290 if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000291 t.Fatalf("Failed to unmarshal background result: %v", err)
292 }
293
294 // Wait for the command output to be written to files
295 waitForFile(t, bgResult.StdoutFile)
296 waitForFile(t, bgResult.StderrFile)
297
298 // Check stdout content
299 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
300 if err != nil {
301 t.Fatalf("Failed to read stdout file: %v", err)
302 }
303 expectedStdout := "Output to stdout\n"
304 if string(stdoutContent) != expectedStdout {
305 t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
306 }
307
308 // Check stderr content
309 stderrContent, err := os.ReadFile(bgResult.StderrFile)
310 if err != nil {
311 t.Fatalf("Failed to read stderr file: %v", err)
312 }
313 expectedStderr := "Output to stderr\n"
314 if string(stderrContent) != expectedStderr {
315 t.Errorf("Expected stderr content %q, got %q", expectedStderr, string(stderrContent))
316 }
317
318 // Clean up
319 os.Remove(bgResult.StdoutFile)
320 os.Remove(bgResult.StderrFile)
321 os.Remove(filepath.Dir(bgResult.StdoutFile))
322 })
323
324 // Test background command running without waiting
325 t.Run("Background Command Running", func(t *testing.T) {
326 // Create a script that will continue running after we check
327 inputObj := struct {
328 Command string `json:"command"`
329 Background bool `json:"background"`
330 }{
331 Command: "echo 'Running in background' && sleep 5",
332 Background: true,
333 }
334 inputJSON, err := json.Marshal(inputObj)
335 if err != nil {
336 t.Fatalf("Failed to marshal input: %v", err)
337 }
338
339 // Start the command in the background
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000340 result, err := Bash.Run(context.Background(), inputJSON)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000341 if err != nil {
342 t.Fatalf("Unexpected error: %v", err)
343 }
344
345 // Parse the returned JSON
346 var bgResult BackgroundResult
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000347 resultStr := result[0].Text
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700348 if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000349 t.Fatalf("Failed to unmarshal background result: %v", err)
350 }
351
352 // Wait for the command output to be written to file
353 waitForFile(t, bgResult.StdoutFile)
354
355 // Check stdout content
356 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
357 if err != nil {
358 t.Fatalf("Failed to read stdout file: %v", err)
359 }
360 expectedStdout := "Running in background\n"
361 if string(stdoutContent) != expectedStdout {
362 t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
363 }
364
365 // Verify the process is still running
366 process, _ := os.FindProcess(bgResult.PID)
367 err = process.Signal(syscall.Signal(0))
368 if err != nil {
369 // Process not running, which is unexpected
370 t.Error("Process is not running")
371 } else {
372 // Expected: process should be running
373 t.Log("Process correctly running in background")
374 // Kill it for cleanup
375 process.Kill()
376 }
377
378 // Clean up
379 os.Remove(bgResult.StdoutFile)
380 os.Remove(bgResult.StderrFile)
381 os.Remove(filepath.Dir(bgResult.StdoutFile))
382 })
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000383}
384
385func TestBashTimeout(t *testing.T) {
386 // Test default timeout values
387 t.Run("Default Timeout Values", func(t *testing.T) {
388 // Test foreground default timeout
389 foreground := bashInput{
390 Command: "echo 'test'",
391 Background: false,
392 }
393 fgTimeout := foreground.timeout()
Philip Zeyliger208938f2025-05-13 17:17:18 -0700394 expectedFg := 10 * time.Second
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000395 if fgTimeout != expectedFg {
396 t.Errorf("Expected foreground default timeout to be %v, got %v", expectedFg, fgTimeout)
397 }
398
399 // Test background default timeout
400 background := bashInput{
401 Command: "echo 'test'",
402 Background: true,
403 }
404 bgTimeout := background.timeout()
405 expectedBg := 10 * time.Minute
406 if bgTimeout != expectedBg {
407 t.Errorf("Expected background default timeout to be %v, got %v", expectedBg, bgTimeout)
408 }
409
410 // Test explicit timeout overrides defaults
411 explicit := bashInput{
412 Command: "echo 'test'",
413 Background: true,
414 Timeout: "5s",
415 }
416 explicitTimeout := explicit.timeout()
417 expectedExplicit := 5 * time.Second
418 if explicitTimeout != expectedExplicit {
419 t.Errorf("Expected explicit timeout to be %v, got %v", expectedExplicit, explicitTimeout)
420 }
421 })
422}
423
424// waitForFile waits for a file to exist and be non-empty or times out
425func waitForFile(t *testing.T, filepath string) {
426 timeout := time.After(5 * time.Second)
427 tick := time.NewTicker(10 * time.Millisecond)
428 defer tick.Stop()
429
430 for {
431 select {
432 case <-timeout:
433 t.Fatalf("Timed out waiting for file to exist and have contents: %s", filepath)
434 return
435 case <-tick.C:
436 info, err := os.Stat(filepath)
437 if err == nil && info.Size() > 0 {
438 return // File exists and has content
439 }
440 }
441 }
442}
443
444// waitForProcessDeath waits for a process to no longer exist or times out
445func waitForProcessDeath(t *testing.T, pid int) {
446 timeout := time.After(5 * time.Second)
447 tick := time.NewTicker(50 * time.Millisecond)
448 defer tick.Stop()
449
450 for {
451 select {
452 case <-timeout:
453 t.Fatalf("Timed out waiting for process %d to exit", pid)
454 return
455 case <-tick.C:
456 process, _ := os.FindProcess(pid)
457 err := process.Signal(syscall.Signal(0))
458 if err != nil {
459 // Process doesn't exist
460 return
461 }
462 }
463 }
464}