@@ -10,6 +10,7 @@ vi.mock("uuid", () => ({
10
10
11
11
vi . mock ( "core/util/ideUtils" , ( ) => ( {
12
12
resolveRelativePathInDir : vi . fn ( ) ,
13
+ inferResolvedUriFromRelativePath : vi . fn ( ) ,
13
14
} ) ) ;
14
15
15
16
vi . mock ( "../../redux/thunks/handleApplyStateUpdate" , ( ) => ( {
@@ -19,12 +20,16 @@ vi.mock("../../redux/thunks/handleApplyStateUpdate", () => ({
19
20
describe ( "multiEditImpl" , ( ) => {
20
21
let mockExtras : ClientToolExtras ;
21
22
let mockResolveRelativePathInDir : Mock ;
23
+ let mockInferResolvedUriFromRelativePath : Mock ;
22
24
let mockApplyForEditTool : Mock ;
23
25
24
26
beforeEach ( ( ) => {
25
27
vi . clearAllMocks ( ) ;
26
28
27
29
mockResolveRelativePathInDir = vi . mocked ( ideUtils . resolveRelativePathInDir ) ;
30
+ mockInferResolvedUriFromRelativePath = vi . mocked (
31
+ ideUtils . inferResolvedUriFromRelativePath ,
32
+ ) ;
28
33
mockApplyForEditTool = vi . mocked ( applyForEditTool ) ;
29
34
30
35
mockExtras = {
@@ -33,7 +38,10 @@ describe("multiEditImpl", () => {
33
38
} ) ) as any ,
34
39
dispatch : vi . fn ( ) as any ,
35
40
ideMessenger : {
36
- ide : { readFile : vi . fn ( ) } ,
41
+ ide : {
42
+ readFile : vi . fn ( ) ,
43
+ getWorkspaceDirs : vi . fn ( ) . mockResolvedValue ( [ "dir1" ] ) ,
44
+ } ,
37
45
request : vi . fn ( ) ,
38
46
} as any ,
39
47
} ;
@@ -64,7 +72,7 @@ describe("multiEditImpl", () => {
64
72
"id" ,
65
73
mockExtras ,
66
74
) ,
67
- ) . rejects . toThrow ( "Edit 1: old_string is required" ) ;
75
+ ) . rejects . toThrow ( "Edit # 1: old_string is required" ) ;
68
76
} ) ;
69
77
70
78
it ( "should throw if edit has undefined new_string" , async ( ) => {
@@ -77,7 +85,7 @@ describe("multiEditImpl", () => {
77
85
"id" ,
78
86
mockExtras ,
79
87
) ,
80
- ) . rejects . toThrow ( "Edit 1: new_string is required" ) ;
88
+ ) . rejects . toThrow ( "Edit # 1: new_string is required" ) ;
81
89
} ) ;
82
90
83
91
it ( "should throw if old_string equals new_string" , async ( ) => {
@@ -90,13 +98,15 @@ describe("multiEditImpl", () => {
90
98
"id" ,
91
99
mockExtras ,
92
100
) ,
93
- ) . rejects . toThrow ( "Edit 1: old_string and new_string must be different" ) ;
101
+ ) . rejects . toThrow ( "Edit # 1: old_string and new_string must be different" ) ;
94
102
} ) ;
95
103
} ) ;
96
104
97
105
describe ( "sequential edits" , ( ) => {
98
106
beforeEach ( ( ) => {
99
- mockResolveRelativePathInDir . mockResolvedValue ( "/test/file.txt" ) ;
107
+ mockResolveRelativePathInDir . mockResolvedValue (
108
+ "file:///dir/test/file.txt" ,
109
+ ) ;
100
110
} ) ;
101
111
102
112
it ( "should apply single edit" , async ( ) => {
@@ -117,7 +127,7 @@ describe("multiEditImpl", () => {
117
127
streamId : "test-uuid" ,
118
128
toolCallId : "id" ,
119
129
text : "Hi world" ,
120
- filepath : "/test/file.txt" ,
130
+ filepath : "file:///dir /test/file.txt" ,
121
131
isSearchAndReplace : true ,
122
132
} ) ;
123
133
} ) ;
@@ -143,7 +153,7 @@ describe("multiEditImpl", () => {
143
153
streamId : "test-uuid" ,
144
154
toolCallId : "id" ,
145
155
text : "Hi universe\nGoodbye universe" ,
146
- filepath : "/test/file.txt" ,
156
+ filepath : "file:///dir /test/file.txt" ,
147
157
isSearchAndReplace : true ,
148
158
} ) ;
149
159
} ) ;
@@ -169,7 +179,7 @@ describe("multiEditImpl", () => {
169
179
streamId : "test-uuid" ,
170
180
toolCallId : "id" ,
171
181
text : "let x = 2;" ,
172
- filepath : "/test/file.txt" ,
182
+ filepath : "file:///dir /test/file.txt" ,
173
183
isSearchAndReplace : true ,
174
184
} ) ;
175
185
} ) ;
@@ -191,7 +201,7 @@ describe("multiEditImpl", () => {
191
201
"id" ,
192
202
mockExtras ,
193
203
) ,
194
- ) . rejects . toThrow ( 'Edit 2: String not found in file: "not found"' ) ;
204
+ ) . rejects . toThrow ( 'Edit # 2: String not found in file: "not found"' ) ;
195
205
} ) ;
196
206
197
207
it ( "should throw if multiple occurrences without replace_all" , async ( ) => {
@@ -208,13 +218,18 @@ describe("multiEditImpl", () => {
208
218
"id" ,
209
219
mockExtras ,
210
220
) ,
211
- ) . rejects . toThrow ( 'Edit 1: String "test" appears 3 times' ) ;
221
+ ) . rejects . toThrow (
222
+ 'Edit #1: String "test" appears 3 times in the file. Either provide a more specific string with surrounding context to make it unique, or use replace_all=true to replace all occurrences.' ,
223
+ ) ;
212
224
} ) ;
213
225
} ) ;
214
226
215
227
describe ( "file creation" , ( ) => {
216
228
it ( "should create new file with empty old_string" , async ( ) => {
217
229
mockResolveRelativePathInDir . mockResolvedValue ( null ) ;
230
+ mockInferResolvedUriFromRelativePath . mockResolvedValue (
231
+ "file:///infered/new.txt" ,
232
+ ) ;
218
233
219
234
await multiEditImpl (
220
235
{
@@ -229,39 +244,17 @@ describe("multiEditImpl", () => {
229
244
streamId : "test-uuid" ,
230
245
toolCallId : "id" ,
231
246
text : "New content\nLine 2" ,
232
- filepath : "new.txt" ,
233
- isSearchAndReplace : true ,
234
- } ) ;
235
- } ) ;
236
-
237
- it ( "should create and edit new file" , async ( ) => {
238
- mockResolveRelativePathInDir . mockResolvedValue ( null ) ;
239
-
240
- await multiEditImpl (
241
- {
242
- filepath : "new.txt" ,
243
- edits : [
244
- { old_string : "" , new_string : "Hello world" } ,
245
- { old_string : "world" , new_string : "universe" } ,
246
- ] ,
247
- } ,
248
- "id" ,
249
- mockExtras ,
250
- ) ;
251
-
252
- expect ( mockApplyForEditTool ) . toHaveBeenCalledWith ( {
253
- streamId : "test-uuid" ,
254
- toolCallId : "id" ,
255
- text : "Hello universe" ,
256
- filepath : "new.txt" ,
247
+ filepath : "file:///infered/new.txt" ,
257
248
isSearchAndReplace : true ,
258
249
} ) ;
259
250
} ) ;
260
251
} ) ;
261
252
262
253
describe ( "replace_all functionality" , ( ) => {
263
254
beforeEach ( ( ) => {
264
- mockResolveRelativePathInDir . mockResolvedValue ( "/test/file.txt" ) ;
255
+ mockResolveRelativePathInDir . mockResolvedValue (
256
+ "file:///dir/test/file.txt" ,
257
+ ) ;
265
258
} ) ;
266
259
267
260
it ( "should replace all occurrences when specified" , async ( ) => {
@@ -282,7 +275,7 @@ describe("multiEditImpl", () => {
282
275
streamId : "test-uuid" ,
283
276
toolCallId : "id" ,
284
277
text : "qux bar qux baz qux" ,
285
- filepath : "/test/file.txt" ,
278
+ filepath : "file:///dir /test/file.txt" ,
286
279
isSearchAndReplace : true ,
287
280
} ) ;
288
281
} ) ;
@@ -308,15 +301,17 @@ describe("multiEditImpl", () => {
308
301
streamId : "test-uuid" ,
309
302
toolCallId : "id" ,
310
303
text : "a b a z a" ,
311
- filepath : "/test/file.txt" ,
304
+ filepath : "file:///dir /test/file.txt" ,
312
305
isSearchAndReplace : true ,
313
306
} ) ;
314
307
} ) ;
315
308
} ) ;
316
309
317
310
describe ( "error handling" , ( ) => {
318
311
it ( "should wrap readFile errors" , async ( ) => {
319
- mockResolveRelativePathInDir . mockResolvedValue ( "/test/file.txt" ) ;
312
+ mockResolveRelativePathInDir . mockResolvedValue (
313
+ "file:///dir/test/file.txt" ,
314
+ ) ;
320
315
mockExtras . ideMessenger . ide . readFile = vi
321
316
. fn ( )
322
317
. mockRejectedValue ( new Error ( "Read failed" ) ) ;
@@ -330,13 +325,15 @@ describe("multiEditImpl", () => {
330
325
"id" ,
331
326
mockExtras ,
332
327
) ,
333
- ) . rejects . toThrow ( "Failed to apply multi edit: Read failed" ) ;
328
+ ) . rejects . toThrow ( "Read failed" ) ;
334
329
} ) ;
335
330
} ) ;
336
331
337
332
describe ( "return value" , ( ) => {
338
333
it ( "should return correct structure" , async ( ) => {
339
- mockResolveRelativePathInDir . mockResolvedValue ( "/test/file.txt" ) ;
334
+ mockResolveRelativePathInDir . mockResolvedValue (
335
+ "file:///dir/test/file.txt" ,
336
+ ) ;
340
337
mockExtras . ideMessenger . ide . readFile = vi . fn ( ) . mockResolvedValue ( "test" ) ;
341
338
342
339
const result = await multiEditImpl (
0 commit comments