package tools import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" ) func TestFileEditReplacesLineRange(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "notes.txt") if err := os.WriteFile(path, []byte("one\ntwo\nthree\n"), 0o644); err != nil { t.Fatal(err) } edit := NewFileEditTool(dir) out, err := edit.Execute(context.Background(), json.RawMessage(`{"path":"notes.txt","start_line":2,"end_line":2,"new":"TWO\n"}`)) if err != nil { t.Fatal(err) } if !strings.Contains(out, "replaced lines 2-2") { t.Fatalf("output = %q", out) } got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if string(got) != "one\nTWO\nthree\n" { t.Fatalf("file content = %q", string(got)) } } func TestFileEditReplacesExactPattern(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "notes.txt") if err := os.WriteFile(path, []byte("alpha beta alpha"), 0o644); err != nil { t.Fatal(err) } edit := NewFileEditTool(dir) out, err := edit.Execute(context.Background(), json.RawMessage(`{"path":"notes.txt","old":"beta","new":"BETA"}`)) if err != nil { t.Fatal(err) } if !strings.Contains(out, "replaced 1 occurrence") { t.Fatalf("output = %q", out) } got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if string(got) != "alpha BETA alpha" { t.Fatalf("file content = %q", string(got)) } } func TestFileEditRejectsAmbiguousPattern(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "notes.txt") if err := os.WriteFile(path, []byte("x x"), 0o644); err != nil { t.Fatal(err) } edit := NewFileEditTool(dir) _, err := edit.Execute(context.Background(), json.RawMessage(`{"path":"notes.txt","old":"x","new":"y"}`)) if err == nil { t.Fatal("expected error for ambiguous pattern") } if !strings.Contains(err.Error(), "matched 2 occurrences") { t.Fatalf("error = %v", err) } } func TestFileEditReplaceAll(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "notes.txt") if err := os.WriteFile(path, []byte("x x"), 0o644); err != nil { t.Fatal(err) } edit := NewFileEditTool(dir) out, err := edit.Execute(context.Background(), json.RawMessage(`{"path":"notes.txt","old":"x","new":"y","replace_all":true}`)) if err != nil { t.Fatal(err) } if !strings.Contains(out, "replaced 2 occurrences") { t.Fatalf("output = %q", out) } got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if string(got) != "y y" { t.Fatalf("file content = %q", string(got)) } } func TestFileEditRejectsEscapingPath(t *testing.T) { dir := t.TempDir() edit := NewFileEditTool(dir) _, err := edit.Execute(context.Background(), json.RawMessage(`{"path":"../outside.txt","old":"x","new":"y"}`)) if err == nil { t.Fatal("expected error for escaping path") } if !strings.Contains(err.Error(), "escapes") { t.Fatalf("error = %v", err) } } func TestFileEditRejectsMissingNew(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "notes.txt") if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil { t.Fatal(err) } edit := NewFileEditTool(dir) _, err := edit.Execute(context.Background(), json.RawMessage(`{"path":"notes.txt","old":"hello"}`)) if err == nil { t.Fatal("expected error for missing new") } if !strings.Contains(err.Error(), "new is required") { t.Fatalf("error = %v", err) } } func TestFileEditRejectsNonExistentFile(t *testing.T) { dir := t.TempDir() edit := NewFileEditTool(dir) _, err := edit.Execute(context.Background(), json.RawMessage(`{"path":"missing.txt","old":"x","new":"y"}`)) if err == nil { t.Fatal("expected error for non-existent file") } }