Skip to content

Commit 4b7ce1f

Browse files
authored
Avoid overaggressive unescaping (#20520)
1 parent ecfa4e0 commit 4b7ce1f

6 files changed

Lines changed: 110 additions & 1369 deletions

File tree

packages/core/src/tools/confirmation-policy.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ describe('Tool Confirmation Policy Updates', () => {
6767
getBaseLlmClient: () => ({}),
6868
getDisableLLMCorrection: () => true,
6969
getIdeMode: () => false,
70+
getActiveModel: () => 'test-model',
7071
getWorkspaceContext: () => ({
7172
isPathWithinWorkspace: () => true,
7273
getDirectories: () => [rootDir],

packages/core/src/tools/line-endings.test.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ import fs from 'node:fs';
2525
import os from 'node:os';
2626
import { GeminiClient } from '../core/client.js';
2727
import type { BaseLlmClient } from '../core/baseLlmClient.js';
28-
import {
29-
ensureCorrectEdit,
30-
ensureCorrectFileContent,
31-
} from '../utils/editCorrector.js';
28+
import { ensureCorrectFileContent } from '../utils/editCorrector.js';
3229
import { StandardFileSystemService } from '../services/fileSystemService.js';
3330
import { WorkspaceContext } from '../utils/workspaceContext.js';
3431
import {
@@ -52,7 +49,6 @@ vi.mock('../ide/ide-client.js', () => ({
5249

5350
let mockGeminiClientInstance: Mocked<GeminiClient>;
5451
let mockBaseLlmClientInstance: Mocked<BaseLlmClient>;
55-
const mockEnsureCorrectEdit = vi.fn<typeof ensureCorrectEdit>();
5652
const mockEnsureCorrectFileContent = vi.fn<typeof ensureCorrectFileContent>();
5753

5854
// Mock Config
@@ -81,6 +77,7 @@ const mockConfigInternal = {
8177
getGeminiMdFileCount: () => 0,
8278
setGeminiMdFileCount: vi.fn(),
8379
getDisableLLMCorrection: vi.fn(() => false),
80+
getActiveModel: () => 'test-model',
8481
validatePathAccess: vi.fn().mockReturnValue(null),
8582
getToolRegistry: () =>
8683
({
@@ -120,7 +117,6 @@ describe('Line Ending Preservation', () => {
120117
generateJson: vi.fn(),
121118
} as unknown as Mocked<BaseLlmClient>;
122119

123-
vi.mocked(ensureCorrectEdit).mockImplementation(mockEnsureCorrectEdit);
124120
vi.mocked(ensureCorrectFileContent).mockImplementation(
125121
mockEnsureCorrectFileContent,
126122
);
@@ -177,14 +173,7 @@ describe('Line Ending Preservation', () => {
177173
const proposedContent = 'line1\nline2\nline3\n';
178174

179175
// Mock corrections to return proposed content as-is (but usually normalized)
180-
mockEnsureCorrectEdit.mockResolvedValue({
181-
params: {
182-
file_path: filePath,
183-
old_string: originalContent,
184-
new_string: proposedContent,
185-
},
186-
occurrences: 1,
187-
});
176+
mockEnsureCorrectFileContent.mockResolvedValue(proposedContent);
188177

189178
const params = { file_path: filePath, content: proposedContent };
190179
const invocation = tool.build(params);

packages/core/src/tools/write-file.test.ts

Lines changed: 53 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import type {
2323
ToolResult,
2424
} from './tools.js';
2525
import { ToolConfirmationOutcome } from './tools.js';
26-
import { type EditToolParams } from './edit.js';
2726
import type { Config } from '../config/config.js';
2827
import { ApprovalMode } from '../policy/types.js';
2928
import type { ToolRegistry } from './tool-registry.js';
@@ -33,11 +32,7 @@ import fs from 'node:fs';
3332
import os from 'node:os';
3433
import { GeminiClient } from '../core/client.js';
3534
import type { BaseLlmClient } from '../core/baseLlmClient.js';
36-
import type { CorrectedEditResult } from '../utils/editCorrector.js';
37-
import {
38-
ensureCorrectEdit,
39-
ensureCorrectFileContent,
40-
} from '../utils/editCorrector.js';
35+
import { ensureCorrectFileContent } from '../utils/editCorrector.js';
4136
import { StandardFileSystemService } from '../services/fileSystemService.js';
4237
import type { DiffUpdateResult } from '../ide/ide-client.js';
4338
import { IdeClient } from '../ide/ide-client.js';
@@ -61,15 +56,13 @@ vi.mock('../ide/ide-client.js', () => ({
6156
let mockGeminiClientInstance: Mocked<GeminiClient>;
6257
let mockBaseLlmClientInstance: Mocked<BaseLlmClient>;
6358
let mockConfig: Config;
64-
const mockEnsureCorrectEdit = vi.fn<typeof ensureCorrectEdit>();
6559
const mockEnsureCorrectFileContent = vi.fn<typeof ensureCorrectFileContent>();
6660
const mockIdeClient = {
6761
openDiff: vi.fn(),
6862
isDiffingEnabled: vi.fn(),
6963
};
7064

7165
// Wire up the mocked functions to be used by the actual module imports
72-
vi.mocked(ensureCorrectEdit).mockImplementation(mockEnsureCorrectEdit);
7366
vi.mocked(ensureCorrectFileContent).mockImplementation(
7467
mockEnsureCorrectFileContent,
7568
);
@@ -110,6 +103,7 @@ const mockConfigInternal = {
110103
}) as unknown as ToolRegistry,
111104
isInteractive: () => false,
112105
getDisableLLMCorrection: vi.fn(() => true),
106+
getActiveModel: () => 'test-model',
113107
storage: {
114108
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
115109
},
@@ -179,7 +173,6 @@ describe('WriteFileTool', () => {
179173
generateJson: vi.fn(),
180174
} as unknown as Mocked<BaseLlmClient>;
181175

182-
vi.mocked(ensureCorrectEdit).mockImplementation(mockEnsureCorrectEdit);
183176
vi.mocked(ensureCorrectFileContent).mockImplementation(
184177
mockEnsureCorrectFileContent,
185178
);
@@ -199,28 +192,9 @@ describe('WriteFileTool', () => {
199192
// Reset mocks before each test
200193
mockConfigInternal.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
201194
mockConfigInternal.setApprovalMode.mockClear();
202-
mockEnsureCorrectEdit.mockReset();
203195
mockEnsureCorrectFileContent.mockReset();
204196

205197
// Default mock implementations that return valid structures
206-
mockEnsureCorrectEdit.mockImplementation(
207-
async (
208-
filePath: string,
209-
_currentContent: string,
210-
params: EditToolParams,
211-
_client: GeminiClient,
212-
_baseClient: BaseLlmClient,
213-
signal?: AbortSignal,
214-
): Promise<CorrectedEditResult> => {
215-
if (signal?.aborted) {
216-
return Promise.reject(new Error('Aborted'));
217-
}
218-
return Promise.resolve({
219-
params: { ...params, new_string: params.new_string ?? '' },
220-
occurrences: 1,
221-
});
222-
},
223-
);
224198
mockEnsureCorrectFileContent.mockImplementation(
225199
async (
226200
content: string,
@@ -369,15 +343,43 @@ describe('WriteFileTool', () => {
369343
mockBaseLlmClientInstance,
370344
abortSignal,
371345
true,
346+
true, // aggressiveUnescape
372347
);
373-
expect(mockEnsureCorrectEdit).not.toHaveBeenCalled();
374348
expect(result.correctedContent).toBe(correctedContent);
375349
expect(result.originalContent).toBe('');
376350
expect(result.fileExists).toBe(false);
377351
expect(result.error).toBeUndefined();
378352
});
379353

380-
it('should call ensureCorrectEdit for an existing file', async () => {
354+
it('should set aggressiveUnescape to false for gemini-3 models', async () => {
355+
const filePath = path.join(rootDir, 'gemini3_file.txt');
356+
const proposedContent = 'Proposed new content.';
357+
const abortSignal = new AbortController().signal;
358+
359+
const mockGemini3Config = {
360+
...mockConfig,
361+
getActiveModel: () => 'gemini-3.0-pro',
362+
} as unknown as Config;
363+
364+
mockEnsureCorrectFileContent.mockResolvedValue('Corrected new content.');
365+
366+
await getCorrectedFileContent(
367+
mockGemini3Config,
368+
filePath,
369+
proposedContent,
370+
abortSignal,
371+
);
372+
373+
expect(mockEnsureCorrectFileContent).toHaveBeenCalledWith(
374+
proposedContent,
375+
mockBaseLlmClientInstance,
376+
abortSignal,
377+
true,
378+
false, // aggressiveUnescape
379+
);
380+
});
381+
382+
it('should call ensureCorrectFileContent for an existing file', async () => {
381383
const filePath = path.join(rootDir, 'existing_corrected_file.txt');
382384
const originalContent = 'Original existing content.';
383385
const proposedContent = 'Proposed replacement content.';
@@ -386,14 +388,7 @@ describe('WriteFileTool', () => {
386388
fs.writeFileSync(filePath, originalContent, 'utf8');
387389

388390
// Ensure this mock is active and returns the correct structure
389-
mockEnsureCorrectEdit.mockResolvedValue({
390-
params: {
391-
file_path: filePath,
392-
old_string: originalContent,
393-
new_string: correctedProposedContent,
394-
},
395-
occurrences: 1,
396-
} as CorrectedEditResult);
391+
mockEnsureCorrectFileContent.mockResolvedValue(correctedProposedContent);
397392

398393
const result = await getCorrectedFileContent(
399394
mockConfig,
@@ -402,20 +397,13 @@ describe('WriteFileTool', () => {
402397
abortSignal,
403398
);
404399

405-
expect(mockEnsureCorrectEdit).toHaveBeenCalledWith(
406-
filePath,
407-
originalContent,
408-
{
409-
old_string: originalContent,
410-
new_string: proposedContent,
411-
file_path: filePath,
412-
},
413-
mockGeminiClientInstance,
400+
expect(mockEnsureCorrectFileContent).toHaveBeenCalledWith(
401+
proposedContent,
414402
mockBaseLlmClientInstance,
415403
abortSignal,
416404
true,
405+
true, // aggressiveUnescape
417406
);
418-
expect(mockEnsureCorrectFileContent).not.toHaveBeenCalled();
419407
expect(result.correctedContent).toBe(correctedProposedContent);
420408
expect(result.originalContent).toBe(originalContent);
421409
expect(result.fileExists).toBe(true);
@@ -441,7 +429,6 @@ describe('WriteFileTool', () => {
441429
);
442430

443431
expect(fsService.readTextFile).toHaveBeenCalledWith(filePath);
444-
expect(mockEnsureCorrectEdit).not.toHaveBeenCalled();
445432
expect(mockEnsureCorrectFileContent).not.toHaveBeenCalled();
446433
expect(result.correctedContent).toBe(proposedContent);
447434
expect(result.originalContent).toBe('');
@@ -492,6 +479,7 @@ describe('WriteFileTool', () => {
492479
mockBaseLlmClientInstance,
493480
abortSignal,
494481
true,
482+
true, // aggressiveUnescape
495483
);
496484
expect(confirmation).toEqual(
497485
expect.objectContaining({
@@ -516,33 +504,20 @@ describe('WriteFileTool', () => {
516504
'Corrected replacement for confirmation.';
517505
fs.writeFileSync(filePath, originalContent, 'utf8');
518506

519-
mockEnsureCorrectEdit.mockResolvedValue({
520-
params: {
521-
file_path: filePath,
522-
old_string: originalContent,
523-
new_string: correctedProposedContent,
524-
},
525-
occurrences: 1,
526-
});
507+
mockEnsureCorrectFileContent.mockResolvedValue(correctedProposedContent);
527508

528509
const params = { file_path: filePath, content: proposedContent };
529510
const invocation = tool.build(params);
530511
const confirmation = (await invocation.shouldConfirmExecute(
531512
abortSignal,
532513
)) as ToolEditConfirmationDetails;
533514

534-
expect(mockEnsureCorrectEdit).toHaveBeenCalledWith(
535-
filePath,
536-
originalContent,
537-
{
538-
old_string: originalContent,
539-
new_string: proposedContent,
540-
file_path: filePath,
541-
},
542-
mockGeminiClientInstance,
515+
expect(mockEnsureCorrectFileContent).toHaveBeenCalledWith(
516+
proposedContent,
543517
mockBaseLlmClientInstance,
544518
abortSignal,
545519
true,
520+
true, // aggressiveUnescape
546521
);
547522
expect(confirmation).toEqual(
548523
expect.objectContaining({
@@ -738,6 +713,7 @@ describe('WriteFileTool', () => {
738713
mockBaseLlmClientInstance,
739714
abortSignal,
740715
true,
716+
true, // aggressiveUnescape
741717
);
742718
expect(result.llmContent).toMatch(
743719
/Successfully created and wrote to new file/,
@@ -768,14 +744,7 @@ describe('WriteFileTool', () => {
768744
const correctedProposedContent = 'Corrected overwrite for execute.';
769745
fs.writeFileSync(filePath, initialContent, 'utf8');
770746

771-
mockEnsureCorrectEdit.mockResolvedValue({
772-
params: {
773-
file_path: filePath,
774-
old_string: initialContent,
775-
new_string: correctedProposedContent,
776-
},
777-
occurrences: 1,
778-
});
747+
mockEnsureCorrectFileContent.mockResolvedValue(correctedProposedContent);
779748

780749
const params = { file_path: filePath, content: proposedContent };
781750
const invocation = tool.build(params);
@@ -784,18 +753,12 @@ describe('WriteFileTool', () => {
784753

785754
const result = await invocation.execute(abortSignal);
786755

787-
expect(mockEnsureCorrectEdit).toHaveBeenCalledWith(
788-
filePath,
789-
initialContent,
790-
{
791-
old_string: initialContent,
792-
new_string: proposedContent,
793-
file_path: filePath,
794-
},
795-
mockGeminiClientInstance,
756+
expect(mockEnsureCorrectFileContent).toHaveBeenCalledWith(
757+
proposedContent,
796758
mockBaseLlmClientInstance,
797759
abortSignal,
798760
true,
761+
true, // aggressiveUnescape
799762
);
800763
expect(result.llmContent).toMatch(/Successfully overwrote file/);
801764
const writtenContent = await fsService.readTextFile(filePath);
@@ -892,14 +855,7 @@ describe('WriteFileTool', () => {
892855
newLines[50] = 'Line 51 Modified'; // Modify one line in the middle
893856

894857
const newContent = newLines.join('\n');
895-
mockEnsureCorrectEdit.mockResolvedValue({
896-
params: {
897-
file_path: filePath,
898-
old_string: originalContent,
899-
new_string: newContent,
900-
},
901-
occurrences: 1,
902-
});
858+
mockEnsureCorrectFileContent.mockResolvedValue(newContent);
903859

904860
const params = { file_path: filePath, content: newContent };
905861
const invocation = tool.build(params);
@@ -1072,28 +1028,21 @@ describe('WriteFileTool', () => {
10721028
mockBaseLlmClientInstance,
10731029
abortSignal,
10741030
true,
1031+
true, // aggressiveUnescape
10751032
);
1076-
expect(mockEnsureCorrectEdit).not.toHaveBeenCalled();
10771033
expect(result.correctedContent).toBe(proposedContent);
10781034
expect(result.fileExists).toBe(false);
10791035
});
10801036

1081-
it('should call ensureCorrectEdit with disableLLMCorrection=true for an existing file when disabled', async () => {
1037+
it('should call ensureCorrectFileContent with disableLLMCorrection=true for an existing file when disabled', async () => {
10821038
const filePath = path.join(rootDir, 'existing_file_no_correction.txt');
10831039
const originalContent = 'Original content.';
10841040
const proposedContent = 'Proposed content.';
10851041
fs.writeFileSync(filePath, originalContent, 'utf8');
10861042

10871043
mockConfigInternal.getDisableLLMCorrection.mockReturnValue(true);
10881044
// Ensure the mock returns the content passed to it
1089-
mockEnsureCorrectEdit.mockResolvedValue({
1090-
params: {
1091-
file_path: filePath,
1092-
old_string: originalContent,
1093-
new_string: proposedContent,
1094-
},
1095-
occurrences: 1,
1096-
});
1045+
mockEnsureCorrectFileContent.mockResolvedValue(proposedContent);
10971046

10981047
const result = await getCorrectedFileContent(
10991048
mockConfig,
@@ -1102,16 +1051,13 @@ describe('WriteFileTool', () => {
11021051
abortSignal,
11031052
);
11041053

1105-
expect(mockEnsureCorrectEdit).toHaveBeenCalledWith(
1106-
filePath,
1107-
originalContent,
1108-
expect.anything(), // params object
1109-
mockGeminiClientInstance,
1054+
expect(mockEnsureCorrectFileContent).toHaveBeenCalledWith(
1055+
proposedContent,
11101056
mockBaseLlmClientInstance,
11111057
abortSignal,
11121058
true,
1059+
true, // aggressiveUnescape
11131060
);
1114-
expect(mockEnsureCorrectFileContent).not.toHaveBeenCalled();
11151061
expect(result.correctedContent).toBe(proposedContent);
11161062
expect(result.originalContent).toBe(originalContent);
11171063
expect(result.fileExists).toBe(true);

0 commit comments

Comments
 (0)