| package claudetool |
| |
| import ( |
| "context" |
| "encoding/json" |
| "os" |
| "path/filepath" |
| "strings" |
| "testing" |
| ) |
| |
| // setupTestFile creates a temporary file with given content for testing |
| func setupTestFile(t *testing.T, content string) string { |
| t.Helper() |
| |
| // Create a temporary directory |
| tempDir, err := os.MkdirTemp("", "anthropic_edit_test_*") |
| if err != nil { |
| t.Fatalf("Failed to create temp directory: %v", err) |
| } |
| |
| // Create a test file in the temp directory |
| testFile := filepath.Join(tempDir, "test_file.txt") |
| if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil { |
| os.RemoveAll(tempDir) |
| t.Fatalf("Failed to write test file: %v", err) |
| } |
| |
| // Register cleanup function |
| t.Cleanup(func() { |
| os.RemoveAll(tempDir) |
| }) |
| |
| return testFile |
| } |
| |
| // callEditTool is a helper to call the edit tool with specific parameters |
| func callEditTool(t *testing.T, input map[string]any) string { |
| t.Helper() |
| |
| // Convert input to JSON |
| inputJSON, err := json.Marshal(input) |
| if err != nil { |
| t.Fatalf("Failed to marshal input: %v", err) |
| } |
| |
| // Call the tool |
| result, err := EditRun(context.Background(), inputJSON) |
| if err != nil { |
| t.Fatalf("Tool execution failed: %v", err) |
| } |
| |
| return ContentToString(result) |
| } |
| |
| // TestEditToolView tests the view command functionality |
| func TestEditToolView(t *testing.T) { |
| content := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" |
| testFile := setupTestFile(t, content) |
| |
| // Test the view command |
| result := callEditTool(t, map[string]any{ |
| "command": "view", |
| "path": testFile, |
| }) |
| |
| // Verify results |
| if !strings.Contains(result, "Line 1") { |
| t.Errorf("View result should contain the file content, got: %s", result) |
| } |
| |
| // Test view with range |
| result = callEditTool(t, map[string]any{ |
| "command": "view", |
| "path": testFile, |
| "view_range": []int{2, 4}, |
| }) |
| |
| // Verify range results |
| if strings.Contains(result, "Line 1") || !strings.Contains(result, "Line 2") { |
| t.Errorf("View with range should show only specified lines, got: %s", result) |
| } |
| } |
| |
| // TestEditToolStrReplace tests the str_replace command functionality |
| func TestEditToolStrReplace(t *testing.T) { |
| content := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" |
| testFile := setupTestFile(t, content) |
| |
| // Test the str_replace command |
| result := callEditTool(t, map[string]any{ |
| "command": "str_replace", |
| "path": testFile, |
| "old_str": "Line 3", |
| "new_str": "Modified Line 3", |
| }) |
| |
| // Verify the file was modified |
| modifiedContent, err := os.ReadFile(testFile) |
| if err != nil { |
| t.Fatalf("Failed to read test file: %v", err) |
| } |
| |
| if !strings.Contains(string(modifiedContent), "Modified Line 3") { |
| t.Errorf("File content should be modified, got: %s", string(modifiedContent)) |
| } |
| |
| // Verify the result contains a snippet |
| if !strings.Contains(result, "Modified Line 3") { |
| t.Errorf("Result should contain the modified content, got: %s", result) |
| } |
| } |
| |
| // TestEditToolInsert tests the insert command functionality |
| func TestEditToolInsert(t *testing.T) { |
| content := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" |
| testFile := setupTestFile(t, content) |
| |
| // Test the insert command |
| result := callEditTool(t, map[string]any{ |
| "command": "insert", |
| "path": testFile, |
| "insert_line": 2, |
| "new_str": "Inserted Line", |
| }) |
| |
| // Verify the file was modified |
| modifiedContent, err := os.ReadFile(testFile) |
| if err != nil { |
| t.Fatalf("Failed to read test file: %v", err) |
| } |
| |
| expected := "Line 1\nLine 2\nInserted Line\nLine 3\nLine 4\nLine 5" |
| if string(modifiedContent) != expected { |
| t.Errorf("File content incorrect after insert. Expected:\n%s\nGot:\n%s", expected, string(modifiedContent)) |
| } |
| |
| // Verify the result contains a snippet |
| if !strings.Contains(result, "Inserted Line") { |
| t.Errorf("Result should contain the inserted content, got: %s", result) |
| } |
| } |
| |
| // TestEditToolCreate tests the create command functionality |
| func TestEditToolCreate(t *testing.T) { |
| tempDir, err := os.MkdirTemp("", "anthropic_edit_test_create_*") |
| if err != nil { |
| t.Fatalf("Failed to create temp directory: %v", err) |
| } |
| |
| t.Cleanup(func() { |
| os.RemoveAll(tempDir) |
| }) |
| |
| newFilePath := filepath.Join(tempDir, "new_file.txt") |
| content := "This is a new file\nWith multiple lines" |
| |
| // Test the create command |
| result := callEditTool(t, map[string]any{ |
| "command": "create", |
| "path": newFilePath, |
| "file_text": content, |
| }) |
| |
| // Verify the file was created with the right content |
| createdContent, err := os.ReadFile(newFilePath) |
| if err != nil { |
| t.Fatalf("Failed to read created file: %v", err) |
| } |
| |
| if string(createdContent) != content { |
| t.Errorf("Created file content incorrect. Expected:\n%s\nGot:\n%s", content, string(createdContent)) |
| } |
| |
| // Verify the result message |
| if !strings.Contains(result, "File created successfully") { |
| t.Errorf("Result should confirm file creation, got: %s", result) |
| } |
| } |
| |
| // TestEditToolUndoEdit tests the undo_edit command functionality |
| func TestEditToolUndoEdit(t *testing.T) { |
| originalContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" |
| testFile := setupTestFile(t, originalContent) |
| |
| // First modify the file |
| callEditTool(t, map[string]any{ |
| "command": "str_replace", |
| "path": testFile, |
| "old_str": "Line 3", |
| "new_str": "Modified Line 3", |
| }) |
| |
| // Then undo the edit |
| result := callEditTool(t, map[string]any{ |
| "command": "undo_edit", |
| "path": testFile, |
| }) |
| |
| // Verify the file was restored to original content |
| restoredContent, err := os.ReadFile(testFile) |
| if err != nil { |
| t.Fatalf("Failed to read test file: %v", err) |
| } |
| |
| if string(restoredContent) != originalContent { |
| t.Errorf("File content should be restored to original, got: %s", string(restoredContent)) |
| } |
| |
| // Verify the result message |
| if !strings.Contains(result, "undone successfully") { |
| t.Errorf("Result should confirm undo operation, got: %s", result) |
| } |
| } |
| |
| // TestEditToolErrors tests various error conditions |
| func TestEditToolErrors(t *testing.T) { |
| content := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" |
| testFile := setupTestFile(t, content) |
| |
| testCases := []struct { |
| name string |
| input map[string]any |
| errMsg string |
| }{ |
| { |
| name: "Invalid command", |
| input: map[string]any{ |
| "command": "invalid_command", |
| "path": testFile, |
| }, |
| errMsg: "unrecognized command", |
| }, |
| { |
| name: "Non-existent file", |
| input: map[string]any{ |
| "command": "view", |
| "path": "/non/existent/file.txt", |
| }, |
| errMsg: "does not exist", |
| }, |
| { |
| name: "Missing required parameter", |
| input: map[string]any{ |
| "command": "str_replace", |
| "path": testFile, |
| // Missing old_str |
| }, |
| errMsg: "parameter old_str is required", |
| }, |
| { |
| name: "Multiple occurrences in str_replace", |
| input: map[string]any{ |
| "command": "str_replace", |
| "path": testFile, |
| "old_str": "Line", // Appears multiple times |
| "new_str": "Modified Line", |
| }, |
| errMsg: "Multiple occurrences", |
| }, |
| { |
| name: "Invalid view range", |
| input: map[string]any{ |
| "command": "view", |
| "path": testFile, |
| "view_range": []int{10, 20}, // Out of range |
| }, |
| errMsg: "invalid view_range", |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| inputJSON, err := json.Marshal(tc.input) |
| if err != nil { |
| t.Fatalf("Failed to marshal input: %v", err) |
| } |
| |
| _, err = EditRun(context.Background(), inputJSON) |
| if err == nil { |
| t.Fatalf("Expected error but got none") |
| } |
| |
| if !strings.Contains(err.Error(), tc.errMsg) { |
| t.Errorf("Error message does not contain expected text. Expected to contain: %q, Got: %q", tc.errMsg, err.Error()) |
| } |
| }) |
| } |
| } |
| |
| // TestHandleStrReplaceEdgeCases tests the handleStrReplace function specifically for edge cases |
| // that could cause panics like "index out of range [0] with length 0" |
| func TestHandleStrReplaceEdgeCases(t *testing.T) { |
| // The issue was with strings.Split returning an empty slice when the separator wasn't found |
| // This test directly tests the internal implementation with conditions that might cause this |
| |
| // Create a test file with empty content |
| emptyFile := setupTestFile(t, "") |
| |
| // Test with empty file content and arbitrary oldStr |
| _, err := handleStrReplace(emptyFile, "some string that doesn't exist", "new content") |
| if err == nil { |
| t.Fatal("Expected error for empty file but got none") |
| } |
| if !strings.Contains(err.Error(), "did not appear verbatim") { |
| t.Errorf("Expected error message to indicate missing string, got: %s", err.Error()) |
| } |
| |
| // Create a file with content that doesn't match oldStr |
| nonMatchingFile := setupTestFile(t, "This is some content\nthat doesn't contain the target string") |
| |
| // Test with content that doesn't contain oldStr |
| _, err = handleStrReplace(nonMatchingFile, "target string not present", "replacement") |
| if err == nil { |
| t.Fatal("Expected error for non-matching content but got none") |
| } |
| if !strings.Contains(err.Error(), "did not appear verbatim") { |
| t.Errorf("Expected error message to indicate missing string, got: %s", err.Error()) |
| } |
| |
| // Test handling of the edge case that could potentially cause the "index out of range" panic |
| // This directly verifies that the handleStrReplace function properly handles the case where |
| // strings.Split returns an empty or unexpected result |
| |
| // Verify that the protection against empty parts slice works |
| fileContent := "" |
| oldStr := "some string" |
| parts := strings.Split(fileContent, oldStr) |
| if len(parts) == 0 { |
| // This should match the protection in the code |
| parts = []string{""} |
| } |
| |
| // This should not panic with the fix in place |
| _ = strings.Count(parts[0], "\n") // This line would have panicked without the fix |
| } |
| |
| // TestViewRangeWithStrReplace tests that the view_range parameter works correctly |
| // with the str_replace command (tests the full workflow) |
| func TestViewRangeWithStrReplace(t *testing.T) { |
| // Create test file with multiple lines |
| content := "Line 1: First line\nLine 2: Second line\nLine 3: Third line\nLine 4: Fourth line\nLine 5: Fifth line" |
| testFile := setupTestFile(t, content) |
| |
| // First view a subset of the file using view_range |
| viewResult := callEditTool(t, map[string]any{ |
| "command": "view", |
| "path": testFile, |
| "view_range": []int{2, 4}, // Only lines 2-4 |
| }) |
| |
| // Verify that we only see the specified lines |
| if strings.Contains(viewResult, "Line 1:") || strings.Contains(viewResult, "Line 5:") { |
| t.Errorf("View with range should only show lines 2-4, got: %s", viewResult) |
| } |
| if !strings.Contains(viewResult, "Line 2:") || !strings.Contains(viewResult, "Line 4:") { |
| t.Errorf("View with range should show lines 2-4, got: %s", viewResult) |
| } |
| |
| // Now perform a str_replace on one of the lines we viewed |
| replaceResult := callEditTool(t, map[string]any{ |
| "command": "str_replace", |
| "path": testFile, |
| "old_str": "Line 3: Third line", |
| "new_str": "Line 3: MODIFIED Third line", |
| }) |
| |
| // Check that the replacement was successful |
| if !strings.Contains(replaceResult, "Line 3: MODIFIED Third line") { |
| t.Errorf("Replace result should contain the modified line, got: %s", replaceResult) |
| } |
| |
| // Verify the file content was updated correctly |
| modifiedContent, err := os.ReadFile(testFile) |
| if err != nil { |
| t.Fatalf("Failed to read test file after modification: %v", err) |
| } |
| |
| expectedContent := "Line 1: First line\nLine 2: Second line\nLine 3: MODIFIED Third line\nLine 4: Fourth line\nLine 5: Fifth line" |
| if string(modifiedContent) != expectedContent { |
| t.Errorf("File content after replacement is incorrect.\nExpected:\n%s\nGot:\n%s", |
| expectedContent, string(modifiedContent)) |
| } |
| |
| // View the modified file with a different view_range |
| finalViewResult := callEditTool(t, map[string]any{ |
| "command": "view", |
| "path": testFile, |
| "view_range": []int{3, 3}, // Only the modified line |
| }) |
| |
| // Verify we can see only the modified line |
| if !strings.Contains(finalViewResult, "Line 3: MODIFIED Third line") { |
| t.Errorf("Final view should show the modified line, got: %s", finalViewResult) |
| } |
| if strings.Contains(finalViewResult, "Line 2:") || strings.Contains(finalViewResult, "Line 4:") { |
| t.Errorf("Final view should only show line 3, got: %s", finalViewResult) |
| } |
| } |