返回 DeepSeek-Reasonix
fuzzy_edit_test.go
根目录 / internal / tool / builtin / fuzzy_edit_test.go
1 package builtin
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 )
10
11 func TestEditFileFuzzyTrailingWhitespace(t *testing.T) {
12 dir := t.TempDir()
13 path := filepath.Join(dir, "main.go")
14 seed := "func main() { \n\tfmt.Println(\"hello\") \n}\n"
15 if err := os.WriteFile(path, []byte(seed), 0o644); err != nil {
16 t.Fatal(err)
17 }
18
19 out, err := (editFile{}).Execute(context.Background(), argsJSON(t, map[string]any{
20 "path": path,
21 "old_string": "func main() {\n\tfmt.Println(\"hello\")\n}",
22 "new_string": "func main() {\n\tfmt.Println(\"bye\")\n}",
23 }))
24 if err != nil {
25 t.Fatalf("edit_file: %v", err)
26 }
27 if !strings.Contains(out, "fuzzy match") {
28 t.Fatalf("output should disclose fuzzy matching, got %q", out)
29 }
30 got, err := os.ReadFile(path)
31 if err != nil {
32 t.Fatal(err)
33 }
34 want := "func main() {\n\tfmt.Println(\"bye\")\n}\n"
35 if string(got) != want {
36 t.Fatalf("content = %q, want %q", got, want)
37 }
38 }
39
40 func TestEditFileFuzzyReadFileLinePrefixes(t *testing.T) {
41 dir := t.TempDir()
42 path := filepath.Join(dir, "notes.txt")
43 seed := "alpha\nbeta\ngamma\n"
44 if err := os.WriteFile(path, []byte(seed), 0o644); err != nil {
45 t.Fatal(err)
46 }
47
48 _, err := (editFile{}).Execute(context.Background(), argsJSON(t, map[string]any{
49 "path": path,
50 "old_string": "1\u2192alpha\n2\u2192beta",
51 "new_string": "ALPHA\nBETA",
52 }))
53 if err != nil {
54 t.Fatalf("edit_file: %v", err)
55 }
56 got, err := os.ReadFile(path)
57 if err != nil {
58 t.Fatal(err)
59 }
60 want := "ALPHA\nBETA\ngamma\n"
61 if string(got) != want {
62 t.Fatalf("content = %q, want %q", got, want)
63 }
64 }
65
66 func TestEditFileFuzzyCRLFPreservesLineEndings(t *testing.T) {
67 dir := t.TempDir()
68 path := filepath.Join(dir, "win.txt")
69 seed := "one \r\ntwo \r\nthree\r\n"
70 if err := os.WriteFile(path, []byte(seed), 0o644); err != nil {
71 t.Fatal(err)
72 }
73
74 _, err := (editFile{}).Execute(context.Background(), argsJSON(t, map[string]any{
75 "path": path,
76 "old_string": "one\ntwo",
77 "new_string": "ONE\nTWO",
78 }))
79 if err != nil {
80 t.Fatalf("edit_file: %v", err)
81 }
82 got, err := os.ReadFile(path)
83 if err != nil {
84 t.Fatal(err)
85 }
86 want := "ONE\r\nTWO\r\nthree\r\n"
87 if string(got) != want {
88 t.Fatalf("content = %q, want %q", got, want)
89 }
90 }
91
92 func TestEditFileCRLFNotFoundHintAvoidsMisattribution(t *testing.T) {
93 dir := t.TempDir()
94 path := filepath.Join(dir, "win.txt")
95 seed := "one\r\ntwo\r\nthree\r\n"
96 if err := os.WriteFile(path, []byte(seed), 0o644); err != nil {
97 t.Fatal(err)
98 }
99
100 _, err := (editFile{}).Execute(context.Background(), argsJSON(t, map[string]any{
101 "path": path,
102 "old_string": "one\nmissing",
103 "new_string": "ONE\nMISSING",
104 }))
105 if err == nil {
106 t.Fatal("expected old_string not found")
107 }
108 msg := err.Error()
109 for _, want := range []string{
110 "old_string not found",
111 "CRLF line endings",
112 "already tolerate LF-only old_string",
113 "stale, incomplete, or non-unique context",
114 } {
115 if !strings.Contains(msg, want) {
116 t.Fatalf("error %q does not contain %q", msg, want)
117 }
118 }
119 }
120
121 func TestEditFileFuzzyAmbiguousDoesNotWrite(t *testing.T) {
122 dir := t.TempDir()
123 path := filepath.Join(dir, "dup.txt")
124 seed := "target \ntarget \n"
125 if err := os.WriteFile(path, []byte(seed), 0o644); err != nil {
126 t.Fatal(err)
127 }
128
129 _, err := (editFile{}).Execute(context.Background(), argsJSON(t, map[string]any{
130 "path": path,
131 "old_string": "target\n",
132 "new_string": "updated\n",
133 }))
134 if err == nil || !strings.Contains(err.Error(), "not unique") {
135 t.Fatalf("expected not-unique error, got %v", err)
136 }
137 got, err := os.ReadFile(path)
138 if err != nil {
139 t.Fatal(err)
140 }
141 if string(got) != seed {
142 t.Fatalf("ambiguous fuzzy edit changed file: %q", got)
143 }
144 }
145
146 func TestEditFileFuzzyLeadingIndentDriftDoesNotWrite(t *testing.T) {
147 dir := t.TempDir()
148 path := filepath.Join(dir, "indent.go")
149 seed := "func f() {\n if ok {\n return nil\n }\n}\n"
150 if err := os.WriteFile(path, []byte(seed), 0o644); err != nil {
151 t.Fatal(err)
152 }
153
154 _, err := (editFile{}).Execute(context.Background(), argsJSON(t, map[string]any{
155 "path": path,
156 "old_string": "if ok {\n return nil\n}",
157 "new_string": "if ok {\n return errors.New(\"nope\")\n}",
158 }))
159 if err == nil || !strings.Contains(err.Error(), "old_string not found") {
160 t.Fatalf("expected not-found error for leading indentation drift, got %v", err)
161 }
162 got, err := os.ReadFile(path)
163 if err != nil {
164 t.Fatal(err)
165 }
166 if string(got) != seed {
167 t.Fatalf("leading indentation drift changed file: %q", got)
168 }
169 }
170
171 func TestMultiEditFuzzyReplaceAll(t *testing.T) {
172 dir := t.TempDir()
173 path := filepath.Join(dir, "list.txt")
174 seed := "item \nitem\t\n"
175 if err := os.WriteFile(path, []byte(seed), 0o644); err != nil {
176 t.Fatal(err)
177 }
178
179 out, err := (multiEdit{}).Execute(context.Background(), argsJSON(t, map[string]any{
180 "path": path,
181 "edits": []map[string]any{
182 {"old_string": "item\n", "new_string": "thing\n", "replace_all": true},
183 },
184 }))
185 if err != nil {
186 t.Fatalf("multi_edit: %v", err)
187 }
188 if !strings.Contains(out, "2 total replacements") || !strings.Contains(out, "fuzzy match") {
189 t.Fatalf("unexpected output: %q", out)
190 }
191 got, err := os.ReadFile(path)
192 if err != nil {
193 t.Fatal(err)
194 }
195 want := "thing\nthing\n"
196 if string(got) != want {
197 t.Fatalf("content = %q, want %q", got, want)
198 }
199 }
200
200 lines GO