170 lines
5.3 KiB
Go
170 lines
5.3 KiB
Go
package builtin
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"reasonix/internal/diff"
|
|
"reasonix/internal/tool"
|
|
)
|
|
|
|
// TestWritersImplementPreviewer locks in that every file-writer exposes the
|
|
// optional Previewer capability the front-end type-asserts on. A new writer
|
|
// that forgets Preview fails here.
|
|
func TestWritersImplementPreviewer(t *testing.T) {
|
|
for _, tl := range []tool.Tool{writeFile{}, editFile{}, multiEdit{}} {
|
|
if _, ok := tl.(tool.Previewer); !ok {
|
|
t.Errorf("%s does not implement tool.Previewer", tl.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPreviewMatchesExecute is the anti-drift guarantee: for each writer,
|
|
// Preview's NewText must equal the bytes Execute actually persists. It runs
|
|
// both against an identical starting file so a future change to an Execute body
|
|
// that isn't mirrored into Preview fails this test instead of silently making
|
|
// the approval card lie.
|
|
func TestPreviewMatchesExecute(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
tool tool.Tool
|
|
// seed is the file's content before the call ("" means create fresh).
|
|
seed string
|
|
args func(path string) map[string]any
|
|
}{
|
|
{
|
|
name: "write_file create",
|
|
tool: writeFile{},
|
|
seed: "",
|
|
args: func(p string) map[string]any {
|
|
return map[string]any{"path": p, "content": "fresh\nfile\n"}
|
|
},
|
|
},
|
|
{
|
|
name: "write_file overwrite",
|
|
tool: writeFile{},
|
|
seed: "old content\n",
|
|
args: func(p string) map[string]any {
|
|
return map[string]any{"path": p, "content": "new content\n"}
|
|
},
|
|
},
|
|
{
|
|
name: "edit_file",
|
|
tool: editFile{},
|
|
seed: "hello world\n",
|
|
args: func(p string) map[string]any {
|
|
return map[string]any{"path": p, "old_string": "world", "new_string": "reasonix"}
|
|
},
|
|
},
|
|
{
|
|
name: "edit_file fuzzy",
|
|
tool: editFile{},
|
|
seed: "alpha \nbeta \n",
|
|
args: func(p string) map[string]any {
|
|
return map[string]any{"path": p, "old_string": "alpha\nbeta", "new_string": "ALPHA\nBETA"}
|
|
},
|
|
},
|
|
{
|
|
name: "multi_edit",
|
|
tool: multiEdit{},
|
|
seed: "package old\n\nfunc old() {\n\told()\n}\n",
|
|
args: func(p string) map[string]any {
|
|
return map[string]any{"path": p, "edits": []map[string]any{
|
|
{"old_string": "package old", "new_string": "package new"},
|
|
{"old_string": "old", "new_string": "reasonix", "replace_all": true},
|
|
}}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Preview against one copy.
|
|
pf := filepath.Join(t.TempDir(), "f.txt")
|
|
if tc.seed != "" {
|
|
os.WriteFile(pf, []byte(tc.seed), 0o644)
|
|
}
|
|
prev, ok := tc.tool.(tool.Previewer)
|
|
if !ok {
|
|
t.Fatalf("%s not a Previewer", tc.tool.Name())
|
|
}
|
|
change, err := prev.Preview(argsJSON(t, tc.args(pf)))
|
|
if err != nil {
|
|
t.Fatalf("Preview: %v", err)
|
|
}
|
|
// Preview must not have touched disk.
|
|
if tc.seed == "" {
|
|
if _, statErr := os.Stat(pf); statErr == nil {
|
|
t.Fatal("Preview created the file (should be side-effect free)")
|
|
}
|
|
} else if b, _ := os.ReadFile(pf); string(b) != tc.seed {
|
|
t.Fatalf("Preview mutated the file: %q", b)
|
|
}
|
|
|
|
// Execute against a second identical copy.
|
|
ef := filepath.Join(t.TempDir(), "f.txt")
|
|
if tc.seed != "" {
|
|
os.WriteFile(ef, []byte(tc.seed), 0o644)
|
|
}
|
|
if _, err := tc.tool.Execute(context.Background(), argsJSON(t, tc.args(ef))); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
got, _ := os.ReadFile(ef)
|
|
if string(got) != change.NewText {
|
|
t.Fatalf("Preview.NewText != Execute result\n preview: %q\n execute: %q", change.NewText, got)
|
|
}
|
|
if change.OldText != tc.seed {
|
|
t.Fatalf("Preview.OldText = %q, want seed %q", change.OldText, tc.seed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPreviewKindAndTally checks the metadata a UI shows: create vs modify and
|
|
// the +N/-M tallies.
|
|
func TestPreviewKindAndTally(t *testing.T) {
|
|
// write_file to a nonexistent path is a create.
|
|
nf := filepath.Join(t.TempDir(), "new.txt")
|
|
c, err := writeFile{}.Preview(argsJSON(t, map[string]any{"path": nf, "content": "a\nb\nc\n"}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Kind != diff.Create {
|
|
t.Errorf("kind = %q, want create", c.Kind)
|
|
}
|
|
if c.Added != 3 || c.Removed != 0 {
|
|
t.Errorf("+%d/-%d, want +3/-0", c.Added, c.Removed)
|
|
}
|
|
|
|
// edit_file on an existing file is a modify with balanced tallies.
|
|
ef := filepath.Join(t.TempDir(), "e.txt")
|
|
os.WriteFile(ef, []byte("one\ntwo\nthree\n"), 0o644)
|
|
c, err = editFile{}.Preview(argsJSON(t, map[string]any{"path": ef, "old_string": "two", "new_string": "TWO"}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Kind != diff.Modify {
|
|
t.Errorf("kind = %q, want modify", c.Kind)
|
|
}
|
|
if c.Added != 1 || c.Removed != 1 {
|
|
t.Errorf("+%d/-%d, want +1/-1", c.Added, c.Removed)
|
|
}
|
|
}
|
|
|
|
// TestPreviewMirrorsErrors confirms an unworkable call fails in Preview the
|
|
// same way it would in Execute — so a UI never previews an impossible change.
|
|
func TestPreviewMirrorsErrors(t *testing.T) {
|
|
f := filepath.Join(t.TempDir(), "x.txt")
|
|
os.WriteFile(f, []byte("x x x"), 0o644)
|
|
|
|
if _, err := (editFile{}).Preview(argsJSON(t, map[string]any{"path": f, "old_string": "x", "new_string": "y"})); err == nil {
|
|
t.Error("expected not-unique error from Preview")
|
|
}
|
|
missing := filepath.Join(t.TempDir(), "nope.txt")
|
|
if _, err := (editFile{}).Preview(argsJSON(t, map[string]any{"path": missing, "old_string": "a", "new_string": "b"})); err == nil {
|
|
t.Error("expected read error for missing file")
|
|
}
|
|
}
|