返回 DeepSeek-Reasonix
preview_test.go
根目录 / internal / tool / builtin / preview_test.go
1 package builtin
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "reasonix/internal/diff"
10 "reasonix/internal/tool"
11 )
12
13 // TestWritersImplementPreviewer locks in that every file-writer exposes the
14 // optional Previewer capability the front-end type-asserts on. A new writer
15 // that forgets Preview fails here.
16 func TestWritersImplementPreviewer(t *testing.T) {
17 for _, tl := range []tool.Tool{writeFile{}, editFile{}, multiEdit{}} {
18 if _, ok := tl.(tool.Previewer); !ok {
19 t.Errorf("%s does not implement tool.Previewer", tl.Name())
20 }
21 }
22 }
23
24 // TestPreviewMatchesExecute is the anti-drift guarantee: for each writer,
25 // Preview's NewText must equal the bytes Execute actually persists. It runs
26 // both against an identical starting file so a future change to an Execute body
27 // that isn't mirrored into Preview fails this test instead of silently making
28 // the approval card lie.
29 func TestPreviewMatchesExecute(t *testing.T) {
30 cases := []struct {
31 name string
32 tool tool.Tool
33 // seed is the file's content before the call ("" means create fresh).
34 seed string
35 args func(path string) map[string]any
36 }{
37 {
38 name: "write_file create",
39 tool: writeFile{},
40 seed: "",
41 args: func(p string) map[string]any {
42 return map[string]any{"path": p, "content": "fresh\nfile\n"}
43 },
44 },
45 {
46 name: "write_file overwrite",
47 tool: writeFile{},
48 seed: "old content\n",
49 args: func(p string) map[string]any {
50 return map[string]any{"path": p, "content": "new content\n"}
51 },
52 },
53 {
54 name: "edit_file",
55 tool: editFile{},
56 seed: "hello world\n",
57 args: func(p string) map[string]any {
58 return map[string]any{"path": p, "old_string": "world", "new_string": "reasonix"}
59 },
60 },
61 {
62 name: "edit_file fuzzy",
63 tool: editFile{},
64 seed: "alpha \nbeta \n",
65 args: func(p string) map[string]any {
66 return map[string]any{"path": p, "old_string": "alpha\nbeta", "new_string": "ALPHA\nBETA"}
67 },
68 },
69 {
70 name: "multi_edit",
71 tool: multiEdit{},
72 seed: "package old\n\nfunc old() {\n\told()\n}\n",
73 args: func(p string) map[string]any {
74 return map[string]any{"path": p, "edits": []map[string]any{
75 {"old_string": "package old", "new_string": "package new"},
76 {"old_string": "old", "new_string": "reasonix", "replace_all": true},
77 }}
78 },
79 },
80 }
81
82 for _, tc := range cases {
83 t.Run(tc.name, func(t *testing.T) {
84 // Preview against one copy.
85 pf := filepath.Join(t.TempDir(), "f.txt")
86 if tc.seed != "" {
87 os.WriteFile(pf, []byte(tc.seed), 0o644)
88 }
89 prev, ok := tc.tool.(tool.Previewer)
90 if !ok {
91 t.Fatalf("%s not a Previewer", tc.tool.Name())
92 }
93 change, err := prev.Preview(argsJSON(t, tc.args(pf)))
94 if err != nil {
95 t.Fatalf("Preview: %v", err)
96 }
97 // Preview must not have touched disk.
98 if tc.seed == "" {
99 if _, statErr := os.Stat(pf); statErr == nil {
100 t.Fatal("Preview created the file (should be side-effect free)")
101 }
102 } else if b, _ := os.ReadFile(pf); string(b) != tc.seed {
103 t.Fatalf("Preview mutated the file: %q", b)
104 }
105
106 // Execute against a second identical copy.
107 ef := filepath.Join(t.TempDir(), "f.txt")
108 if tc.seed != "" {
109 os.WriteFile(ef, []byte(tc.seed), 0o644)
110 }
111 if _, err := tc.tool.Execute(context.Background(), argsJSON(t, tc.args(ef))); err != nil {
112 t.Fatalf("Execute: %v", err)
113 }
114 got, _ := os.ReadFile(ef)
115 if string(got) != change.NewText {
116 t.Fatalf("Preview.NewText != Execute result\n preview: %q\n execute: %q", change.NewText, got)
117 }
118 if change.OldText != tc.seed {
119 t.Fatalf("Preview.OldText = %q, want seed %q", change.OldText, tc.seed)
120 }
121 })
122 }
123 }
124
125 // TestPreviewKindAndTally checks the metadata a UI shows: create vs modify and
126 // the +N/-M tallies.
127 func TestPreviewKindAndTally(t *testing.T) {
128 // write_file to a nonexistent path is a create.
129 nf := filepath.Join(t.TempDir(), "new.txt")
130 c, err := writeFile{}.Preview(argsJSON(t, map[string]any{"path": nf, "content": "a\nb\nc\n"}))
131 if err != nil {
132 t.Fatal(err)
133 }
134 if c.Kind != diff.Create {
135 t.Errorf("kind = %q, want create", c.Kind)
136 }
137 if c.Added != 3 || c.Removed != 0 {
138 t.Errorf("+%d/-%d, want +3/-0", c.Added, c.Removed)
139 }
140
141 // edit_file on an existing file is a modify with balanced tallies.
142 ef := filepath.Join(t.TempDir(), "e.txt")
143 os.WriteFile(ef, []byte("one\ntwo\nthree\n"), 0o644)
144 c, err = editFile{}.Preview(argsJSON(t, map[string]any{"path": ef, "old_string": "two", "new_string": "TWO"}))
145 if err != nil {
146 t.Fatal(err)
147 }
148 if c.Kind != diff.Modify {
149 t.Errorf("kind = %q, want modify", c.Kind)
150 }
151 if c.Added != 1 || c.Removed != 1 {
152 t.Errorf("+%d/-%d, want +1/-1", c.Added, c.Removed)
153 }
154 }
155
156 // TestPreviewMirrorsErrors confirms an unworkable call fails in Preview the
157 // same way it would in Execute — so a UI never previews an impossible change.
158 func TestPreviewMirrorsErrors(t *testing.T) {
159 f := filepath.Join(t.TempDir(), "x.txt")
160 os.WriteFile(f, []byte("x x x"), 0o644)
161
162 if _, err := (editFile{}).Preview(argsJSON(t, map[string]any{"path": f, "old_string": "x", "new_string": "y"})); err == nil {
163 t.Error("expected not-unique error from Preview")
164 }
165 missing := filepath.Join(t.TempDir(), "nope.txt")
166 if _, err := (editFile{}).Preview(argsJSON(t, map[string]any{"path": missing, "old_string": "a", "new_string": "b"})); err == nil {
167 t.Error("expected read error for missing file")
168 }
169 }
170
170 lines GO