返回 DeepSeek-Reasonix
delete_range_test.go
根目录 / internal / tool / builtin / delete_range_test.go
1 package builtin
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 )
10
11 func TestDeleteRangeBasic(t *testing.T) {
12 f := filepath.Join(t.TempDir(), "a.txt")
13 body := "line1\nline2\nline3\nline4\nline5\n"
14 os.WriteFile(f, []byte(body), 0o644)
15
16 out := runTool(t, deleteRange{}, map[string]any{
17 "path": f, "start_anchor": "line2", "end_anchor": "line4",
18 })
19 if !strings.Contains(out, "---") || !strings.Contains(out, "+++") {
20 t.Errorf("expected unified diff output, got: %s", out)
21 }
22 got, _ := os.ReadFile(f)
23 want := "line1\nline5\n"
24 if string(got) != want {
25 t.Errorf("file = %q, want %q", got, want)
26 }
27 }
28
29 func TestDeleteRangeInclusive(t *testing.T) {
30 f := filepath.Join(t.TempDir(), "a.txt")
31 os.WriteFile(f, []byte("line1\nline2\nline3\nline4\nline5\n"), 0o644)
32 runTool(t, deleteRange{}, map[string]any{
33 "path": f, "start_anchor": "line2", "end_anchor": "line4", "inclusive": true,
34 })
35 got, _ := os.ReadFile(f)
36 if string(got) != "line1\nline5\n" {
37 t.Errorf("inclusive=true: got %q, want %q", got, "line1\\nline5\\n")
38 }
39
40 f2 := filepath.Join(t.TempDir(), "b.txt")
41 os.WriteFile(f2, []byte("line1\nline2\nline3\nline4\nline5\n"), 0o644)
42 runTool(t, deleteRange{}, map[string]any{
43 "path": f2, "start_anchor": "line2", "end_anchor": "line4", "inclusive": false,
44 })
45 got2, _ := os.ReadFile(f2)
46 if string(got2) != "line1\nline2\nline4\nline5\n" {
47 t.Errorf("inclusive=false: got %q, want %q", got2, "line1\\nline2\\nline4\\nline5\\n")
48 }
49 }
50
51 func TestDeleteRangeDuplicateAnchor(t *testing.T) {
52 f := filepath.Join(t.TempDir(), "dup.txt")
53 body := "line1\nline2\nline3\nline2\nline5\n"
54 os.WriteFile(f, []byte(body), 0o644)
55
56 args := argsJSON(t, map[string]any{
57 "path": f, "start_anchor": "line2", "end_anchor": "line5",
58 })
59 _, err := (deleteRange{}).Execute(context.Background(), args)
60 if err == nil {
61 t.Fatal("expected duplicate anchor error")
62 }
63 if !strings.Contains(err.Error(), "not unique") {
64 t.Errorf("error should mention 'not unique': %v", err)
65 }
66 got, _ := os.ReadFile(f)
67 if string(got) != body {
68 t.Errorf("file modified despite error: %q", got)
69 }
70 }
71
72 func TestDeleteRangeMissingAnchor(t *testing.T) {
73 f := filepath.Join(t.TempDir(), "missing.txt")
74 body := "line1\nline2\nline3\n"
75 os.WriteFile(f, []byte(body), 0o644)
76
77 args := argsJSON(t, map[string]any{
78 "path": f, "start_anchor": "line2", "end_anchor": "no_such_line",
79 })
80 _, err := (deleteRange{}).Execute(context.Background(), args)
81 if err == nil {
82 t.Fatal("expected missing anchor error")
83 }
84 if !strings.Contains(err.Error(), "not found") {
85 t.Errorf("error should mention 'not found': %v", err)
86 }
87 }
88
89 func TestDeleteRangeReversed(t *testing.T) {
90 f := filepath.Join(t.TempDir(), "rev.txt")
91 body := "line1\nline2\nline3\nline4\nline5\n"
92 os.WriteFile(f, []byte(body), 0o644)
93
94 args := argsJSON(t, map[string]any{
95 "path": f, "start_anchor": "line4", "end_anchor": "line2",
96 })
97 _, err := (deleteRange{}).Execute(context.Background(), args)
98 if err == nil {
99 t.Fatal("expected reversed anchor error")
100 }
101 if !strings.Contains(err.Error(), "after") {
102 t.Errorf("error should mention ordering: %v", err)
103 }
104 }
105
106 func TestDeleteRangeRejectsEndAnchorOpeningBlock(t *testing.T) {
107 f := filepath.Join(t.TempDir(), "map.html")
108 body := strings.Join([]string{
109 "// map engine",
110 "function switchToProvinceMap() {",
111 " redraw();",
112 "}",
113 "function highlightCurrentOnMap() {",
114 " if (!state.mapInstance) return;",
115 " repaint();",
116 "}",
117 "",
118 }, "\n")
119 os.WriteFile(f, []byte(body), 0o644)
120
121 args := argsJSON(t, map[string]any{
122 "path": f, "start_anchor": "// map engine", "end_anchor": "function highlightCurrentOnMap() {",
123 })
124 _, err := (deleteRange{}).Execute(context.Background(), args)
125 if err == nil {
126 t.Fatal("expected block-opening end_anchor to be rejected")
127 }
128 for _, want := range []string{"appears to open a code block", "closing line", "edit_file/multi_edit"} {
129 if !strings.Contains(err.Error(), want) {
130 t.Errorf("error should mention %q: %v", want, err)
131 }
132 }
133 got, _ := os.ReadFile(f)
134 if string(got) != body {
135 t.Errorf("file modified despite rejected range:\n%s", got)
136 }
137 }
138
139 func TestDeleteRangeAllowsCompleteBraceBlock(t *testing.T) {
140 f := filepath.Join(t.TempDir(), "map.html")
141 body := strings.Join([]string{
142 "function before() {",
143 " keep();",
144 "}",
145 "function removeMe() {",
146 " if (ok) {",
147 " redraw();",
148 " }",
149 "} // removeMe",
150 "function after() {",
151 " keep();",
152 "}",
153 "",
154 }, "\n")
155 os.WriteFile(f, []byte(body), 0o644)
156
157 runTool(t, deleteRange{}, map[string]any{
158 "path": f, "start_anchor": "function removeMe() {", "end_anchor": "} // removeMe",
159 })
160 got, _ := os.ReadFile(f)
161 want := strings.Join([]string{
162 "function before() {",
163 " keep();",
164 "}",
165 "function after() {",
166 " keep();",
167 "}",
168 "",
169 }, "\n")
170 if string(got) != want {
171 t.Errorf("file = %q, want %q", got, want)
172 }
173 }
174
175 func TestDeleteRangeRejectsClosingBraceWithoutOpening(t *testing.T) {
176 f := filepath.Join(t.TempDir(), "map.html")
177 body := strings.Join([]string{
178 "function keepHeader() {",
179 " removeBody();",
180 "} // keepHeader",
181 "function after() {",
182 " keep();",
183 "}",
184 "",
185 }, "\n")
186 os.WriteFile(f, []byte(body), 0o644)
187
188 args := argsJSON(t, map[string]any{
189 "path": f, "start_anchor": " removeBody();", "end_anchor": "} // keepHeader",
190 })
191 _, err := (deleteRange{}).Execute(context.Background(), args)
192 if err == nil {
193 t.Fatal("expected range cutting a block close to be rejected")
194 }
195 for _, want := range []string{"would cut a code block", "closing brace", "opening brace"} {
196 if !strings.Contains(err.Error(), want) {
197 t.Errorf("error should mention %q: %v", want, err)
198 }
199 }
200 got, _ := os.ReadFile(f)
201 if string(got) != body {
202 t.Errorf("file modified despite rejected range:\n%s", got)
203 }
204 }
205
206 func TestDeleteRangeAllowsPartialBraceDeletionInPlainText(t *testing.T) {
207 f := filepath.Join(t.TempDir(), "notes.md")
208 body := strings.Join([]string{
209 "intro",
210 "example {",
211 " remove this prose line",
212 "}",
213 "end",
214 "",
215 }, "\n")
216 os.WriteFile(f, []byte(body), 0o644)
217
218 runTool(t, deleteRange{}, map[string]any{
219 "path": f, "start_anchor": "example {", "end_anchor": " remove this prose line",
220 })
221 got, _ := os.ReadFile(f)
222 want := strings.Join([]string{
223 "intro",
224 "}",
225 "end",
226 "",
227 }, "\n")
228 if string(got) != want {
229 t.Errorf("file = %q, want %q", got, want)
230 }
231 }
232
233 func TestDeleteRangeDuplicateAnchorReportsLines(t *testing.T) {
234 f := filepath.Join(t.TempDir(), "dup-separator.js")
235 body := strings.Join([]string{
236 "// ═══════════════════════════════════════",
237 "const a = 1;",
238 "// ═══════════════════════════════════════",
239 "const b = 2;",
240 "// ═══════════════════════════════════════",
241 "",
242 }, "\n")
243 os.WriteFile(f, []byte(body), 0o644)
244
245 args := argsJSON(t, map[string]any{
246 "path": f, "start_anchor": "// ═══════════════════════════════════════", "end_anchor": "const b = 2;",
247 })
248 _, err := (deleteRange{}).Execute(context.Background(), args)
249 if err == nil {
250 t.Fatal("expected duplicate anchor error")
251 }
252 for _, want := range []string{"not unique", "matching lines include 1, 3, 5", "repeated separator lines"} {
253 if !strings.Contains(err.Error(), want) {
254 t.Errorf("error should mention %q: %v", want, err)
255 }
256 }
257 }
258
259 func TestDeleteRangeCRLF(t *testing.T) {
260 f := filepath.Join(t.TempDir(), "crlf.txt")
261 body := "line1\r\nline2\r\nline3\r\nline4\r\nline5\r\n"
262 os.WriteFile(f, []byte(body), 0o644)
263
264 runTool(t, deleteRange{}, map[string]any{
265 "path": f, "start_anchor": "line2", "end_anchor": "line4",
266 })
267 got, _ := os.ReadFile(f)
268 want := "line1\r\nline5\r\n"
269 if string(got) != want {
270 t.Errorf("CRLF file: got %q, want %q", got, want)
271 }
272 }
273
274 func TestDeleteRangeWholeNewlineTerminatedFile(t *testing.T) {
275 f := filepath.Join(t.TempDir(), "whole.txt")
276 os.WriteFile(f, []byte("line1\n"), 0o644)
277
278 runTool(t, deleteRange{}, map[string]any{
279 "path": f, "start_anchor": "line1", "end_anchor": "line1",
280 })
281 got, _ := os.ReadFile(f)
282 if string(got) != "" {
283 t.Errorf("whole-file delete left content %q, want empty", got)
284 }
285 }
286
287 func TestDeleteRangePreview(t *testing.T) {
288 f := filepath.Join(t.TempDir(), "preview.txt")
289 body := "line1\nline2\nline3\nline4\nline5\n"
290 os.WriteFile(f, []byte(body), 0o644)
291
292 change, err := deleteRange{}.Preview(argsJSON(t, map[string]any{
293 "path": f, "start_anchor": "line2", "end_anchor": "line4",
294 }))
295 if err != nil {
296 t.Fatalf("Preview: %v", err)
297 }
298
299 got, _ := os.ReadFile(f)
300 if string(got) != body {
301 t.Errorf("Preview mutated the file: %q", got)
302 }
303
304 if change.Kind != "modify" {
305 t.Errorf("kind = %q, want modify", change.Kind)
306 }
307 if change.OldText != body {
308 t.Errorf("OldText = %q, want %q", change.OldText, body)
309 }
310 }
311
311 lines GO