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
143 lines
3.6 KiB
Go
143 lines
3.6 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func requireGit(t *testing.T) {
|
|
t.Helper()
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("git not installed")
|
|
}
|
|
}
|
|
|
|
func runGitForTest(t *testing.T, dir string, args ...string) {
|
|
t.Helper()
|
|
gitArgs := append([]string{"-C", dir}, args...)
|
|
cmd := exec.Command("git", gitArgs...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %v failed: %v\n%s", args, err, out)
|
|
}
|
|
}
|
|
|
|
func initTestRepo(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
runGitForTest(t, dir, "init")
|
|
runGitForTest(t, dir, "-c", "user.email=test@example.com", "-c", "user.name=Test", "commit", "--allow-empty", "-m", "initial")
|
|
return dir
|
|
}
|
|
|
|
func TestGitStatusToolShowsChangedFiles(t *testing.T) {
|
|
requireGit(t)
|
|
dir := initTestRepo(t)
|
|
|
|
path := filepath.Join(dir, "tracked.txt")
|
|
if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runGitForTest(t, dir, "add", "tracked.txt")
|
|
runGitForTest(t, dir, "-c", "user.email=test@example.com", "-c", "user.name=Test", "commit", "-m", "add tracked")
|
|
|
|
if err := os.WriteFile(path, []byte("world"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tool := NewGitStatusTool(dir)
|
|
out, err := tool.Execute(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out, "Changed files:") {
|
|
t.Fatalf("output missing 'Changed files:': %q", out)
|
|
}
|
|
if !strings.Contains(out, "M tracked.txt") {
|
|
t.Fatalf("output missing 'M tracked.txt': %q", out)
|
|
}
|
|
}
|
|
|
|
func TestGitDiffToolShowsUnstagedDiff(t *testing.T) {
|
|
requireGit(t)
|
|
dir := initTestRepo(t)
|
|
|
|
path := filepath.Join(dir, "tracked.txt")
|
|
if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runGitForTest(t, dir, "add", "tracked.txt")
|
|
runGitForTest(t, dir, "-c", "user.email=test@example.com", "-c", "user.name=Test", "commit", "-m", "add tracked")
|
|
|
|
if err := os.WriteFile(path, []byte("world"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tool := NewGitDiffTool(dir)
|
|
out, err := tool.Execute(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out, "diff --git") {
|
|
t.Fatalf("output missing 'diff --git': %q", out)
|
|
}
|
|
}
|
|
|
|
func TestGitDiffToolShowsStat(t *testing.T) {
|
|
requireGit(t)
|
|
dir := initTestRepo(t)
|
|
|
|
path := filepath.Join(dir, "tracked.txt")
|
|
if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runGitForTest(t, dir, "add", "tracked.txt")
|
|
runGitForTest(t, dir, "-c", "user.email=test@example.com", "-c", "user.name=Test", "commit", "-m", "add tracked")
|
|
|
|
if err := os.WriteFile(path, []byte("world"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tool := NewGitDiffTool(dir)
|
|
out, err := tool.Execute(context.Background(), json.RawMessage(`{"stat":true}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out, "tracked.txt") {
|
|
t.Fatalf("output missing 'tracked.txt': %q", out)
|
|
}
|
|
}
|
|
|
|
func TestGitLogToolShowsCommits(t *testing.T) {
|
|
requireGit(t)
|
|
dir := initTestRepo(t)
|
|
|
|
tool := NewGitLogTool(dir)
|
|
out, err := tool.Execute(context.Background(), json.RawMessage(`{"max_count":1}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out, "initial") {
|
|
t.Fatalf("output missing 'initial': %q", out)
|
|
}
|
|
}
|
|
|
|
func TestGitToolsRejectEscapingPath(t *testing.T) {
|
|
requireGit(t)
|
|
dir := initTestRepo(t)
|
|
|
|
tool := NewGitStatusTool(dir)
|
|
_, err := tool.Execute(context.Background(), json.RawMessage(`{"path":"../outside"}`))
|
|
if err == nil {
|
|
t.Fatal("expected error for escaping path")
|
|
}
|
|
if !strings.Contains(err.Error(), "escapes") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
}
|