blob: 98410d5183a324e0477e57b4e9f6e21c6ff98a79 [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
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000014func TestBashSlowOk(t *testing.T) {
15 // Test that slow_ok flag is properly handled
16 t.Run("SlowOk Flag", func(t *testing.T) {
17 input := json.RawMessage(`{"command":"echo 'slow test'","slow_ok":true}`)
18
19 bashTool := (&BashTool{}).Tool()
20 result, err := bashTool.Run(context.Background(), input)
21 if err != nil {
22 t.Fatalf("Unexpected error: %v", err)
23 }
24
25 expected := "slow test\n"
26 if len(result) == 0 || result[0].Text != expected {
27 t.Errorf("Expected %q, got %q", expected, result[0].Text)
28 }
29 })
30
31 // Test that slow_ok with background works
32 t.Run("SlowOk with Background", func(t *testing.T) {
33 input := json.RawMessage(`{"command":"echo 'slow background test'","slow_ok":true,"background":true}`)
34
35 bashTool := (&BashTool{}).Tool()
36 result, err := bashTool.Run(context.Background(), input)
37 if err != nil {
38 t.Fatalf("Unexpected error: %v", err)
39 }
40
41 // Should return background result JSON
42 var bgResult BackgroundResult
43 resultStr := result[0].Text
44 if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
45 t.Fatalf("Failed to unmarshal background result: %v", err)
46 }
47
48 if bgResult.PID <= 0 {
49 t.Errorf("Invalid PID returned: %d", bgResult.PID)
50 }
51
52 // Clean up
53 os.Remove(bgResult.StdoutFile)
54 os.Remove(bgResult.StderrFile)
55 os.Remove(filepath.Dir(bgResult.StdoutFile))
56 })
57}
58
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000059func TestBashTool(t *testing.T) {
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000060 var bashTool BashTool
61 tool := bashTool.Tool()
62
Earl Lee2e463fb2025-04-17 11:22:22 -070063 // Test basic functionality
64 t.Run("Basic Command", func(t *testing.T) {
65 input := json.RawMessage(`{"command":"echo 'Hello, world!'"}`)
66
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000067 result, err := tool.Run(context.Background(), input)
Earl Lee2e463fb2025-04-17 11:22:22 -070068 if err != nil {
69 t.Fatalf("Unexpected error: %v", err)
70 }
71
72 expected := "Hello, world!\n"
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000073 if len(result) == 0 || result[0].Text != expected {
74 t.Errorf("Expected %q, got %q", expected, result[0].Text)
Earl Lee2e463fb2025-04-17 11:22:22 -070075 }
76 })
77
78 // Test with arguments
79 t.Run("Command With Arguments", func(t *testing.T) {
80 input := json.RawMessage(`{"command":"echo -n foo && echo -n bar"}`)
81
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000082 result, err := tool.Run(context.Background(), input)
Earl Lee2e463fb2025-04-17 11:22:22 -070083 if err != nil {
84 t.Fatalf("Unexpected error: %v", err)
85 }
86
87 expected := "foobar"
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000088 if len(result) == 0 || result[0].Text != expected {
89 t.Errorf("Expected %q, got %q", expected, result[0].Text)
Earl Lee2e463fb2025-04-17 11:22:22 -070090 }
91 })
92
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000093 // Test with slow_ok parameter
94 t.Run("With SlowOK", func(t *testing.T) {
Earl Lee2e463fb2025-04-17 11:22:22 -070095 inputObj := struct {
96 Command string `json:"command"`
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000097 SlowOK bool `json:"slow_ok"`
Earl Lee2e463fb2025-04-17 11:22:22 -070098 }{
99 Command: "sleep 0.1 && echo 'Completed'",
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000100 SlowOK: true,
Earl Lee2e463fb2025-04-17 11:22:22 -0700101 }
102 inputJSON, err := json.Marshal(inputObj)
103 if err != nil {
104 t.Fatalf("Failed to marshal input: %v", err)
105 }
106
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000107 result, err := tool.Run(context.Background(), inputJSON)
Earl Lee2e463fb2025-04-17 11:22:22 -0700108 if err != nil {
109 t.Fatalf("Unexpected error: %v", err)
110 }
111
112 expected := "Completed\n"
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000113 if len(result) == 0 || result[0].Text != expected {
114 t.Errorf("Expected %q, got %q", expected, result[0].Text)
Earl Lee2e463fb2025-04-17 11:22:22 -0700115 }
116 })
117
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000118 // Test command timeout with custom timeout config
Earl Lee2e463fb2025-04-17 11:22:22 -0700119 t.Run("Command Timeout", func(t *testing.T) {
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000120 // Use a custom BashTool with very short timeout
121 customTimeouts := &Timeouts{
122 Fast: 100 * time.Millisecond,
123 Slow: 100 * time.Millisecond,
124 Background: 100 * time.Millisecond,
Earl Lee2e463fb2025-04-17 11:22:22 -0700125 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000126 customBash := &BashTool{
127 Timeouts: customTimeouts,
Earl Lee2e463fb2025-04-17 11:22:22 -0700128 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000129 tool := customBash.Tool()
Earl Lee2e463fb2025-04-17 11:22:22 -0700130
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000131 input := json.RawMessage(`{"command":"sleep 0.5 && echo 'Should not see this'"}`)
132
133 _, err := tool.Run(context.Background(), input)
Earl Lee2e463fb2025-04-17 11:22:22 -0700134 if err == nil {
135 t.Errorf("Expected timeout error, got none")
136 } else if !strings.Contains(err.Error(), "timed out") {
137 t.Errorf("Expected timeout error, got: %v", err)
138 }
139 })
140
141 // Test command that fails
142 t.Run("Failed Command", func(t *testing.T) {
143 input := json.RawMessage(`{"command":"exit 1"}`)
144
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000145 _, err := tool.Run(context.Background(), input)
Earl Lee2e463fb2025-04-17 11:22:22 -0700146 if err == nil {
147 t.Errorf("Expected error for failed command, got none")
148 }
149 })
150
151 // Test invalid input
152 t.Run("Invalid JSON Input", func(t *testing.T) {
153 input := json.RawMessage(`{"command":123}`) // Invalid JSON (command must be string)
154
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000155 _, err := tool.Run(context.Background(), input)
Earl Lee2e463fb2025-04-17 11:22:22 -0700156 if err == nil {
157 t.Errorf("Expected error for invalid input, got none")
158 }
159 })
160}
161
162func TestExecuteBash(t *testing.T) {
163 ctx := context.Background()
164
165 // Test successful command
166 t.Run("Successful Command", func(t *testing.T) {
167 req := bashInput{
168 Command: "echo 'Success'",
Earl Lee2e463fb2025-04-17 11:22:22 -0700169 }
170
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000171 output, err := executeBash(ctx, req, 5*time.Second)
Earl Lee2e463fb2025-04-17 11:22:22 -0700172 if err != nil {
173 t.Fatalf("Unexpected error: %v", err)
174 }
175
176 want := "Success\n"
177 if output != want {
178 t.Errorf("Expected %q, got %q", want, output)
179 }
180 })
181
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100182 // Test SKETCH=1 environment variable is set
183 t.Run("SKETCH Environment Variable", func(t *testing.T) {
184 req := bashInput{
185 Command: "echo $SKETCH",
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100186 }
187
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000188 output, err := executeBash(ctx, req, 5*time.Second)
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100189 if err != nil {
190 t.Fatalf("Unexpected error: %v", err)
191 }
192
193 want := "1\n"
194 if output != want {
195 t.Errorf("Expected SKETCH=1, got %q", output)
196 }
197 })
198
Earl Lee2e463fb2025-04-17 11:22:22 -0700199 // Test command with output to stderr
200 t.Run("Command with stderr", func(t *testing.T) {
201 req := bashInput{
202 Command: "echo 'Error message' >&2 && echo 'Success'",
Earl Lee2e463fb2025-04-17 11:22:22 -0700203 }
204
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000205 output, err := executeBash(ctx, req, 5*time.Second)
Earl Lee2e463fb2025-04-17 11:22:22 -0700206 if err != nil {
207 t.Fatalf("Unexpected error: %v", err)
208 }
209
210 want := "Error message\nSuccess\n"
211 if output != want {
212 t.Errorf("Expected %q, got %q", want, output)
213 }
214 })
215
216 // Test command that fails with stderr
217 t.Run("Failed Command with stderr", func(t *testing.T) {
218 req := bashInput{
219 Command: "echo 'Error message' >&2 && exit 1",
Earl Lee2e463fb2025-04-17 11:22:22 -0700220 }
221
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000222 _, err := executeBash(ctx, req, 5*time.Second)
Earl Lee2e463fb2025-04-17 11:22:22 -0700223 if err == nil {
224 t.Errorf("Expected error for failed command, got none")
225 } else if !strings.Contains(err.Error(), "Error message") {
226 t.Errorf("Expected stderr in error message, got: %v", err)
227 }
228 })
229
230 // Test timeout
231 t.Run("Command Timeout", func(t *testing.T) {
232 req := bashInput{
233 Command: "sleep 1 && echo 'Should not see this'",
Earl Lee2e463fb2025-04-17 11:22:22 -0700234 }
235
236 start := time.Now()
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000237 _, err := executeBash(ctx, req, 100*time.Millisecond)
Earl Lee2e463fb2025-04-17 11:22:22 -0700238 elapsed := time.Since(start)
239
240 // Command should time out after ~100ms, not wait for full 1 second
241 if elapsed >= 1*time.Second {
242 t.Errorf("Command did not respect timeout, took %v", elapsed)
243 }
244
245 if err == nil {
246 t.Errorf("Expected timeout error, got none")
247 } else if !strings.Contains(err.Error(), "timed out") {
248 t.Errorf("Expected timeout error, got: %v", err)
249 }
250 })
251}
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000252
253func TestBackgroundBash(t *testing.T) {
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000254 var bashTool BashTool
255 tool := bashTool.Tool()
256
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000257 // Test basic background execution
258 t.Run("Basic Background Command", func(t *testing.T) {
259 inputObj := struct {
260 Command string `json:"command"`
261 Background bool `json:"background"`
262 }{
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100263 Command: "echo 'Hello from background' $SKETCH",
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000264 Background: true,
265 }
266 inputJSON, err := json.Marshal(inputObj)
267 if err != nil {
268 t.Fatalf("Failed to marshal input: %v", err)
269 }
270
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000271 result, err := tool.Run(context.Background(), inputJSON)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000272 if err != nil {
273 t.Fatalf("Unexpected error: %v", err)
274 }
275
276 // Parse the returned JSON
277 var bgResult BackgroundResult
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000278 resultStr := result[0].Text
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700279 if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000280 t.Fatalf("Failed to unmarshal background result: %v", err)
281 }
282
283 // Verify we got a valid PID
284 if bgResult.PID <= 0 {
285 t.Errorf("Invalid PID returned: %d", bgResult.PID)
286 }
287
288 // Verify output files exist
289 if _, err := os.Stat(bgResult.StdoutFile); os.IsNotExist(err) {
290 t.Errorf("Stdout file doesn't exist: %s", bgResult.StdoutFile)
291 }
292 if _, err := os.Stat(bgResult.StderrFile); os.IsNotExist(err) {
293 t.Errorf("Stderr file doesn't exist: %s", bgResult.StderrFile)
294 }
295
296 // Wait for the command output to be written to file
297 waitForFile(t, bgResult.StdoutFile)
298
299 // Check file contents
300 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
301 if err != nil {
302 t.Fatalf("Failed to read stdout file: %v", err)
303 }
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100304 expected := "Hello from background 1\n"
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000305 if string(stdoutContent) != expected {
306 t.Errorf("Expected stdout content %q, got %q", expected, string(stdoutContent))
307 }
308
309 // Clean up
310 os.Remove(bgResult.StdoutFile)
311 os.Remove(bgResult.StderrFile)
312 os.Remove(filepath.Dir(bgResult.StdoutFile))
313 })
314
315 // Test background command with stderr output
316 t.Run("Background Command with stderr", func(t *testing.T) {
317 inputObj := struct {
318 Command string `json:"command"`
319 Background bool `json:"background"`
320 }{
321 Command: "echo 'Output to stdout' && echo 'Output to stderr' >&2",
322 Background: true,
323 }
324 inputJSON, err := json.Marshal(inputObj)
325 if err != nil {
326 t.Fatalf("Failed to marshal input: %v", err)
327 }
328
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000329 result, err := tool.Run(context.Background(), inputJSON)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000330 if err != nil {
331 t.Fatalf("Unexpected error: %v", err)
332 }
333
334 // Parse the returned JSON
335 var bgResult BackgroundResult
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000336 resultStr := result[0].Text
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700337 if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000338 t.Fatalf("Failed to unmarshal background result: %v", err)
339 }
340
341 // Wait for the command output to be written to files
342 waitForFile(t, bgResult.StdoutFile)
343 waitForFile(t, bgResult.StderrFile)
344
345 // Check stdout content
346 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
347 if err != nil {
348 t.Fatalf("Failed to read stdout file: %v", err)
349 }
350 expectedStdout := "Output to stdout\n"
351 if string(stdoutContent) != expectedStdout {
352 t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
353 }
354
355 // Check stderr content
356 stderrContent, err := os.ReadFile(bgResult.StderrFile)
357 if err != nil {
358 t.Fatalf("Failed to read stderr file: %v", err)
359 }
360 expectedStderr := "Output to stderr\n"
361 if string(stderrContent) != expectedStderr {
362 t.Errorf("Expected stderr content %q, got %q", expectedStderr, string(stderrContent))
363 }
364
365 // Clean up
366 os.Remove(bgResult.StdoutFile)
367 os.Remove(bgResult.StderrFile)
368 os.Remove(filepath.Dir(bgResult.StdoutFile))
369 })
370
371 // Test background command running without waiting
372 t.Run("Background Command Running", func(t *testing.T) {
373 // Create a script that will continue running after we check
374 inputObj := struct {
375 Command string `json:"command"`
376 Background bool `json:"background"`
377 }{
378 Command: "echo 'Running in background' && sleep 5",
379 Background: true,
380 }
381 inputJSON, err := json.Marshal(inputObj)
382 if err != nil {
383 t.Fatalf("Failed to marshal input: %v", err)
384 }
385
386 // Start the command in the background
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000387 result, err := tool.Run(context.Background(), inputJSON)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000388 if err != nil {
389 t.Fatalf("Unexpected error: %v", err)
390 }
391
392 // Parse the returned JSON
393 var bgResult BackgroundResult
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000394 resultStr := result[0].Text
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700395 if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000396 t.Fatalf("Failed to unmarshal background result: %v", err)
397 }
398
399 // Wait for the command output to be written to file
400 waitForFile(t, bgResult.StdoutFile)
401
402 // Check stdout content
403 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
404 if err != nil {
405 t.Fatalf("Failed to read stdout file: %v", err)
406 }
407 expectedStdout := "Running in background\n"
408 if string(stdoutContent) != expectedStdout {
409 t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
410 }
411
412 // Verify the process is still running
413 process, _ := os.FindProcess(bgResult.PID)
414 err = process.Signal(syscall.Signal(0))
415 if err != nil {
416 // Process not running, which is unexpected
417 t.Error("Process is not running")
418 } else {
419 // Expected: process should be running
420 t.Log("Process correctly running in background")
421 // Kill it for cleanup
422 process.Kill()
423 }
424
425 // Clean up
426 os.Remove(bgResult.StdoutFile)
427 os.Remove(bgResult.StderrFile)
428 os.Remove(filepath.Dir(bgResult.StdoutFile))
429 })
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000430}
431
432func TestBashTimeout(t *testing.T) {
433 // Test default timeout values
434 t.Run("Default Timeout Values", func(t *testing.T) {
435 // Test foreground default timeout
436 foreground := bashInput{
437 Command: "echo 'test'",
438 Background: false,
439 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000440 fgTimeout := foreground.timeout(nil)
441 expectedFg := 30 * time.Second
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000442 if fgTimeout != expectedFg {
443 t.Errorf("Expected foreground default timeout to be %v, got %v", expectedFg, fgTimeout)
444 }
445
446 // Test background default timeout
447 background := bashInput{
448 Command: "echo 'test'",
449 Background: true,
450 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000451 bgTimeout := background.timeout(nil)
452 expectedBg := 24 * time.Hour
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000453 if bgTimeout != expectedBg {
454 t.Errorf("Expected background default timeout to be %v, got %v", expectedBg, bgTimeout)
455 }
456
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000457 // Test slow_ok timeout
458 slowOk := bashInput{
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000459 Command: "echo 'test'",
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000460 Background: false,
461 SlowOK: true,
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000462 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000463 slowTimeout := slowOk.timeout(nil)
464 expectedSlow := 15 * time.Minute
465 if slowTimeout != expectedSlow {
466 t.Errorf("Expected slow_ok timeout to be %v, got %v", expectedSlow, slowTimeout)
467 }
468
469 // Test custom timeout config
470 customTimeouts := &Timeouts{
471 Fast: 5 * time.Second,
472 Slow: 2 * time.Minute,
473 Background: 1 * time.Hour,
474 }
475 customFast := bashInput{
476 Command: "echo 'test'",
477 Background: false,
478 }
479 customTimeout := customFast.timeout(customTimeouts)
480 expectedCustom := 5 * time.Second
481 if customTimeout != expectedCustom {
482 t.Errorf("Expected custom timeout to be %v, got %v", expectedCustom, customTimeout)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000483 }
484 })
485}
486
487// waitForFile waits for a file to exist and be non-empty or times out
488func waitForFile(t *testing.T, filepath string) {
489 timeout := time.After(5 * time.Second)
490 tick := time.NewTicker(10 * time.Millisecond)
491 defer tick.Stop()
492
493 for {
494 select {
495 case <-timeout:
496 t.Fatalf("Timed out waiting for file to exist and have contents: %s", filepath)
497 return
498 case <-tick.C:
499 info, err := os.Stat(filepath)
500 if err == nil && info.Size() > 0 {
501 return // File exists and has content
502 }
503 }
504 }
505}
506
507// waitForProcessDeath waits for a process to no longer exist or times out
508func waitForProcessDeath(t *testing.T, pid int) {
509 timeout := time.After(5 * time.Second)
510 tick := time.NewTicker(50 * time.Millisecond)
511 defer tick.Stop()
512
513 for {
514 select {
515 case <-timeout:
516 t.Fatalf("Timed out waiting for process %d to exit", pid)
517 return
518 case <-tick.C:
519 process, _ := os.FindProcess(pid)
520 err := process.Signal(syscall.Signal(0))
521 if err != nil {
522 // Process doesn't exist
523 return
524 }
525 }
526 }
527}