feat: complete v0.3.1 agent workflow
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user