blob: ab687fa5554478c79c4d0ca5325b4a5cd1a7c079 [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001package claudetool
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12// setupTestFile creates a temporary file with given content for testing
13func setupTestFile(t *testing.T, content string) string {
14 t.Helper()
15
16 // Create a temporary directory
17 tempDir, err := os.MkdirTemp("", "anthropic_edit_test_*")
18 if err != nil {
19 t.Fatalf("Failed to create temp directory: %v", err)
20 }
21
22 // Create a test file in the temp directory
23 testFile := filepath.Join(tempDir, "test_file.txt")
24 if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil {
25 os.RemoveAll(tempDir)
26 t.Fatalf("Failed to write test file: %v", err)
27 }
28
29 // Register cleanup function
30 t.Cleanup(func() {
31 os.RemoveAll(tempDir)
32 })
33
34 return testFile
35}
36
37// callEditTool is a helper to call the edit tool with specific parameters
38func callEditTool(t *testing.T, input map[string]any) string {
39 t.Helper()
40
41 // Convert input to JSON
42 inputJSON, err := json.Marshal(input)
43 if err != nil {
44 t.Fatalf("Failed to marshal input: %v", err)
45 }
46
47 // Call the tool
48 result, err := EditRun(context.Background(), inputJSON)
49 if err != nil {
50 t.Fatalf("Tool execution failed: %v", err)
51 }
52
Philip Zeyliger72252cb2025-05-10 17:00:08 -070053 return ContentToString(result)
Earl Lee2e463fb2025-04-17 11:22:22 -070054}
55
56// TestEditToolView tests the view command functionality
57func TestEditToolView(t *testing.T) {
58 content := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
59 testFile := setupTestFile(t, content)
60
61 // Test the view command
62 result := callEditTool(t, map[string]any{
63 "command": "view",
64 "path": testFile,
65 })
66
67 // Verify results
68 if !strings.Contains(result, "Line 1") {
69 t.Errorf("View result should contain the file content, got: %s", result)
70 }
71
72 // Test view with range
73 result = callEditTool(t, map[string]any{
74 "command": "view",
75 "path": testFile,
76 "view_range": []int{2, 4},
77 })
78
79 // Verify range results
80 if strings.Contains(result, "Line 1") || !strings.Contains(result, "Line 2") {
81 t.Errorf("View with range should show only specified lines, got: %s", result)
82 }
83}
84
85// TestEditToolStrReplace tests the str_replace command functionality
86func TestEditToolStrReplace(t *testing.T) {
87 content := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
88 testFile := setupTestFile(t, content)
89
90 // Test the str_replace command
91 result := callEditTool(t, map[string]any{
92 "command": "str_replace",
93 "path": testFile,
94 "old_str": "Line 3",
95 "new_str": "Modified Line 3",
96 })
97
98 // Verify the file was modified
99 modifiedContent, err := os.ReadFile(testFile)
100 if err != nil {
101 t.Fatalf("Failed to read test file: %v", err)
102 }
103
104 if !strings.Contains(string(modifiedContent), "Modified Line 3") {
105 t.Errorf("File content should be modified, got: %s", string(modifiedContent))
106 }
107
108 // Verify the result contains a snippet
109 if !strings.Contains(result, "Modified Line 3") {
110 t.Errorf("Result should contain the modified content, got: %s", result)
111 }
112}
113
114// TestEditToolInsert tests the insert command functionality
115func TestEditToolInsert(t *testing.T) {
116 content := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
117 testFile := setupTestFile(t, content)
118
119 // Test the insert command
120 result := callEditTool(t, map[string]any{
121 "command": "insert",
122 "path": testFile,
123 "insert_line": 2,
124 "new_str": "Inserted Line",
125 })
126
127 // Verify the file was modified
128 modifiedContent, err := os.ReadFile(testFile)
129 if err != nil {
130 t.Fatalf("Failed to read test file: %v", err)
131 }
132
133 expected := "Line 1\nLine 2\nInserted Line\nLine 3\nLine 4\nLine 5"
134 if string(modifiedContent) != expected {
135 t.Errorf("File content incorrect after insert. Expected:\n%s\nGot:\n%s", expected, string(modifiedContent))
136 }
137
138 // Verify the result contains a snippet
139 if !strings.Contains(result, "Inserted Line") {
140 t.Errorf("Result should contain the inserted content, got: %s", result)
141 }
142}
143
144// TestEditToolCreate tests the create command functionality
145func TestEditToolCreate(t *testing.T) {
146 tempDir, err := os.MkdirTemp("", "anthropic_edit_test_create_*")
147 if err != nil {
148 t.Fatalf("Failed to create temp directory: %v", err)
149 }
150
151 t.Cleanup(func() {
152 os.RemoveAll(tempDir)
153 })
154
155 newFilePath := filepath.Join(tempDir, "new_file.txt")
156 content := "This is a new file\nWith multiple lines"
157
158 // Test the create command
159 result := callEditTool(t, map[string]any{
160 "command": "create",
161 "path": newFilePath,
162 "file_text": content,
163 })
164
165 // Verify the file was created with the right content
166 createdContent, err := os.ReadFile(newFilePath)
167 if err != nil {
168 t.Fatalf("Failed to read created file: %v", err)
169 }
170
171 if string(createdContent) != content {
172 t.Errorf("Created file content incorrect. Expected:\n%s\nGot:\n%s", content, string(createdContent))
173 }
174
175 // Verify the result message
176 if !strings.Contains(result, "File created successfully") {
177 t.Errorf("Result should confirm file creation, got: %s", result)
178 }
179}
180
181// TestEditToolUndoEdit tests the undo_edit command functionality
182func TestEditToolUndoEdit(t *testing.T) {
183 originalContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
184 testFile := setupTestFile(t, originalContent)
185
186 // First modify the file
187 callEditTool(t, map[string]any{
188 "command": "str_replace",
189 "path": testFile,
190 "old_str": "Line 3",
191 "new_str": "Modified Line 3",
192 })
193
194 // Then undo the edit
195 result := callEditTool(t, map[string]any{
196 "command": "undo_edit",
197 "path": testFile,
198 })
199
200 // Verify the file was restored to original content
201 restoredContent, err := os.ReadFile(testFile)
202 if err != nil {
203 t.Fatalf("Failed to read test file: %v", err)
204 }
205
206 if string(restoredContent) != originalContent {
207 t.Errorf("File content should be restored to original, got: %s", string(restoredContent))
208 }
209
210 // Verify the result message
211 if !strings.Contains(result, "undone successfully") {
212 t.Errorf("Result should confirm undo operation, got: %s", result)
213 }
214}
215
216// TestEditToolErrors tests various error conditions
217func TestEditToolErrors(t *testing.T) {
218 content := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
219 testFile := setupTestFile(t, content)
220
221 testCases := []struct {
222 name string
223 input map[string]any
224 errMsg string
225 }{
226 {
227 name: "Invalid command",
228 input: map[string]any{
229 "command": "invalid_command",
230 "path": testFile,
231 },
232 errMsg: "unrecognized command",
233 },
234 {
235 name: "Non-existent file",
236 input: map[string]any{
237 "command": "view",
238 "path": "/non/existent/file.txt",
239 },
240 errMsg: "does not exist",
241 },
242 {
243 name: "Missing required parameter",
244 input: map[string]any{
245 "command": "str_replace",
246 "path": testFile,
247 // Missing old_str
248 },
249 errMsg: "parameter old_str is required",
250 },
251 {
252 name: "Multiple occurrences in str_replace",
253 input: map[string]any{
254 "command": "str_replace",
255 "path": testFile,
256 "old_str": "Line", // Appears multiple times
257 "new_str": "Modified Line",
258 },
259 errMsg: "Multiple occurrences",
260 },
261 {
262 name: "Invalid view range",
263 input: map[string]any{
264 "command": "view",
265 "path": testFile,
266 "view_range": []int{10, 20}, // Out of range
267 },
268 errMsg: "invalid view_range",
269 },
270 }
271
272 for _, tc := range testCases {
273 t.Run(tc.name, func(t *testing.T) {
274 inputJSON, err := json.Marshal(tc.input)
275 if err != nil {
276 t.Fatalf("Failed to marshal input: %v", err)
277 }
278
279 _, err = EditRun(context.Background(), inputJSON)
280 if err == nil {
281 t.Fatalf("Expected error but got none")
282 }
283
284 if !strings.Contains(err.Error(), tc.errMsg) {
285 t.Errorf("Error message does not contain expected text. Expected to contain: %q, Got: %q", tc.errMsg, err.Error())
286 }
287 })
288 }
289}
290
291// TestHandleStrReplaceEdgeCases tests the handleStrReplace function specifically for edge cases
292// that could cause panics like "index out of range [0] with length 0"
293func TestHandleStrReplaceEdgeCases(t *testing.T) {
294 // The issue was with strings.Split returning an empty slice when the separator wasn't found
295 // This test directly tests the internal implementation with conditions that might cause this
296
297 // Create a test file with empty content
298 emptyFile := setupTestFile(t, "")
299
300 // Test with empty file content and arbitrary oldStr
301 _, err := handleStrReplace(emptyFile, "some string that doesn't exist", "new content")
302 if err == nil {
303 t.Fatal("Expected error for empty file but got none")
304 }
305 if !strings.Contains(err.Error(), "did not appear verbatim") {
306 t.Errorf("Expected error message to indicate missing string, got: %s", err.Error())
307 }
308
309 // Create a file with content that doesn't match oldStr
310 nonMatchingFile := setupTestFile(t, "This is some content\nthat doesn't contain the target string")
311
312 // Test with content that doesn't contain oldStr
313 _, err = handleStrReplace(nonMatchingFile, "target string not present", "replacement")
314 if err == nil {
315 t.Fatal("Expected error for non-matching content but got none")
316 }
317 if !strings.Contains(err.Error(), "did not appear verbatim") {
318 t.Errorf("Expected error message to indicate missing string, got: %s", err.Error())
319 }
320
321 // Test handling of the edge case that could potentially cause the "index out of range" panic
322 // This directly verifies that the handleStrReplace function properly handles the case where
323 // strings.Split returns an empty or unexpected result
324
325 // Verify that the protection against empty parts slice works
326 fileContent := ""
327 oldStr := "some string"
328 parts := strings.Split(fileContent, oldStr)
329 if len(parts) == 0 {
330 // This should match the protection in the code
331 parts = []string{""}
332 }
333
334 // This should not panic with the fix in place
335 _ = strings.Count(parts[0], "\n") // This line would have panicked without the fix
336}
337
338// TestViewRangeWithStrReplace tests that the view_range parameter works correctly
339// with the str_replace command (tests the full workflow)
340func TestViewRangeWithStrReplace(t *testing.T) {
341 // Create test file with multiple lines
342 content := "Line 1: First line\nLine 2: Second line\nLine 3: Third line\nLine 4: Fourth line\nLine 5: Fifth line"
343 testFile := setupTestFile(t, content)
344
345 // First view a subset of the file using view_range
346 viewResult := callEditTool(t, map[string]any{
347 "command": "view",
348 "path": testFile,
349 "view_range": []int{2, 4}, // Only lines 2-4
350 })
351
352 // Verify that we only see the specified lines
353 if strings.Contains(viewResult, "Line 1:") || strings.Contains(viewResult, "Line 5:") {
354 t.Errorf("View with range should only show lines 2-4, got: %s", viewResult)
355 }
356 if !strings.Contains(viewResult, "Line 2:") || !strings.Contains(viewResult, "Line 4:") {
357 t.Errorf("View with range should show lines 2-4, got: %s", viewResult)
358 }
359
360 // Now perform a str_replace on one of the lines we viewed
361 replaceResult := callEditTool(t, map[string]any{
362 "command": "str_replace",
363 "path": testFile,
364 "old_str": "Line 3: Third line",
365 "new_str": "Line 3: MODIFIED Third line",
366 })
367
368 // Check that the replacement was successful
369 if !strings.Contains(replaceResult, "Line 3: MODIFIED Third line") {
370 t.Errorf("Replace result should contain the modified line, got: %s", replaceResult)
371 }
372
373 // Verify the file content was updated correctly
374 modifiedContent, err := os.ReadFile(testFile)
375 if err != nil {
376 t.Fatalf("Failed to read test file after modification: %v", err)
377 }
378
379 expectedContent := "Line 1: First line\nLine 2: Second line\nLine 3: MODIFIED Third line\nLine 4: Fourth line\nLine 5: Fifth line"
380 if string(modifiedContent) != expectedContent {
381 t.Errorf("File content after replacement is incorrect.\nExpected:\n%s\nGot:\n%s",
382 expectedContent, string(modifiedContent))
383 }
384
385 // View the modified file with a different view_range
386 finalViewResult := callEditTool(t, map[string]any{
387 "command": "view",
388 "path": testFile,
389 "view_range": []int{3, 3}, // Only the modified line
390 })
391
392 // Verify we can see only the modified line
393 if !strings.Contains(finalViewResult, "Line 3: MODIFIED Third line") {
394 t.Errorf("Final view should show the modified line, got: %s", finalViewResult)
395 }
396 if strings.Contains(finalViewResult, "Line 2:") || strings.Contains(finalViewResult, "Line 4:") {
397 t.Errorf("Final view should only show line 3, got: %s", finalViewResult)
398 }
399}