blob: 93bbe1c20018727ad507124e3893fbe4a4ddeac7 [file] [log] [blame]
package claudetool
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"sketch.dev/llm"
)
func TestPatchTool_BasicOperations(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
// Test overwrite operation (creates new file)
testFile := filepath.Join(tempDir, "test.txt")
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: "Hello World\n",
}},
}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("overwrite failed: %v", result.Error)
}
content, err := os.ReadFile(testFile)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
if string(content) != "Hello World\n" {
t.Errorf("expected 'Hello World\\n', got %q", string(content))
}
// Test replace operation
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "World",
NewText: "Patch",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("replace failed: %v", result.Error)
}
content, _ = os.ReadFile(testFile)
if string(content) != "Hello Patch\n" {
t.Errorf("expected 'Hello Patch\\n', got %q", string(content))
}
// Test append_eof operation
input.Patches = []PatchRequest{{
Operation: "append_eof",
NewText: "Appended line\n",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("append_eof failed: %v", result.Error)
}
content, _ = os.ReadFile(testFile)
expected := "Hello Patch\nAppended line\n"
if string(content) != expected {
t.Errorf("expected %q, got %q", expected, string(content))
}
// Test prepend_bof operation
input.Patches = []PatchRequest{{
Operation: "prepend_bof",
NewText: "Prepended line\n",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("prepend_bof failed: %v", result.Error)
}
content, _ = os.ReadFile(testFile)
expected = "Prepended line\nHello Patch\nAppended line\n"
if string(content) != expected {
t.Errorf("expected %q, got %q", expected, string(content))
}
}
func TestPatchTool_ClipboardOperations(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
testFile := filepath.Join(tempDir, "clipboard.txt")
// Create initial content
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: "function original() {\n return 'original';\n}\n",
}},
}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("initial overwrite failed: %v", result.Error)
}
// Test toClipboard operation
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "function original() {\n return 'original';\n}",
NewText: "function renamed() {\n return 'renamed';\n}",
ToClipboard: "saved_func",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("toClipboard failed: %v", result.Error)
}
// Test fromClipboard operation
input.Patches = []PatchRequest{{
Operation: "append_eof",
FromClipboard: "saved_func",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("fromClipboard failed: %v", result.Error)
}
content, _ := os.ReadFile(testFile)
if !strings.Contains(string(content), "function original()") {
t.Error("clipboard content not restored properly")
}
}
func TestPatchTool_IndentationAdjustment(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
testFile := filepath.Join(tempDir, "indent.go")
// Create file with tab indentation
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: "package main\n\nfunc main() {\n\tif true {\n\t\t// placeholder\n\t}\n}\n",
}},
}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("initial setup failed: %v", result.Error)
}
// Test indentation adjustment: convert spaces to tabs
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "// placeholder",
NewText: " fmt.Println(\"hello\")\n fmt.Println(\"world\")",
Reindent: &Reindent{
Strip: " ",
Add: "\t\t",
},
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("indentation adjustment failed: %v", result.Error)
}
content, _ := os.ReadFile(testFile)
expected := "\t\tfmt.Println(\"hello\")\n\t\tfmt.Println(\"world\")"
if !strings.Contains(string(content), expected) {
t.Errorf("indentation not adjusted correctly, got:\n%s", string(content))
}
}
func TestPatchTool_FuzzyMatching(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
testFile := filepath.Join(tempDir, "fuzzy.go")
// Create Go file with specific indentation
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: "package main\n\nfunc test() {\n\tif condition {\n\t\tfmt.Println(\"hello\")\n\t\tfmt.Println(\"world\")\n\t}\n}\n",
}},
}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("initial setup failed: %v", result.Error)
}
// Test fuzzy matching with different whitespace
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "if condition {\n fmt.Println(\"hello\")\n fmt.Println(\"world\")\n }", // spaces instead of tabs
NewText: "if condition {\n\t\tfmt.Println(\"modified\")\n\t}",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("fuzzy matching failed: %v", result.Error)
}
content, _ := os.ReadFile(testFile)
if !strings.Contains(string(content), "modified") {
t.Error("fuzzy matching did not work")
}
}
func TestPatchTool_ErrorCases(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
testFile := filepath.Join(tempDir, "error.txt")
// Test replace operation on non-existent file
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "replace",
OldText: "something",
NewText: "else",
}},
}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if result.Error == nil {
t.Error("expected error for replace on non-existent file")
}
// Create file with duplicate text
input.Patches = []PatchRequest{{
Operation: "overwrite",
NewText: "duplicate\nduplicate\n",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("failed to create test file: %v", result.Error)
}
// Test non-unique text
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "duplicate",
NewText: "unique",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error == nil || !strings.Contains(result.Error.Error(), "not unique") {
t.Error("expected non-unique error")
}
// Test missing text
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "nonexistent",
NewText: "something",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error == nil || !strings.Contains(result.Error.Error(), "not found") {
t.Error("expected not found error")
}
// Test invalid clipboard reference
input.Patches = []PatchRequest{{
Operation: "append_eof",
FromClipboard: "nonexistent",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error == nil || !strings.Contains(result.Error.Error(), "clipboard") {
t.Error("expected clipboard error")
}
}
func TestPatchTool_FlexibleInputParsing(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
testFile := filepath.Join(tempDir, "flexible.txt")
// Test single patch format (PatchInputOne)
inputOne := PatchInputOne{
Path: testFile,
Patches: PatchRequest{
Operation: "overwrite",
NewText: "Single patch format\n",
},
}
msg, _ := json.Marshal(inputOne)
result := patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("single patch format failed: %v", result.Error)
}
content, _ := os.ReadFile(testFile)
if string(content) != "Single patch format\n" {
t.Error("single patch format did not work")
}
// Test string patch format (PatchInputOneString)
patchStr := `{"operation": "replace", "oldText": "Single", "newText": "Modified"}`
inputStr := PatchInputOneString{
Path: testFile,
Patches: patchStr,
}
msg, _ = json.Marshal(inputStr)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("string patch format failed: %v", result.Error)
}
content, _ = os.ReadFile(testFile)
if !strings.Contains(string(content), "Modified") {
t.Error("string patch format did not work")
}
}
func TestPatchTool_AutogeneratedDetection(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
testFile := filepath.Join(tempDir, "generated.go")
// Create autogenerated file
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: "// Code generated by tool. DO NOT EDIT.\npackage main\n\nfunc generated() {}\n",
}},
}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("failed to create generated file: %v", result.Error)
}
// Test patching autogenerated file (should warn but work)
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "func generated() {}",
NewText: "func modified() {}",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("patching generated file failed: %v", result.Error)
}
if len(result.LLMContent) == 0 || !strings.Contains(result.LLMContent[0].Text, "autogenerated") {
t.Error("expected autogenerated warning")
}
}
func TestPatchTool_MultiplePatches(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
testFile := filepath.Join(tempDir, "multi.go")
var msg []byte
var result llm.ToolOut
// Apply multiple patches - first create file, then modify
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: "package main\n\nfunc first() {\n\tprintln(\"first\")\n}\n\nfunc second() {\n\tprintln(\"second\")\n}\n",
}},
}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("failed to create initial file: %v", result.Error)
}
// Now apply multiple patches in one call
input.Patches = []PatchRequest{
{
Operation: "replace",
OldText: "println(\"first\")",
NewText: "println(\"ONE\")",
},
{
Operation: "replace",
OldText: "println(\"second\")",
NewText: "println(\"TWO\")",
},
{
Operation: "append_eof",
NewText: "\n// Multiple patches applied\n",
},
}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("multiple patches failed: %v", result.Error)
}
content, _ := os.ReadFile(testFile)
contentStr := string(content)
if !strings.Contains(contentStr, "ONE") || !strings.Contains(contentStr, "TWO") {
t.Error("multiple patches not applied correctly")
}
if !strings.Contains(contentStr, "Multiple patches applied") {
t.Error("append_eof in multiple patches not applied")
}
}
func TestPatchTool_CopyRecipe(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
testFile := filepath.Join(tempDir, "copy.txt")
// Create initial content
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: "original text",
}},
}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("failed to create file: %v", result.Error)
}
// Test copy recipe (toClipboard + fromClipboard with same name)
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "original text",
NewText: "replaced text",
ToClipboard: "copy_test",
FromClipboard: "copy_test",
}}
msg, _ = json.Marshal(input)
result = patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("copy recipe failed: %v", result.Error)
}
content, _ := os.ReadFile(testFile)
// The copy recipe should preserve the original text
if string(content) != "original text" {
t.Errorf("copy recipe failed, expected 'original text', got %q", string(content))
}
}
func TestPatchTool_RelativePaths(t *testing.T) {
tempDir := t.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
// Test relative path resolution
input := PatchInput{
Path: "relative.txt", // relative path
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: "relative path test\n",
}},
}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if result.Error != nil {
t.Fatalf("relative path failed: %v", result.Error)
}
// Check file was created in correct location
expectedPath := filepath.Join(tempDir, "relative.txt")
content, err := os.ReadFile(expectedPath)
if err != nil {
t.Fatalf("file not created at expected path: %v", err)
}
if string(content) != "relative path test\n" {
t.Error("relative path file content incorrect")
}
}
// Benchmark basic patch operations
func BenchmarkPatchTool_BasicOperations(b *testing.B) {
tempDir := b.TempDir()
patch := &PatchTool{Pwd: tempDir}
ctx := context.Background()
testFile := filepath.Join(tempDir, "bench.go")
initialContent := "package main\n\nfunc test() {\n\tfor i := 0; i < 100; i++ {\n\t\tfmt.Println(i)\n\t}\n}\n"
// Setup
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: initialContent,
}},
}
msg, _ := json.Marshal(input)
patch.Run(ctx, msg)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Benchmark replace operation
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "fmt.Println(i)",
NewText: "fmt.Printf(\"%d\\n\", i)",
}}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if result.Error != nil {
b.Fatalf("benchmark failed: %v", result.Error)
}
// Reset for next iteration
input.Patches = []PatchRequest{{
Operation: "replace",
OldText: "fmt.Printf(\"%d\\n\", i)",
NewText: "fmt.Println(i)",
}}
msg, _ = json.Marshal(input)
patch.Run(ctx, msg)
}
}
func TestPatchTool_CallbackFunction(t *testing.T) {
tempDir := t.TempDir()
callbackCalled := false
var capturedInput PatchInput
var capturedOutput llm.ToolOut
patch := &PatchTool{
Pwd: tempDir,
Callback: func(input PatchInput, output llm.ToolOut) llm.ToolOut {
callbackCalled = true
capturedInput = input
capturedOutput = output
// Modify the output
output.LLMContent = llm.TextContent("Modified by callback")
return output
},
}
ctx := context.Background()
testFile := filepath.Join(tempDir, "callback.txt")
input := PatchInput{
Path: testFile,
Patches: []PatchRequest{{
Operation: "overwrite",
NewText: "callback test",
}},
}
msg, _ := json.Marshal(input)
result := patch.Run(ctx, msg)
if !callbackCalled {
t.Error("callback was not called")
}
if capturedInput.Path != testFile {
t.Error("callback did not receive correct input")
}
if len(result.LLMContent) == 0 || result.LLMContent[0].Text != "Modified by callback" {
t.Error("callback did not modify output correctly")
}
if capturedOutput.Error != nil {
t.Errorf("callback received error: %v", capturedOutput.Error)
}
}