| Earl Lee | 2e463fb | 2025-04-17 11:22:22 -0700 | [diff] [blame] | 1 | package claudetool |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | // TestEmptyContentHandling tests handling of empty content in str_replace and related operations |
| 11 | // This test specifically reproduces conditions that might lead to "index out of range [0]" panic |
| 12 | func TestEmptyContentHandling(t *testing.T) { |
| 13 | // Create a file with empty content |
| 14 | emptyFile := setupTestFile(t, "") |
| 15 | |
| 16 | // Test running EditRun directly with empty content |
| 17 | // This more closely simulates the actual call flow that led to the panic |
| 18 | input := map[string]any{ |
| 19 | "command": "str_replace", |
| 20 | "path": emptyFile, |
| 21 | "old_str": "nonexistent text", |
| 22 | "new_str": "new content", |
| 23 | } |
| 24 | |
| 25 | inputJSON, err := json.Marshal(input) |
| 26 | if err != nil { |
| 27 | t.Fatalf("Failed to marshal input: %v", err) |
| 28 | } |
| 29 | |
| 30 | // This should not panic but return an error |
| 31 | _, err = EditRun(context.Background(), inputJSON) |
| 32 | if err == nil { |
| 33 | t.Fatalf("Expected error for empty file with str_replace but got none") |
| 34 | } |
| 35 | |
| 36 | // Make sure the error message is as expected |
| 37 | if !strings.Contains(err.Error(), "did not appear verbatim") { |
| 38 | t.Errorf("Expected error message to indicate missing string, got: %s", err.Error()) |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | // TestNilParameterHandling tests error cases with nil parameters |
| 43 | // This test validates proper error handling when nil or invalid parameters are provided |
| 44 | func TestNilParameterHandling(t *testing.T) { |
| 45 | // Create a test file |
| 46 | testFile := setupTestFile(t, "test content") |
| 47 | |
| 48 | // Test case 1: nil old_str in str_replace |
| 49 | input1 := map[string]any{ |
| 50 | "command": "str_replace", |
| 51 | "path": testFile, |
| 52 | // old_str is deliberately missing |
| 53 | "new_str": "replacement", |
| 54 | } |
| 55 | |
| 56 | inputJSON1, err := json.Marshal(input1) |
| 57 | if err != nil { |
| 58 | t.Fatalf("Failed to marshal input: %v", err) |
| 59 | } |
| 60 | |
| 61 | _, err = EditRun(context.Background(), inputJSON1) |
| 62 | if err == nil { |
| 63 | t.Fatalf("Expected error for missing old_str but got none") |
| 64 | } |
| 65 | if !strings.Contains(err.Error(), "parameter old_str is required") { |
| 66 | t.Errorf("Expected error message to indicate missing old_str, got: %s", err.Error()) |
| 67 | } |
| 68 | |
| 69 | // Test case 2: nil new_str in insert |
| 70 | input2 := map[string]any{ |
| 71 | "command": "insert", |
| 72 | "path": testFile, |
| 73 | "insert_line": 1, |
| 74 | // new_str is deliberately missing |
| 75 | } |
| 76 | |
| 77 | inputJSON2, err := json.Marshal(input2) |
| 78 | if err != nil { |
| 79 | t.Fatalf("Failed to marshal input: %v", err) |
| 80 | } |
| 81 | |
| 82 | _, err = EditRun(context.Background(), inputJSON2) |
| 83 | if err == nil { |
| 84 | t.Fatalf("Expected error for missing new_str but got none") |
| 85 | } |
| 86 | if !strings.Contains(err.Error(), "parameter new_str is required") { |
| 87 | t.Errorf("Expected error message to indicate missing new_str, got: %s", err.Error()) |
| 88 | } |
| 89 | |
| 90 | // Test case 3: nil view_range in view |
| 91 | // This doesn't cause an error, but tests the code path |
| 92 | input3 := map[string]any{ |
| 93 | "command": "view", |
| 94 | "path": testFile, |
| 95 | // No view_range |
| 96 | } |
| 97 | |
| 98 | inputJSON3, err := json.Marshal(input3) |
| 99 | if err != nil { |
| 100 | t.Fatalf("Failed to marshal input: %v", err) |
| 101 | } |
| 102 | |
| 103 | // This should not result in an error |
| 104 | _, err = EditRun(context.Background(), inputJSON3) |
| 105 | if err != nil { |
| 106 | t.Fatalf("Unexpected error for nil view_range: %v", err) |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | // TestEmptySplitResult tests the specific scenario where strings.Split might return empty results |
| 111 | // This directly reproduces conditions that might have led to the "index out of range [0]" panic |
| 112 | func TestEmptySplitResult(t *testing.T) { |
| 113 | // Direct test of strings.Split behavior and our handling of it |
| 114 | emptyCases := []struct { |
| 115 | content string |
| 116 | oldStr string |
| 117 | }{ |
| 118 | {"", "any string"}, |
| 119 | {"content", "not in string"}, |
| 120 | {"\n\n", "also not here"}, |
| 121 | } |
| 122 | |
| 123 | for _, tc := range emptyCases { |
| 124 | parts := strings.Split(tc.content, tc.oldStr) |
| 125 | |
| 126 | // Verify that strings.Split with non-matching separator returns a slice with original content |
| 127 | if len(parts) != 1 { |
| 128 | t.Errorf("Expected strings.Split to return a slice with 1 element when separator isn't found, got %d elements", len(parts)) |
| 129 | } |
| 130 | |
| 131 | // Double check the content |
| 132 | if len(parts) > 0 && parts[0] != tc.content { |
| 133 | t.Errorf("Expected parts[0] to be original content %q, got %q", tc.content, parts[0]) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | // Test the actual unsafe scenario with empty content |
| 138 | emptyFile := setupTestFile(t, "") |
| 139 | |
| 140 | // Get the content and simulate the internal string splitting |
| 141 | content, _ := readFile(emptyFile) |
| 142 | oldStr := "nonexistent" |
| 143 | parts := strings.Split(content, oldStr) |
| 144 | |
| 145 | // Validate that the defensive code would work |
| 146 | if len(parts) == 0 { |
| 147 | parts = []string{""} // This is the fix |
| 148 | } |
| 149 | |
| 150 | // This would have panicked without the fix |
| 151 | _ = strings.Count(parts[0], "\n") |
| 152 | } |