Skip to content

Commit c3ad86f

Browse files
committed
fix: gui tests for edit tools
1 parent 4f18953 commit c3ad86f

File tree

4 files changed

+83
-106
lines changed

4 files changed

+83
-106
lines changed

gui/src/util/clientTools/multiEditImpl.test.ts

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ vi.mock("uuid", () => ({
1010

1111
vi.mock("core/util/ideUtils", () => ({
1212
resolveRelativePathInDir: vi.fn(),
13+
inferResolvedUriFromRelativePath: vi.fn(),
1314
}));
1415

1516
vi.mock("../../redux/thunks/handleApplyStateUpdate", () => ({
@@ -19,12 +20,16 @@ vi.mock("../../redux/thunks/handleApplyStateUpdate", () => ({
1920
describe("multiEditImpl", () => {
2021
let mockExtras: ClientToolExtras;
2122
let mockResolveRelativePathInDir: Mock;
23+
let mockInferResolvedUriFromRelativePath: Mock;
2224
let mockApplyForEditTool: Mock;
2325

2426
beforeEach(() => {
2527
vi.clearAllMocks();
2628

2729
mockResolveRelativePathInDir = vi.mocked(ideUtils.resolveRelativePathInDir);
30+
mockInferResolvedUriFromRelativePath = vi.mocked(
31+
ideUtils.inferResolvedUriFromRelativePath,
32+
);
2833
mockApplyForEditTool = vi.mocked(applyForEditTool);
2934

3035
mockExtras = {
@@ -33,7 +38,10 @@ describe("multiEditImpl", () => {
3338
})) as any,
3439
dispatch: vi.fn() as any,
3540
ideMessenger: {
36-
ide: { readFile: vi.fn() },
41+
ide: {
42+
readFile: vi.fn(),
43+
getWorkspaceDirs: vi.fn().mockResolvedValue(["dir1"]),
44+
},
3745
request: vi.fn(),
3846
} as any,
3947
};
@@ -64,7 +72,7 @@ describe("multiEditImpl", () => {
6472
"id",
6573
mockExtras,
6674
),
67-
).rejects.toThrow("Edit 1: old_string is required");
75+
).rejects.toThrow("Edit #1: old_string is required");
6876
});
6977

7078
it("should throw if edit has undefined new_string", async () => {
@@ -77,7 +85,7 @@ describe("multiEditImpl", () => {
7785
"id",
7886
mockExtras,
7987
),
80-
).rejects.toThrow("Edit 1: new_string is required");
88+
).rejects.toThrow("Edit #1: new_string is required");
8189
});
8290

8391
it("should throw if old_string equals new_string", async () => {
@@ -90,13 +98,15 @@ describe("multiEditImpl", () => {
9098
"id",
9199
mockExtras,
92100
),
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");
94102
});
95103
});
96104

97105
describe("sequential edits", () => {
98106
beforeEach(() => {
99-
mockResolveRelativePathInDir.mockResolvedValue("/test/file.txt");
107+
mockResolveRelativePathInDir.mockResolvedValue(
108+
"file:///dir/test/file.txt",
109+
);
100110
});
101111

102112
it("should apply single edit", async () => {
@@ -117,7 +127,7 @@ describe("multiEditImpl", () => {
117127
streamId: "test-uuid",
118128
toolCallId: "id",
119129
text: "Hi world",
120-
filepath: "/test/file.txt",
130+
filepath: "file:///dir/test/file.txt",
121131
isSearchAndReplace: true,
122132
});
123133
});
@@ -143,7 +153,7 @@ describe("multiEditImpl", () => {
143153
streamId: "test-uuid",
144154
toolCallId: "id",
145155
text: "Hi universe\nGoodbye universe",
146-
filepath: "/test/file.txt",
156+
filepath: "file:///dir/test/file.txt",
147157
isSearchAndReplace: true,
148158
});
149159
});
@@ -169,7 +179,7 @@ describe("multiEditImpl", () => {
169179
streamId: "test-uuid",
170180
toolCallId: "id",
171181
text: "let x = 2;",
172-
filepath: "/test/file.txt",
182+
filepath: "file:///dir/test/file.txt",
173183
isSearchAndReplace: true,
174184
});
175185
});
@@ -191,7 +201,7 @@ describe("multiEditImpl", () => {
191201
"id",
192202
mockExtras,
193203
),
194-
).rejects.toThrow('Edit 2: String not found in file: "not found"');
204+
).rejects.toThrow('Edit #2: String not found in file: "not found"');
195205
});
196206

197207
it("should throw if multiple occurrences without replace_all", async () => {
@@ -208,13 +218,18 @@ describe("multiEditImpl", () => {
208218
"id",
209219
mockExtras,
210220
),
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+
);
212224
});
213225
});
214226

