3d403bd685
New tools: - file_edit: surgical line-range or pattern edits (yolo only) - git_status: show changed files (read-only) - git_diff: show unstaged/staged diff (read-only) - git_log: show recent commits (read-only) Agent improvements: - parallel execution for independent read-only tool calls - mutating tools (file_write, file_edit, shell_run) run sequentially - tool output preview in logs with [output] marker TUI improvements: - tool cards now show truncated output preview (up to 8 lines) - mergeToolLog updates placeholder cards with results - toolStatusText keeps activity bar single-line Documentation: - README updated with git tools and file_edit descriptions
137 lines
3.6 KiB
Go
137 lines
3.6 KiB
Go
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")
|
|
}
|
|
}
|