🎯 PR: Improve Edit Tool Reliability with Fuzzy Matching Pipeline (#1025)

This commit is contained in:
tanzhenxin
2025-11-13 19:01:09 +08:00
committed by GitHub
parent b029f0d2ce
commit 0752a31e1e
5 changed files with 718 additions and 109 deletions

View File

@@ -425,7 +425,9 @@ describe('EditTool', () => {
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toMatch(/Successfully modified file/);
expect(result.llmContent).toMatch(
/Showing lines \d+-\d+ of \d+ from the edited file:/,
);
expect(fs.readFileSync(filePath, 'utf8')).toBe(newContent);
const display = result.returnDisplay as FileDiff;
expect(display.fileDiff).toMatch(initialContent);
@@ -450,6 +452,9 @@ describe('EditTool', () => {
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toMatch(/Created new file/);
expect(result.llmContent).toMatch(
/Showing lines \d+-\d+ of \d+ from the edited file:/,
);
expect(fs.existsSync(newFilePath)).toBe(true);
expect(fs.readFileSync(newFilePath, 'utf8')).toBe(fileContent);
@@ -485,7 +490,7 @@ describe('EditTool', () => {
);
});
it('should return error if multiple occurrences of old_string are found', async () => {
it('should return error if multiple occurrences of old_string are found and replace_all is false', async () => {
fs.writeFileSync(filePath, 'multiple old old strings', 'utf8');
const params: EditToolParams = {
file_path: filePath,
@@ -494,27 +499,27 @@ describe('EditTool', () => {
};
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toMatch(
/Expected 1 occurrence but found 2 for old_string in file/,
);
expect(result.llmContent).toMatch(/replace_all was not enabled/);
expect(result.returnDisplay).toMatch(
/Failed to edit, expected 1 occurrence but found 2/,
/Failed to edit because the text matches multiple locations/,
);
});
it('should successfully replace multiple occurrences when expected_replacements specified', async () => {
it('should successfully replace multiple occurrences when replace_all is true', async () => {
fs.writeFileSync(filePath, 'old text\nold text\nold text', 'utf8');
const params: EditToolParams = {
file_path: filePath,
old_string: 'old',
new_string: 'new',
expected_replacements: 3,
replace_all: true,
};
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toMatch(/Successfully modified file/);
expect(result.llmContent).toMatch(
/Showing lines \d+-\d+ of \d+ from the edited file/,
);
expect(fs.readFileSync(filePath, 'utf8')).toBe(
'new text\nnew text\nnew text',
);
@@ -535,24 +540,6 @@ describe('EditTool', () => {
});
});
it('should return error if expected_replacements does not match actual occurrences', async () => {
fs.writeFileSync(filePath, 'old text old text', 'utf8');
const params: EditToolParams = {
file_path: filePath,
old_string: 'old',
new_string: 'new',
expected_replacements: 3, // Expecting 3 but only 2 exist
};
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toMatch(
/Expected 3 occurrences but found 2 for old_string in file/,
);
expect(result.returnDisplay).toMatch(
/Failed to edit, expected 3 occurrences but found 2/,
);
});
it('should return error if trying to create a file that already exists (empty old_string)', async () => {
fs.writeFileSync(filePath, 'Existing content', 'utf8');
const params: EditToolParams = {
@@ -568,38 +555,6 @@ describe('EditTool', () => {
);
});
it('should include modification message when proposed content is modified', async () => {
const initialContent = 'Line 1\nold line\nLine 3\nLine 4\nLine 5\n';
fs.writeFileSync(filePath, initialContent, 'utf8');
const params: EditToolParams = {
file_path: filePath,
old_string: 'old',
new_string: 'new',
modified_by_user: true,
ai_proposed_content: 'Line 1\nAI line\nLine 3\nLine 4\nLine 5\n',
};
(mockConfig.getApprovalMode as Mock).mockReturnValueOnce(
ApprovalMode.AUTO_EDIT,
);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toMatch(
/User modified the `new_string` content/,
);
expect((result.returnDisplay as FileDiff).diffStat).toStrictEqual({
model_added_lines: 1,
model_removed_lines: 1,
model_added_chars: 7,
model_removed_chars: 8,
user_added_lines: 1,
user_removed_lines: 1,
user_added_chars: 8,
user_removed_chars: 7,
});
});
it('should not include modification message when proposed content is not modified', async () => {
const initialContent = 'This is some old text.';
fs.writeFileSync(filePath, initialContent, 'utf8');
@@ -723,13 +678,12 @@ describe('EditTool', () => {
expect(result.error?.type).toBe(ToolErrorType.EDIT_NO_OCCURRENCE_FOUND);
});
it('should return EXPECTED_OCCURRENCE_MISMATCH error', async () => {
it('should return EXPECTED_OCCURRENCE_MISMATCH error when replace_all is false and text is not unique', async () => {
fs.writeFileSync(filePath, 'one one two', 'utf8');
const params: EditToolParams = {
file_path: filePath,
old_string: 'one',
new_string: 'new',
expected_replacements: 3,
};
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);