Skip to content

Commit 673854b

Browse files
authored
fix: Remove unreliable editCorrector that injects extra escape characters (QwenLM#713)
1 parent 4e7a7e2 commit 673854b

File tree

6 files changed

+56
-1935
lines changed

6 files changed

+56
-1935
lines changed

packages/core/src/tools/edit.test.ts

Lines changed: 8 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,10 @@
66

77
/* eslint-disable @typescript-eslint/no-explicit-any */
88

9-
const mockEnsureCorrectEdit = vi.hoisted(() => vi.fn());
10-
const mockGenerateJson = vi.hoisted(() => vi.fn());
119
const mockOpenDiff = vi.hoisted(() => vi.fn());
1210

1311
import { IDEConnectionStatus } from '../ide/ide-client.js';
1412

15-
vi.mock('../utils/editCorrector.js', () => ({
16-
ensureCorrectEdit: mockEnsureCorrectEdit,
17-
}));
18-
19-
vi.mock('../core/client.js', () => ({
20-
GeminiClient: vi.fn().mockImplementation(() => ({
21-
generateJson: mockGenerateJson,
22-
})),
23-
}));
24-
2513
vi.mock('../utils/editor.js', () => ({
2614
openDiff: mockOpenDiff,
2715
}));
@@ -42,7 +30,6 @@ import fs from 'node:fs';
4230
import os from 'node:os';
4331
import type { Config } from '../config/config.js';
4432
import { ApprovalMode } from '../config/config.js';
45-
import type { Content, Part, SchemaUnion } from '@google/genai';
4633
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
4734
import { StandardFileSystemService } from '../services/fileSystemService.js';
4835

@@ -51,30 +38,20 @@ describe('EditTool', () => {
5138
let tempDir: string;
5239
let rootDir: string;
5340
let mockConfig: Config;
54-
let geminiClient: any;
55-
5641
beforeEach(() => {
5742
vi.restoreAllMocks();
5843
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'edit-tool-test-'));
5944
rootDir = path.join(tempDir, 'root');
6045
fs.mkdirSync(rootDir);
6146

62-
geminiClient = {
63-
generateJson: mockGenerateJson, // mockGenerateJson is already defined and hoisted
64-
};
65-
6647
mockConfig = {
67-
getGeminiClient: vi.fn().mockReturnValue(geminiClient),
6848
getTargetDir: () => rootDir,
6949
getApprovalMode: vi.fn(),
7050
setApprovalMode: vi.fn(),
7151
getWorkspaceContext: () => createMockWorkspaceContext(rootDir),
7252
getFileSystemService: () => new StandardFileSystemService(),
7353
getIdeClient: () => undefined,
7454
getIdeMode: () => false,
75-
// getGeminiConfig: () => ({ apiKey: 'test-api-key' }), // This was not a real Config method
76-
// Add other properties/methods of Config if EditTool uses them
77-
// Minimal other methods to satisfy Config type if needed by EditTool constructor or other direct uses:
7855
getApiKey: () => 'test-api-key',
7956
getModel: () => 'test-model',
8057
getSandbox: () => false,
@@ -98,65 +75,6 @@ describe('EditTool', () => {
9875
// Default to not skipping confirmation
9976
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.DEFAULT);
10077

101-
// Reset mocks and set default implementation for ensureCorrectEdit
102-
mockEnsureCorrectEdit.mockReset();
103-
mockEnsureCorrectEdit.mockImplementation(
104-
async (_, currentContent, params) => {
105-
let occurrences = 0;
106-
if (params.old_string && currentContent) {
107-
// Simple string counting for the mock
108-
let index = currentContent.indexOf(params.old_string);
109-
while (index !== -1) {
110-
occurrences++;
111-
index = currentContent.indexOf(params.old_string, index + 1);
112-
}
113-
} else if (params.old_string === '') {
114-
occurrences = 0; // Creating a new file
115-
}
116-
return Promise.resolve({ params, occurrences });
117-
},
118-
);
119-
120-
// Default mock for generateJson to return the snippet unchanged
121-
mockGenerateJson.mockReset();
122-
mockGenerateJson.mockImplementation(
123-
async (contents: Content[], schema: SchemaUnion) => {
124-
// The problematic_snippet is the last part of the user's content
125-
const userContent = contents.find((c: Content) => c.role === 'user');
126-
let promptText = '';
127-
if (userContent && userContent.parts) {
128-
promptText = userContent.parts
129-
.filter((p: Part) => typeof (p as any).text === 'string')
130-
.map((p: Part) => (p as any).text)
131-
.join('\n');
132-
}
133-
const snippetMatch = promptText.match(
134-
/Problematic target snippet:\n```\n([\s\S]*?)\n```/,
135-
);
136-
const problematicSnippet =
137-
snippetMatch && snippetMatch[1] ? snippetMatch[1] : '';
138-
139-
if (((schema as any).properties as any)?.corrected_target_snippet) {
140-
return Promise.resolve({
141-
corrected_target_snippet: problematicSnippet,
142-
});
143-
}
144-
if (((schema as any).properties as any)?.corrected_new_string) {
145-
// For new_string correction, we might need more sophisticated logic,
146-
// but for now, returning original is a safe default if not specified by a test.
147-
const originalNewStringMatch = promptText.match(
148-
/original_new_string \(what was intended to replace original_old_string\):\n```\n([\s\S]*?)\n```/,
149-
);
150-
const originalNewString =
151-
originalNewStringMatch && originalNewStringMatch[1]
152-
? originalNewStringMatch[1]
153-
: '';
154-
return Promise.resolve({ corrected_new_string: originalNewString });
155-
}
156-
return Promise.resolve({}); // Default empty object if schema doesn't match
157-
},
158-
);
159-
16078
tool = new EditTool(mockConfig);
16179
});
16280

@@ -249,8 +167,6 @@ describe('EditTool', () => {
249167
old_string: 'old',
250168
new_string: 'new',
251169
};
252-
// ensureCorrectEdit will be called by shouldConfirmExecute
253-
mockEnsureCorrectEdit.mockResolvedValueOnce({ params, occurrences: 1 });
254170
const invocation = tool.build(params);
255171
const confirmation = await invocation.shouldConfirmExecute(
256172
new AbortController().signal,
@@ -264,29 +180,27 @@ describe('EditTool', () => {
264180
);
265181
});
266182

267-
it('should return false if old_string is not found (ensureCorrectEdit returns 0)', async () => {
183+
it('should return false if old_string is not found', async () => {
268184
fs.writeFileSync(filePath, 'some content here');
269185
const params: EditToolParams = {
270186
file_path: filePath,
271187
old_string: 'not_found',
272188
new_string: 'new',
273189
};
274-
mockEnsureCorrectEdit.mockResolvedValueOnce({ params, occurrences: 0 });
275190
const invocation = tool.build(params);
276191
const confirmation = await invocation.shouldConfirmExecute(
277192
new AbortController().signal,
278193
);
279194
expect(confirmation).toBe(false);
280195
});
281196

282-
it('should return false if multiple occurrences of old_string are found (ensureCorrectEdit returns > 1)', async () => {
197+
it('should return false if multiple occurrences of old_string are found', async () => {
283198
fs.writeFileSync(filePath, 'old old content here');
284199
const params: EditToolParams = {
285200
file_path: filePath,
286201
old_string: 'old',
287202
new_string: 'new',
288203
};
289-
mockEnsureCorrectEdit.mockResolvedValueOnce({ params, occurrences: 2 });
290204
const invocation = tool.build(params);
291205
const confirmation = await invocation.shouldConfirmExecute(
292206
new AbortController().signal,
@@ -302,10 +216,6 @@ describe('EditTool', () => {
302216
old_string: '',
303217
new_string: 'new file content',
304218
};
305-
// ensureCorrectEdit might not be called if old_string is empty,
306-
// as shouldConfirmExecute handles this for diff generation.
307-
// If it is called, it should return 0 occurrences for a new file.
308-
mockEnsureCorrectEdit.mockResolvedValueOnce({ params, occurrences: 0 });
309219
const invocation = tool.build(params);
310220
const confirmation = await invocation.shouldConfirmExecute(
311221
new AbortController().signal,
@@ -319,65 +229,9 @@ describe('EditTool', () => {
319229
);
320230
});
321231

322-
it('should use corrected params from ensureCorrectEdit for diff generation', async () => {
323-
const originalContent = 'This is the original string to be replaced.';
324-
const originalOldString = 'original string';
325-
const originalNewString = 'new string';
326-
327-
const correctedOldString = 'original string to be replaced'; // More specific
328-
const correctedNewString = 'completely new string'; // Different replacement
329-
const expectedFinalContent = 'This is the completely new string.';
330-
331-
fs.writeFileSync(filePath, originalContent);
332-
const params: EditToolParams = {
333-
file_path: filePath,
334-
old_string: originalOldString,
335-
new_string: originalNewString,
336-
};
337-
338-
// The main beforeEach already calls mockEnsureCorrectEdit.mockReset()
339-
// Set a specific mock for this test case
340-
let mockCalled = false;
341-
mockEnsureCorrectEdit.mockImplementationOnce(
342-
async (_, content, p, client) => {
343-
mockCalled = true;
344-
expect(content).toBe(originalContent);
345-
expect(p).toBe(params);
346-
expect(client).toBe(geminiClient);
347-
return {
348-
params: {
349-
file_path: filePath,
350-
old_string: correctedOldString,
351-
new_string: correctedNewString,
352-
},
353-
occurrences: 1,
354-
};
355-
},
356-
);
357-
const invocation = tool.build(params);
358-
const confirmation = (await invocation.shouldConfirmExecute(
359-
new AbortController().signal,
360-
)) as FileDiff;
361-
362-
expect(mockCalled).toBe(true); // Check if the mock implementation was run
363-
// expect(mockEnsureCorrectEdit).toHaveBeenCalledWith(originalContent, params, expect.anything()); // Keep this commented for now
364-
expect(confirmation).toEqual(
365-
expect.objectContaining({
366-
title: `Confirm Edit: ${testFile}`,
367-
fileName: testFile,
368-
}),
369-
);
370-
// Check that the diff is based on the corrected strings leading to the new state
371-
expect(confirmation.fileDiff).toContain(`-${originalContent}`);
372-
expect(confirmation.fileDiff).toContain(`+${expectedFinalContent}`);
373-
374-
// Verify that applying the correctedOldString and correctedNewString to originalContent
375-
// indeed produces the expectedFinalContent, which is what the diff should reflect.
376-
const patchedContent = originalContent.replace(
377-
correctedOldString, // This was the string identified by ensureCorrectEdit for replacement
378-
correctedNewString, // This was the string identified by ensureCorrectEdit as the replacement
379-
);
380-
expect(patchedContent).toBe(expectedFinalContent);
232+
// This test is no longer relevant since editCorrector functionality was removed
233+
it.skip('should use corrected params from ensureCorrectEdit for diff generation', async () => {
234+
// Test skipped - editCorrector functionality removed
381235
});
382236
});
383237

@@ -387,20 +241,6 @@ describe('EditTool', () => {
387241

388242
beforeEach(() => {
389243
filePath = path.join(rootDir, testFile);
390-
// Default for execute tests, can be overridden
391-
mockEnsureCorrectEdit.mockImplementation(async (_, content, params) => {
392-
let occurrences = 0;
393-
if (params.old_string && content) {
394-
let index = content.indexOf(params.old_string);
395-
while (index !== -1) {
396-
occurrences++;
397-
index = content.indexOf(params.old_string, index + 1);
398-
}
399-
} else if (params.old_string === '') {
400-
occurrences = 0;
401-
}
402-
return { params, occurrences };
403-
});
404244
});
405245

406246
it('should throw error if file path is not absolute', async () => {
@@ -433,10 +273,6 @@ describe('EditTool', () => {
433273
new_string: 'new',
434274
};
435275

436-
// Specific mock for this test's execution path in calculateEdit
437-
// ensureCorrectEdit is NOT called by calculateEdit, only by shouldConfirmExecute
438-
// So, the default mockEnsureCorrectEdit should correctly return 1 occurrence for 'old' in initialContent
439-
440276
const invocation = tool.build(params);
441277
const result = await invocation.execute(new AbortController().signal);
442278

@@ -477,7 +313,6 @@ describe('EditTool', () => {
477313
old_string: 'nonexistent',
478314
new_string: 'replacement',
479315
};
480-
// The default mockEnsureCorrectEdit will return 0 occurrences for 'nonexistent'
481316
const invocation = tool.build(params);
482317
const result = await invocation.execute(new AbortController().signal);
483318
expect(result.llmContent).toMatch(
@@ -495,7 +330,6 @@ describe('EditTool', () => {
495330
old_string: 'old',
496331
new_string: 'new',
497332
};
498-
// The default mockEnsureCorrectEdit will return 2 occurrences for 'old'
499333
const invocation = tool.build(params);
500334
const result = await invocation.execute(new AbortController().signal);
501335
expect(result.llmContent).toMatch(
@@ -638,8 +472,7 @@ describe('EditTool', () => {
638472
});
639473

640474
it('should return EDIT_NO_CHANGE error if replacement results in identical content', async () => {
641-
// This can happen if ensureCorrectEdit finds a fuzzy match, but the literal
642-
// string replacement with `replaceAll` results in no change.
475+
// This can happen if the literal string replacement with `replaceAll` results in no change.
643476
const initialContent = 'line 1\nline 2\nline 3'; // Note the double space
644477
fs.writeFileSync(filePath, initialContent, 'utf8');
645478
const params: EditToolParams = {
@@ -649,16 +482,12 @@ describe('EditTool', () => {
649482
new_string: 'line 1\nnew line 2\nline 3',
650483
};
651484

652-
// Mock ensureCorrectEdit to simulate it finding a match (e.g., via fuzzy matching)
653-
// but it doesn't correct the old_string to the literal content.
654-
mockEnsureCorrectEdit.mockResolvedValueOnce({ params, occurrences: 1 });
655-
656485
const invocation = tool.build(params);
657486
const result = await invocation.execute(new AbortController().signal);
658487

659-
expect(result.error?.type).toBe(ToolErrorType.EDIT_NO_CHANGE);
488+
expect(result.error?.type).toBe(ToolErrorType.EDIT_NO_OCCURRENCE_FOUND);
660489
expect(result.returnDisplay).toMatch(
661-
/No changes to apply. The new content is identical to the current content./,
490+
/Failed to edit, could not find the string to replace./,
662491
);
663492
// Ensure the file was not actually changed
664493
expect(fs.readFileSync(filePath, 'utf8')).toBe(initialContent);
@@ -870,10 +699,6 @@ describe('EditTool', () => {
870699
old_string: 'old',
871700
new_string: 'new',
872701
};
873-
mockEnsureCorrectEdit.mockResolvedValueOnce({
874-
params: { ...params, old_string: 'old', new_string: 'new' },
875-
occurrences: 1,
876-
});
877702
ideClient.openDiff.mockResolvedValueOnce({
878703
status: 'accepted',
879704
content: modifiedContent,

0 commit comments

Comments
 (0)