201 lines
5.7 KiB
Go
201 lines
5.7 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestApplyPatchChangesMultipleFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "old.txt"), []byte("before\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
patch := `*** Begin Patch
|
|
*** Update File: old.txt
|
|
@@
|
|
-before
|
|
+after
|
|
*** Add File: new.txt
|
|
+created
|
|
*** End Patch`
|
|
out, err := NewApplyPatchTool(dir).Execute(context.Background(), mustJSON(t, map[string]any{"patch": patch}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out, "2 file(s) changed") {
|
|
t.Fatalf("output = %q", out)
|
|
}
|
|
assertFileContent(t, filepath.Join(dir, "old.txt"), "after\n")
|
|
assertFileContent(t, filepath.Join(dir, "new.txt"), "created\n")
|
|
if info, err := os.Stat(filepath.Join(dir, "old.txt")); err != nil || info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("updated mode = %v, err = %v", info.Mode().Perm(), err)
|
|
}
|
|
}
|
|
|
|
func TestApplyPatchRollsBackEarlierFilesOnFailure(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "first.txt")
|
|
if err := os.WriteFile(path, []byte("original\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
patch := `*** Begin Patch
|
|
*** Update File: first.txt
|
|
@@
|
|
-original
|
|
+changed
|
|
*** Update File: missing.txt
|
|
@@
|
|
-missing
|
|
+changed
|
|
*** End Patch`
|
|
_, err := NewApplyPatchTool(dir).Execute(context.Background(), mustJSON(t, map[string]any{"patch": patch}))
|
|
if err == nil {
|
|
t.Fatal("expected patch failure")
|
|
}
|
|
assertFileContent(t, path, "original\n")
|
|
}
|
|
|
|
func TestApplyPatchDryRunDoesNotWrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
patch := "*** Begin Patch\n*** Add File: dry.txt\n+dry\n*** End Patch"
|
|
out, err := NewApplyPatchTool(dir).Execute(context.Background(), mustJSON(t, map[string]any{"patch": patch, "dry_run": true}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out, "dry run") {
|
|
t.Fatalf("output = %q", out)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "dry.txt")); !os.IsNotExist(err) {
|
|
t.Fatalf("dry run created file: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApplyPatchRejectsMalformedEnvelopeAndDuplicatePaths(t *testing.T) {
|
|
for _, patch := range []string{
|
|
"*** Begin Patch\n*** Add File: x.txt\n+x",
|
|
"prefix\n*** Begin Patch\n*** Add File: x.txt\n+x\n*** End Patch",
|
|
"*** Begin Patch\n*** Add File: x.txt\n+x\n*** Delete File: x.txt\n*** End Patch",
|
|
} {
|
|
if _, err := parsePatch(patch); err == nil {
|
|
t.Fatalf("expected parse failure for %q", patch)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolvePathRejectsSymlinkEscape(t *testing.T) {
|
|
parent := t.TempDir()
|
|
base := filepath.Join(parent, "project")
|
|
outside := filepath.Join(parent, "outside")
|
|
if err := os.MkdirAll(base, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(outside, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(outside, "secret.txt"), []byte("secret"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(outside, filepath.Join(base, "link")); err != nil {
|
|
t.Skipf("symlinks unavailable: %v", err)
|
|
}
|
|
if _, err := resolvePath(base, "link/secret.txt"); err == nil {
|
|
t.Fatal("expected read symlink escape rejection")
|
|
}
|
|
if _, err := resolvePathForWrite(base, "link/new.txt"); err == nil {
|
|
t.Fatal("expected write symlink escape rejection")
|
|
}
|
|
}
|
|
|
|
func TestTodoRejectsInvalidData(t *testing.T) {
|
|
tool := NewTodoTool()
|
|
if _, err := tool.Execute(context.Background(), json.RawMessage(`{"action":"create","items":[""]}`)); err == nil {
|
|
t.Fatal("expected empty task rejection")
|
|
}
|
|
if _, err := tool.Execute(context.Background(), json.RawMessage(`{"action":"create","items":["one"]}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tool.Execute(context.Background(), json.RawMessage(`{"action":"update","task":"one","status":"invalid"}`)); err == nil {
|
|
t.Fatal("expected invalid status rejection")
|
|
}
|
|
}
|
|
|
|
func TestProjectOutlineIncludesGoTypeScriptAndRustSymbols(t *testing.T) {
|
|
dir := t.TempDir()
|
|
files := map[string]string{
|
|
"main.go": "package demo\nfunc Run() {}\n",
|
|
"ui.ts": "export class Panel {}\nexport function render() {}\n",
|
|
"lib.rs": "pub struct Worker;\npub fn execute() {}\n",
|
|
}
|
|
for name, content := range files {
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
out, err := NewProjectOutlineTool(dir).Execute(context.Background(), json.RawMessage(`{}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{"main.go", "Run", "ui.ts", "Panel", "render", "lib.rs", "Worker", "execute"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("outline missing %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRollbackStoreRestoresInReverseOrder(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "state.txt")
|
|
if err := os.WriteFile(path, []byte("one"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := NewRollbackStore()
|
|
first, err := store.Capture("edit", "state.txt", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("two"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := store.Capture("edit", "state.txt", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("three"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if store.LastID() != second {
|
|
t.Fatalf("last ID = %q", store.LastID())
|
|
}
|
|
if _, err := store.Restore(second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertFileContent(t, path, "two")
|
|
if store.LastID() != first {
|
|
t.Fatalf("last ID after restore = %q, want %q", store.LastID(), first)
|
|
}
|
|
}
|
|
|
|
func mustJSON(t *testing.T, value any) json.RawMessage {
|
|
t.Helper()
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func assertFileContent(t *testing.T, path, want string) {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != want {
|
|
t.Fatalf("%s content = %q, want %q", path, string(data), want)
|
|
}
|
|
}
|