feat(core): add disableLLMCorrection setting to skip auto-correction in edit tools (#16000)

This commit is contained in:
Sandy Tao
2026-01-13 09:26:53 +08:00
committed by GitHub
parent 2fc61685a3
commit b81fe68325
13 changed files with 221 additions and 12 deletions
@@ -64,6 +64,7 @@ describe('Tool Confirmation Policy Updates', () => {
getFileFilteringOptions: () => ({}),
getGeminiClient: () => ({}),
getBaseLlmClient: () => ({}),
getDisableLLMCorrection: () => false,
getIdeMode: () => false,
getWorkspaceContext: () => ({
isPathWithinWorkspace: () => true,
+44
View File
@@ -120,6 +120,7 @@ describe('EditTool', () => {
setGeminiMdFileCount: vi.fn(),
getToolRegistry: () => ({}) as any,
isInteractive: () => false,
getDisableLLMCorrection: vi.fn(() => false),
getExperiments: () => {},
} as unknown as Config;
@@ -858,4 +859,47 @@ describe('EditTool', () => {
expect(totalActualRemoved).toBe(totalExpectedRemoved);
});
});
describe('disableLLMCorrection', () => {
it('should NOT call FixLLMEditWithInstruction when disableLLMCorrection is true', async () => {
const filePath = path.join(rootDir, 'disable_llm_test.txt');
fs.writeFileSync(filePath, 'Some content.', 'utf8');
// Enable the setting
(mockConfig.getDisableLLMCorrection as Mock).mockReturnValue(true);
const params: EditToolParams = {
file_path: filePath,
instruction: 'Replace non-existent text',
old_string: 'nonexistent',
new_string: 'replacement',
};
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.error?.type).toBe(ToolErrorType.EDIT_NO_OCCURRENCE_FOUND);
expect(mockFixLLMEditWithInstruction).not.toHaveBeenCalled();
});
it('should call FixLLMEditWithInstruction when disableLLMCorrection is false (default)', async () => {
const filePath = path.join(rootDir, 'enable_llm_test.txt');
fs.writeFileSync(filePath, 'Some content.', 'utf8');
// Default is false, but being explicit
(mockConfig.getDisableLLMCorrection as Mock).mockReturnValue(false);
const params: EditToolParams = {
file_path: filePath,
instruction: 'Replace non-existent text',
old_string: 'nonexistent',
new_string: 'replacement',
};
const invocation = tool.build(params);
await invocation.execute(new AbortController().signal);
expect(mockFixLLMEditWithInstruction).toHaveBeenCalled();
});
});
});
+11
View File
@@ -639,6 +639,17 @@ class EditToolInvocation
};
}
if (this.config.getDisableLLMCorrection()) {
return {
currentContent,
newContent: currentContent,
occurrences: replacementResult.occurrences,
isNewFile: false,
error: initialError,
originalLineEnding,
};
}
// If there was an error, try to self-correct.
return this.attemptSelfCorrection(
params,
@@ -106,6 +106,7 @@ const mockConfigInternal = {
discoverTools: vi.fn(),
}) as unknown as ToolRegistry,
isInteractive: () => false,
getDisableLLMCorrection: vi.fn(() => false),
};
const mockConfig = mockConfigInternal as unknown as Config;
@@ -293,6 +294,7 @@ describe('WriteFileTool', () => {
proposedContent,
mockBaseLlmClientInstance,
abortSignal,
false,
);
expect(mockEnsureCorrectEdit).not.toHaveBeenCalled();
expect(result.correctedContent).toBe(correctedContent);
@@ -337,6 +339,7 @@ describe('WriteFileTool', () => {
mockGeminiClientInstance,
mockBaseLlmClientInstance,
abortSignal,
false,
);
expect(mockEnsureCorrectFileContent).not.toHaveBeenCalled();
expect(result.correctedContent).toBe(correctedProposedContent);
@@ -414,6 +417,7 @@ describe('WriteFileTool', () => {
proposedContent,
mockBaseLlmClientInstance,
abortSignal,
false,
);
expect(confirmation).toEqual(
expect.objectContaining({
@@ -464,6 +468,7 @@ describe('WriteFileTool', () => {
mockGeminiClientInstance,
mockBaseLlmClientInstance,
abortSignal,
false,
);
expect(confirmation).toEqual(
expect.objectContaining({
@@ -658,6 +663,7 @@ describe('WriteFileTool', () => {
proposedContent,
mockBaseLlmClientInstance,
abortSignal,
false,
);
expect(result.llmContent).toMatch(
/Successfully created and wrote to new file/,
@@ -715,6 +721,7 @@ describe('WriteFileTool', () => {
mockGeminiClientInstance,
mockBaseLlmClientInstance,
abortSignal,
false,
);
expect(result.llmContent).toMatch(/Successfully overwrote file/);
const writtenContent = await fsService.readTextFile(filePath);
@@ -899,4 +906,73 @@ describe('WriteFileTool', () => {
},
);
});
describe('disableLLMCorrection', () => {
const abortSignal = new AbortController().signal;
it('should call ensureCorrectFileContent with disableLLMCorrection=true for a new file when disabled', async () => {
const filePath = path.join(rootDir, 'new_file_no_correction.txt');
const proposedContent = 'Proposed content.';
mockConfigInternal.getDisableLLMCorrection.mockReturnValue(true);
// Ensure the mock returns the content passed to it (simulating no change or unescaped change)
mockEnsureCorrectFileContent.mockResolvedValue(proposedContent);
const result = await getCorrectedFileContent(
mockConfig,
filePath,
proposedContent,
abortSignal,
);
expect(mockEnsureCorrectFileContent).toHaveBeenCalledWith(
proposedContent,
mockBaseLlmClientInstance,
abortSignal,
true,
);
expect(mockEnsureCorrectEdit).not.toHaveBeenCalled();
expect(result.correctedContent).toBe(proposedContent);
expect(result.fileExists).toBe(false);
});
it('should call ensureCorrectEdit with disableLLMCorrection=true for an existing file when disabled', async () => {
const filePath = path.join(rootDir, 'existing_file_no_correction.txt');
const originalContent = 'Original content.';
const proposedContent = 'Proposed content.';
fs.writeFileSync(filePath, originalContent, 'utf8');
mockConfigInternal.getDisableLLMCorrection.mockReturnValue(true);
// Ensure the mock returns the content passed to it
mockEnsureCorrectEdit.mockResolvedValue({
params: {
file_path: filePath,
old_string: originalContent,
new_string: proposedContent,
},
occurrences: 1,
});
const result = await getCorrectedFileContent(
mockConfig,
filePath,
proposedContent,
abortSignal,
);
expect(mockEnsureCorrectEdit).toHaveBeenCalledWith(
filePath,
originalContent,
expect.anything(), // params object
mockGeminiClientInstance,
mockBaseLlmClientInstance,
abortSignal,
true,
);
expect(mockEnsureCorrectFileContent).not.toHaveBeenCalled();
expect(result.correctedContent).toBe(proposedContent);
expect(result.originalContent).toBe(originalContent);
expect(result.fileExists).toBe(true);
});
});
});
+2
View File
@@ -127,6 +127,7 @@ export async function getCorrectedFileContent(
config.getGeminiClient(),
config.getBaseLlmClient(),
abortSignal,
config.getDisableLLMCorrection(),
);
correctedContent = correctedParams.new_string;
} else {
@@ -135,6 +136,7 @@ export async function getCorrectedFileContent(
proposedContent,
config.getBaseLlmClient(),
abortSignal,
config.getDisableLLMCorrection(),
);
}
return { originalContent, correctedContent, fileExists };