| Philip Zeyliger | 064f8ae | 2025-05-14 00:47:41 +0000 | [diff] [blame] | 1 | package test |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "testing" |
| 7 | |
| 8 | "sketch.dev/claudetool" |
| Philip Zeyliger | 064f8ae | 2025-05-14 00:47:41 +0000 | [diff] [blame] | 9 | ) |
| 10 | |
| 11 | func TestBashTimeout(t *testing.T) { |
| 12 | // Create a bash tool |
| Josh Bleecher Snyder | 495c1fa | 2025-05-29 00:37:22 +0000 | [diff] [blame] | 13 | bashTool := claudetool.NewBashTool(nil, claudetool.NoBashToolJITInstall) |
| Philip Zeyliger | 064f8ae | 2025-05-14 00:47:41 +0000 | [diff] [blame] | 14 | |
| 15 | // Create a command that will output text and then sleep |
| 16 | cmd := `echo "Starting command..."; echo "This should appear in partial output"; sleep 5; echo "This shouldn't appear"` |
| 17 | |
| 18 | // Prepare the input with a very short timeout |
| Philip Zeyliger | ec398cb | 2025-05-14 00:48:06 +0000 | [diff] [blame] | 19 | input := map[string]any{ |
| Philip Zeyliger | 064f8ae | 2025-05-14 00:47:41 +0000 | [diff] [blame] | 20 | "command": cmd, |
| 21 | "timeout": "1s", // Very short timeout to trigger the timeout case |
| 22 | } |
| 23 | |
| 24 | // Marshal the input to JSON |
| 25 | inputJSON, err := json.Marshal(input) |
| 26 | if err != nil { |
| 27 | t.Fatalf("Failed to marshal input: %v", err) |
| 28 | } |
| 29 | |
| 30 | // Run the bash tool |
| 31 | ctx := context.Background() |
| 32 | result, err := bashTool.Run(ctx, inputJSON) |
| 33 | |
| 34 | // Check that we got an error (due to timeout) |
| 35 | if err == nil { |
| 36 | t.Fatalf("Expected timeout error, got nil") |
| 37 | } |
| 38 | |
| 39 | // Error should mention timeout |
| 40 | if !containsString(err.Error(), "timed out") { |
| 41 | t.Errorf("Error doesn't mention timeout: %v", err) |
| 42 | } |
| 43 | |
| Philip Zeyliger | 8a1b89a | 2025-05-13 17:58:41 -0700 | [diff] [blame] | 44 | // No output should be returned directly, it should be in the error message |
| 45 | if len(result) > 0 { |
| 46 | t.Fatalf("Expected no direct output, got: %v", result) |
| Philip Zeyliger | 064f8ae | 2025-05-14 00:47:41 +0000 | [diff] [blame] | 47 | } |
| 48 | |
| Philip Zeyliger | 8a1b89a | 2025-05-13 17:58:41 -0700 | [diff] [blame] | 49 | // The error should contain the partial output |
| 50 | errorMsg := err.Error() |
| 51 | if !containsString(errorMsg, "Starting command") || !containsString(errorMsg, "should appear in partial output") { |
| 52 | t.Errorf("Error should contain the partial output: %v", errorMsg) |
| Philip Zeyliger | 064f8ae | 2025-05-14 00:47:41 +0000 | [diff] [blame] | 53 | } |
| 54 | |
| Philip Zeyliger | 8a1b89a | 2025-05-13 17:58:41 -0700 | [diff] [blame] | 55 | // The error should indicate a timeout |
| 56 | if !containsString(errorMsg, "timed out") { |
| 57 | t.Errorf("Error should indicate a timeout: %v", errorMsg) |
| Philip Zeyliger | 064f8ae | 2025-05-14 00:47:41 +0000 | [diff] [blame] | 58 | } |
| 59 | |
| Philip Zeyliger | 8a1b89a | 2025-05-13 17:58:41 -0700 | [diff] [blame] | 60 | // The error should not contain the output that would appear after the sleep |
| 61 | if containsString(err.Error(), "shouldn't appear") { |
| 62 | t.Errorf("Error contains output that should not have been captured (after timeout): %s", err.Error()) |
| Philip Zeyliger | 064f8ae | 2025-05-14 00:47:41 +0000 | [diff] [blame] | 63 | } |
| 64 | } |
| 65 | |
| 66 | func containsString(s, substr string) bool { |
| 67 | return s != "" && s != "<nil>" && stringIndexOf(s, substr) >= 0 |
| 68 | } |
| 69 | |
| 70 | func stringIndexOf(s, substr string) int { |
| 71 | for i := 0; i <= len(s)-len(substr); i++ { |
| 72 | if s[i:i+len(substr)] == substr { |
| 73 | return i |
| 74 | } |
| 75 | } |
| 76 | return -1 |
| 77 | } |