215227
describe("file creation", () => {
216228
it("should create new file with empty old_string", async () => {
217229
mockResolveRelativePathInDir.mockResolvedValue(null);
230+
mockInferResolvedUriFromRelativePath.mockResolvedValue(
231+
"file:///infered/new.txt",
232+
);
218233

219234
await multiEditImpl(
220235
{
@@ -229,39 +244,17 @@ describe("multiEditImpl", () => {
229244
streamId: "test-uuid",
230245
toolCallId: "id",
231246
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",
257248
isSearchAndReplace: true,
258249
});
259250
});
260251
});
261252

262253
describe("replace_all functionality", () => {
263254
beforeEach(() => {
264-
mockResolveRelativePathInDir.mockResolvedValue("/test/file.txt");
255+
mockResolveRelativePathInDir.mockResolvedValue(
256+
"file:///dir/test/file.txt",
257+
);
265258
});
266259

267260
it("should replace all occurrences when specified", async () => {
@@ -282,7 +275,7 @@ describe("multiEditImpl", () => {
282275
streamId: "test-uuid",
283276
toolCallId: "id",
284277
text: "qux bar qux baz qux",
285-
filepath: "/test/file.txt",
278+
filepath: "file:///dir/test/file.txt",
286279
isSearchAndReplace: true,
287280
});
288281
});
@@ -308,15 +301,17 @@ describe("multiEditImpl", () => {
308301
streamId: "test-uuid",
309302
toolCallId: "id",
310303
text: "a b a z a",
311-
filepath: "/test/file.txt",
304+
filepath: "file:///dir/test/file.txt",
312305
isSearchAndReplace: true,
313306
});
314307
});
315308
});
316309

317310
describe("error handling", () => {
318311
it("should wrap readFile errors", async () => {
319-
mockResolveRelativePathInDir.mockResolvedValue("/test/file.txt");
312+
mockResolveRelativePathInDir.mockResolvedValue(
313+
"file:///dir/test/file.txt",
314+
);
320315
mockExtras.ideMessenger.ide.readFile = vi
321316
.fn()
322317
.mockRejectedValue(new Error("Read failed"));
@@ -330,13 +325,15 @@ describe("multiEditImpl", () => {
330325
"id",
331326
mockExtras,
332327
),
333-
).rejects.toThrow("Failed to apply multi edit: Read failed");
328+
).rejects.toThrow("Read failed");
334329
});
335330
});
336331

