bots-garden/mini-mepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

fileedit_test.go · 307 lines · 10.2 KBGo Blame HistoryRaw
💾 Saved. d722711 k33g 4h ago1// The cases below are the `edit` CLI's own tests (tools/edit/internal/*),
2// ported: what they guard is that the built-in tools refuse exactly what the
3// CLI refuses, in words a model can act on — otherwise the A/B comparison of
4// this part would measure two different tools.
5package fileedit
6
7import (
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strings"
12 "testing"
13)
14
15func inDir(t *testing.T, name, content string) string {
16 t.Helper()
17 path := filepath.Join(t.TempDir(), name)
18 if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
19 t.Fatal(err)
20 }
21 return path
22}
23
24func fileOf(t *testing.T, path string) string {
25 t.Helper()
26 data, err := os.ReadFile(path)
27 if err != nil {
28 t.Fatal(err)
29 }
30 return string(data)
31}
32
33func TestApply(t *testing.T) {
34 cases := []struct {
35 name string
36 body string
37 edits []Edit
38 want string
39 }{
40 {"one replacement", "const port = 3000;\n",
41 []Edit{{"const port = 3000;", "const port = envPort(3000);"}}, "const port = envPort(3000);\n"},
42 {"two independent edits, resolved against the original", "HOST = \"localhost\"\nPORT = 3000\n",
43 []Edit{{`HOST = "localhost"`, `HOST = env("HOST", "localhost")`}, {`PORT = 3000`, `PORT = envInt("PORT", 3000)`}},
44 "HOST = env(\"HOST\", \"localhost\")\nPORT = envInt(\"PORT\", 3000)\n"},
45 {"the order of the edits does not change the result", "a\nb\nc\n",
46 []Edit{{"c", "C"}, {"a", "A"}}, "A\nb\nC\n"},
47 {"an empty new deletes", "keep\nto delete\nkeep too\n",
48 []Edit{{"to delete\n", ""}}, "keep\nkeep too\n"},
49 {"the old text spans several lines", "func f() {\n\treturn 1\n}\n",
50 []Edit{{"func f() {\n\treturn 1\n}", "func f() int {\n\treturn 2\n}"}}, "func f() int {\n\treturn 2\n}\n"},
51 {"an edit is NOT re-read by the next one", "x\ny\n",
52 []Edit{{"x", "y"}, {"y", "z"}}, "y\nz\n"},
53 }
54 for _, tc := range cases {
55 t.Run(tc.name, func(t *testing.T) {
56 got, err := apply(tc.body, tc.edits)
57 if err != nil {
58 t.Fatalf("apply: %v", err)
59 }
60 if got != tc.want {
61 t.Errorf("apply = %q, want %q", got, tc.want)
62 }
63 })
64 }
65}
66
67// TestApplyRefuses guards the rules that make an edit verifiable. Each message
68// is read by a MODEL, which must be able to fix the call without guessing: the
69// test also checks that the message carries the fix.
70func TestApplyRefuses(t *testing.T) {
71 cases := []struct {
72 name string
73 body string
74 edits []Edit
75 says string
76 }{
77 {"text not found", "a\n", []Edit{{"b", "c"}}, "not found"},
78 {"ambiguous text", "x = 1\nx = 1\n", []Edit{{"x = 1", "x = 2"}}, "appears 2 times — add the surrounding lines until it is unique"},
79 {"overlapping regions", "func hello() {\n\treturn \"hello\"\n}\n",
80 []Edit{{"func hello() {\n\treturn \"hello\"\n}", "func hello() {\n\treturn \"bonjour\"\n}"}, {"return \"hello\"", "return \"salut\""}},
81 "overlap"},
82 {"empty old", "a\n", []Edit{{"", "b"}}, "empty old text"},
83 {"an edit that changes nothing", "a\n", []Edit{{"a", "a"}}, "changes nothing"},
84 {"no edit at all", "a\n", nil, "no edit given"},
85 }
86 for _, tc := range cases {
87 t.Run(tc.name, func(t *testing.T) {
88 got, err := apply(tc.body, tc.edits)
89 if err == nil {
90 t.Fatalf("apply succeeded, want an error — result %q", got)
91 }
92 if !strings.Contains(err.Error(), tc.says) {
93 t.Errorf("error = %q, want it to contain %q", err, tc.says)
94 }
95 if got != "" {
96 t.Errorf("apply returned %q along with the error: nothing must be written", got)
97 }
98 })
99 }
100}
101
102func TestRead(t *testing.T) {
103 path := inDir(t, "f.txt", "one\ntwo\nthree\n")
104
105 got, err := Read(path, 0, 0, false)
106 if err != nil || got != "one\ntwo\nthree\n" {
107 t.Errorf("Read = (%q, %v)", got, err)
108 }
109 got, err = Read(path, 2, 2, true)
110 if err != nil || got != " 2 two\n" {
111 t.Errorf("Read 2-2 numbered = (%q, %v)", got, err)
112 }
113 if _, err := Read(filepath.Join(filepath.Dir(path), "nope.txt"), 0, 0, false); err == nil || !strings.Contains(err.Error(), "does not exist") {
114 t.Errorf("Read of a missing file = %v", err)
115 }
116 if _, err := Read(path, 99, 0, false); err == nil || !strings.Contains(err.Error(), "past the end") {
117 t.Errorf("Read start 99 = %v", err)
118 }
119}
120
121func TestWriteCreatesAndReports(t *testing.T) {
122 path := filepath.Join(t.TempDir(), "notes.md")
123
124 r, err := Write(path, "# Notes")
125 if err != nil {
126 t.Fatal(err)
127 }
128 if got := fileOf(t, path); got != "# Notes\n" {
129 t.Errorf("file = %q, want %q — the final newline is missing", got, "# Notes\n")
130 }
131 if !r.Created || !strings.Contains(r.Headline, "created") {
132 t.Errorf("Result = %+v, want it to announce the creation", r)
133 }
134 // Rewriting the same content must do nothing, and SAY so.
135 r, err = Write(path, "# Notes")
136 if err != nil || r.Changed || !strings.Contains(r.Headline, "unchanged") {
137 t.Errorf("identical Write = (%+v, %v)", r, err)
138 }
139}
140
141func TestReplace(t *testing.T) {
142 path := inDir(t, "server.go", "port := 3000\nhost := \"local\"\n")
143
144 r, err := Replace(path, []Edit{{"port := 3000", "port := envPort(3000)"}}, false)
145 if err != nil {
146 t.Fatal(err)
147 }
148 if got := fileOf(t, path); got != "port := envPort(3000)\nhost := \"local\"\n" {
149 t.Errorf("file = %q", got)
150 }
151 if r.FirstChangedLine != 1 || !strings.Contains(r.Headline, "first change at line 1") || !strings.Contains(r.Diff, "+ port := envPort(3000)") {
152 t.Errorf("Result = %+v", r)
153 }
154}
155
156// TestReplaceIsAllOrNothing: the second edit is impossible, so the first must
157// not have happened. A half-edited file is the worst result — it looks like a
158// success.
159func TestReplaceIsAllOrNothing(t *testing.T) {
160 const before = "a = 1\nb = 2\n"
161 path := inDir(t, "f.txt", before)
162 _, err := Replace(path, []Edit{{"a = 1", "a = 10"}, {"absent", "x"}}, false)
163 if err == nil || !strings.Contains(err.Error(), "not found") {
164 t.Errorf("err = %v", err)
165 }
166 if got := fileOf(t, path); got != before {
167 t.Errorf("file = %q, want unchanged %q", got, before)
168 }
169}
170
171func TestReplaceRefusesAMissingFile(t *testing.T) {
172 _, err := Replace(filepath.Join(t.TempDir(), "nope.txt"), []Edit{{"a", "b"}}, false)
173 if err == nil || !strings.Contains(err.Error(), "does not exist") {
174 t.Errorf("err = %v", err)
175 }
176}
177
178func TestDryRunWritesNothing(t *testing.T) {
179 const before = "a = 1\n"
180 path := inDir(t, "f.txt", before)
181 r, err := Replace(path, []Edit{{"a = 1", "a = 2"}}, true)
182 if err != nil || !r.DryRun || !strings.Contains(r.Headline, "dry run") || !strings.Contains(r.Diff, "+ a = 2") {
183 t.Errorf("dry run = (%+v, %v)", r, err)
184 }
185 if got := fileOf(t, path); got != before {
186 t.Errorf("file = %q, want unchanged", got)
187 }
188}
189
190// TestShapeSurvives: BOM and CRLF are removed for the edit and put back on
191// write, else every line of a Windows file would show up as changed.
192func TestShapeSurvives(t *testing.T) {
193 cases := []struct{ name, before, old, new, after string }{
194 {"CRLF kept", "a\r\nb\r\n", "b", "B", "a\r\nB\r\n"},
195 {"BOM kept", "\uFEFFa\n", "a", "A", "\uFEFFA\n"},
196 {"plain LF", "a\n", "a", "A", "A\n"},
197 }
198 for _, tc := range cases {
199 t.Run(tc.name, func(t *testing.T) {
200 path := inDir(t, "f.txt", tc.before)
201 if _, err := Replace(path, []Edit{{tc.old, tc.new}}, false); err != nil {
202 t.Fatal(err)
203 }
204 if got := fileOf(t, path); got != tc.after {
205 t.Errorf("file = %q, want %q", got, tc.after)
206 }
207 })
208 }
209}
210
211// TestWriteKeepsTheMode: an executable script that gets edited must stay
212// executable.
213func TestWriteKeepsTheMode(t *testing.T) {
214 path := inDir(t, "s.sh", "echo a\n")
215 if err := os.Chmod(path, 0o755); err != nil {
216 t.Fatal(err)
217 }
218 if _, err := Write(path, "echo b\n"); err != nil {
219 t.Fatal(err)
220 }
221 info, _ := os.Stat(path)
222 if info.Mode().Perm() != 0o755 {
223 t.Errorf("mode = %v, want 755", info.Mode().Perm())
224 }
225}
226
227func TestWriteCreatesParentDirs(t *testing.T) {
228 path := filepath.Join(t.TempDir(), "a", "b", "notes.md")
229 if _, err := Write(path, "# Notes\n"); err != nil {
230 t.Fatalf("Write: %v", err)
231 }
232 if got := fileOf(t, path); got != "# Notes\n" {
233 t.Errorf("file = %q", got)
234 }
235}
236
237func TestFirstChanged(t *testing.T) {
238 cases := []struct {
239 name string
240 old, new string
241 want int
242 }{
243 {"nothing changed", "a\nb\n", "a\nb\n", 0},
244 {"the second line", "a\nb\nc\n", "a\nB\nc\n", 2},
245 {"an addition at the top", "a\n", "zero\na\n", 1},
246 {"a deletion", "a\nb\nc\n", "a\nc\n", 2},
247 {"a created file", "", "a\n", 1},
248 }
249 for _, tc := range cases {
250 t.Run(tc.name, func(t *testing.T) {
251 if got := compare(tc.old, tc.new).firstChanged(); got != tc.want {
252 t.Errorf("firstChanged = %d, want %d", got, tc.want)
253 }
254 })
255 }
256}
257
258func TestRenderHasNoANSI(t *testing.T) {
259 out := compare("a\nb\n", "a\nB\n").render()
260 if strings.Contains(out, "\033") || !strings.Contains(out, "- b") || !strings.Contains(out, "+ B") {
261 t.Errorf("render = %q", out)
262 }
263}
264
265// TestUnifiedMatchesGitApply is the test that counts: a patch that "looks
266// like" a unified patch is useless. git APPLIES it, and refuses anything that
267// is not exact — headers, line numbers, counts, end-of-file marker.
268func TestUnifiedMatchesGitApply(t *testing.T) {
269 if _, err := exec.LookPath("git"); err != nil {
270 t.Skip("git absent")
271 }
272 cases := []struct{ name, old, new string }{
273 {"one line in the middle", "a\nb\nc\nd\ne\n", "a\nb\nC\nd\ne\n"},
274 {"two distant areas", "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n", "1\n2\nTHREE\n4\n5\n6\n7\n8\n9\n10\n11\n12\nTHIRTEEN\n14\n15\n"},
275 {"two neighbouring areas: one hunk", "1\n2\n3\n4\n5\n6\n7\n8\n", "1\n2\nTHREE\n4\nFIVE\n6\n7\n8\n"},
276 {"addition at the top", "a\nb\n", "zero\na\nb\n"},
277 {"deletion at the tail", "a\nb\nc\n", "a\nb\n"},
278 {"everything replaced", "a\nb\n", "x\ny\nz\n"},
279 {"no final newline", "a\nb\nc", "a\nB\nc"},
280 {"final newline added", "a\nb", "a\nb\n"},
281 {"file emptied", "a\nb\n", ""},
282 }
283 for _, tc := range cases {
284 t.Run(tc.name, func(t *testing.T) {
285 patch := compare(tc.old, tc.new).unified("f.txt")
286 if patch == "" {
287 t.Fatal("empty patch")
288 }
289 dir := t.TempDir()
290 run := func(args ...string) {
291 t.Helper()
292 cmd := exec.Command("git", args...)
293 cmd.Dir = dir
294 if out, err := cmd.CombinedOutput(); err != nil {
295 t.Fatalf("git %s: %v\n%s\npatch:\n%s", strings.Join(args, " "), err, out, patch)
296 }
297 }
298 run("init", "-q")
299 os.WriteFile(filepath.Join(dir, "f.txt"), []byte(tc.old), 0o644)
300 os.WriteFile(filepath.Join(dir, "p.diff"), []byte(patch), 0o644)
301 run("apply", "p.diff")
302 if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new {
303 t.Errorf("after apply: %q, want %q\npatch:\n%s", got, tc.new, patch)
304 }
305 })
306 }
307}