diff --git a/packages/core/src/tools/edit.test.ts b/packages/core/src/tools/edit.test.ts index 93b17e73..f102030e 100644 --- a/packages/core/src/tools/edit.test.ts +++ b/packages/core/src/tools/edit.test.ts @@ -632,6 +632,33 @@ describe('EditTool', () => { expect(result.llmContent).toMatch(/No changes to apply/); expect(result.returnDisplay).toMatch(/No changes to apply/); }); + + it('should return EDIT_NO_CHANGE error if replacement results in identical content', async () => { + // This can happen if ensureCorrectEdit finds a fuzzy match, but the literal + // string replacement with `replaceAll` results in no change. + const initialContent = 'line 1\nline 2\nline 3'; // Note the double space + fs.writeFileSync(filePath, initialContent, 'utf8'); + const params: EditToolParams = { + file_path: filePath, + // old_string has a single space, so it won't be found by replaceAll + old_string: 'line 1\nline 2\nline 3', + new_string: 'line 1\nnew line 2\nline 3', + }; + + // Mock ensureCorrectEdit to simulate it finding a match (e.g., via fuzzy matching) + // but it doesn't correct the old_string to the literal content. + mockEnsureCorrectEdit.mockResolvedValueOnce({ params, occurrences: 1 }); + + const invocation = tool.build(params); + const result = await invocation.execute(new AbortController().signal); + + expect(result.error?.type).toBe(ToolErrorType.EDIT_NO_CHANGE); + expect(result.returnDisplay).toMatch( + /No changes to apply. The new content is identical to the current content./, + ); + // Ensure the file was not actually changed + expect(fs.readFileSync(filePath, 'utf8')).toBe(initialContent); + }); }); describe('Error Scenarios', () => { diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index 915097d1..e69e10cb 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -204,12 +204,23 @@ class EditToolInvocation implements ToolInvocation { }; } - const newContent = applyReplacement( - currentContent, - finalOldString, - finalNewString, - isNewFile, - ); + const newContent = !error + ? applyReplacement( + currentContent, + finalOldString, + finalNewString, + isNewFile, + ) + : (currentContent ?? ''); + + if (!error && fileExists && currentContent === newContent) { + error = { + display: + 'No changes to apply. The new content is identical to the current content.', + raw: `No changes to apply. The new content is identical to the current content in file: ${params.file_path}`, + type: ToolErrorType.EDIT_NO_CHANGE, + }; + } return { currentContent,