337332
describe("return value", () => {
338333
it("should return correct structure", async () => {
339-
mockResolveRelativePathInDir.mockResolvedValue("/test/file.txt");
334+
mockResolveRelativePathInDir.mockResolvedValue(
335+
"file:///dir/test/file.txt",
336+
);
340337
mockExtras.ideMessenger.ide.readFile = vi.fn().mockResolvedValue("test");
341338

342339
const result = await multiEditImpl(

gui/src/util/clientTools/multiEditImpl.ts

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,11 @@ export const multiEditImpl: ClientToolImpl = async (
5252
);
5353
}
5454
newContent = edits[0].new_string;
55-
const response = await extras.ideMessenger.request(
56-
"getWorkspaceDirs",
57-
undefined,
58-
);
59-
if (response.status === "error") {
60-
throw new Error(
61-
"Error getting workspace directories to infer file creation path",
62-
);
63-
}
55+
const dirs = await extras.ideMessenger.ide.getWorkspaceDirs();
6456
fileUri = await inferResolvedUriFromRelativePath(
6557
filepath,
6658
extras.ideMessenger.ide,
67-
response.content,
59+
dirs,
6860
);
6961
} else {
7062
if (!resolvedUri) {
@@ -86,26 +78,20 @@ export const multiEditImpl: ClientToolImpl = async (
8678
}
8779
}
8880

89-
try {
90-
// Apply the changes to the file
91-
void extras.dispatch(
92-
applyForEditTool({
93-
streamId,
94-
toolCallId,
95-
text: newContent,
96-
filepath: fileUri,
97-
isSearchAndReplace: true,
98-
}),
99-
);
81+
// Apply the changes to the file
82+
void extras.dispatch(
83+
applyForEditTool({
84+
streamId,
85+
toolCallId,
86+
text: newContent,
87+
filepath: fileUri,
88+
isSearchAndReplace: true,
89+
}),
90+
);
10091

101-
// Return success - applyToFile will handle the completion state
102-
return {
103-
respondImmediately: false, // Let apply state handle completion
104-
output: undefined,
105-
};
106-
} catch (error) {
107-
throw new Error(
108-
`Failed to apply multi edit: ${error instanceof Error ? error.message : String(error)}`,
109-
);
110-
}
92+
// Return success - applyToFile will handle the completion state
93+
return {
94+
respondImmediately: false, // Let apply state handle completion
95+
output: undefined,
96+
};
11197
};

gui/src/util/clientTools/singleFindAndReplaceImpl.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ describe("singleFindAndReplaceImpl", () => {
148148

149149
await expect(
150150
singleFindAndReplaceImpl(args, "tool-call-id", mockExtras),
151-
).rejects.toThrow("String not found in file: not found");
151+
).rejects.toThrow(`String not found in file: "not found"`);
152152
});
153153

154154
it("should replace single occurrence", async () => {
@@ -328,7 +328,7 @@ describe("singleFindAndReplaceImpl", () => {
328328

329329
await expect(
330330
singleFindAndReplaceImpl(args, "tool-call-id", mockExtras),
331-
).rejects.toThrow("Failed to apply find and replace: Permission denied");
331+
).rejects.toThrow("Permission denied");
332332
});
333333
});
334334
});

gui/src/util/clientTools/singleFindAndReplaceImpl.ts

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,38 +31,32 @@ export const singleFindAndReplaceImpl: ClientToolImpl = async (
3131
throw new Error(`File ${filepath} does not exist`);
3232
}
3333

34-
try {
35-
// Read the current file content
36-
const originalContent =
37-
await extras.ideMessenger.ide.readFile(resolvedFilepath);
38-
39-
// Perform the find and replace operation
40-
const newContent = performFindAndReplace(
41-
originalContent,
42-
old_string,
43-
new_string,
44-
replace_all,
45-
);
34+
// Read the current file content
35+
const originalContent =
36+
await extras.ideMessenger.ide.readFile(resolvedFilepath);
37+
38+
// Perform the find and replace operation
39+
const newContent = performFindAndReplace(
40+
originalContent,
41+
old_string,
42+
new_string,
43+
replace_all,
44+
);
4645

47-
// Apply the changes to the file
48-
void extras.dispatch(
49-
applyForEditTool({
50-
streamId,
51-
toolCallId,
52-
text: newContent,
53-
filepath: resolvedFilepath,
54-
isSearchAndReplace: true,
55-
}),
56-
);
46+
// Apply the changes to the file
47+
void extras.dispatch(
48+
applyForEditTool({
49+
streamId,
50+
toolCallId,
51+
text: newContent,
52+
filepath: resolvedFilepath,
53+
isSearchAndReplace: true,
54+
}),
55+
);
5756

58-
// Return success - applyToFile will handle the completion state
59-
return {
60-
respondImmediately: false, // Let apply state handle completion
61-
output: undefined,
62-
};
63-
} catch (error) {
64-
throw new Error(
65-
`Failed to apply find and replace: ${error instanceof Error ? error.message : String(error)}`,
66-
);
67-
}
57+
// Return success - applyToFile will handle the completion state
58+
return {
59+
respondImmediately: false, // Let apply state handle completion
60+
output: undefined,
61+
};
6862
};

0 commit comments

Comments
 (0)