Skip to content

Commit 4a114a2

Browse files
committed
fix: enhance null edit detection for Gemini models
- Add detailed error messages when search and replace content are identical - Provide clear guidance on common causes (model hallucination, copy/paste errors) - Add comprehensive tests for null edit detection - Help AI models (especially Gemini) avoid producing phantom edits Fixes #7360
1 parent fc70012 commit 4a114a2

File tree

3 files changed

+154
-6
lines changed

3 files changed

+154
-6
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace"
2+
3+
describe("null edit detection", () => {
4+
let strategy: MultiSearchReplaceDiffStrategy
5+
6+
beforeEach(() => {
7+
strategy = new MultiSearchReplaceDiffStrategy()
8+
})
9+
10+
it("should detect and reject null edits (identical search and replace)", async () => {
11+
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
12+
const diffContent = [
13+
"<<<<<<< SEARCH",
14+
"function hello() {",
15+
' console.log("hello")',
16+
"}",
17+
"=======",
18+
"function hello() {",
19+
' console.log("hello")',
20+
"}",
21+
">>>>>>> REPLACE",
22+
].join("\n")
23+
24+
const result = await strategy.applyDiff(originalContent, diffContent)
25+
expect(result.success).toBe(false)
26+
if (!result.success && result.failParts && result.failParts.length > 0) {
27+
const firstError = result.failParts[0]
28+
if (!firstError.success && firstError.error) {
29+
expect(firstError.error).toContain("NULL EDIT DETECTED")
30+
expect(firstError.error).toContain("Search and replace content are identical")
31+
expect(firstError.error).toContain("This is a common issue with AI models (especially Gemini)")
32+
expect(firstError.error).toContain("Model hallucination")
33+
}
34+
}
35+
})
36+
37+
it("should detect null edits in multi-block diffs", async () => {
38+
const originalContent =
39+
'function hello() {\n console.log("hello")\n}\nfunction world() {\n return "world"\n}'
40+
const diffContent = [
41+
"<<<<<<< SEARCH",
42+
"function hello() {",
43+
' console.log("hello")',
44+
"}",
45+
"=======",
46+
"function hello() {",
47+
' console.log("hello world")',
48+
"}",
49+
">>>>>>> REPLACE",
50+
"",
51+
"<<<<<<< SEARCH",
52+
"function world() {",
53+
' return "world"',
54+
"}",
55+
"=======",
56+
"function world() {",
57+
' return "world"',
58+
"}",
59+
">>>>>>> REPLACE",
60+
].join("\n")
61+
62+
const result = await strategy.applyDiff(originalContent, diffContent)
63+
// Should partially succeed but report the null edit
64+
expect(result.success).toBe(true)
65+
if (result.success) {
66+
expect(result.failParts).toBeDefined()
67+
if (result.failParts && result.failParts[0] && !result.failParts[0].success) {
68+
expect(result.failParts[0].error).toContain("NULL EDIT DETECTED")
69+
}
70+
}
71+
})
72+
73+
it("should detect null edits with line numbers", async () => {
74+
const originalContent = "function test() {\n return true;\n}\n"
75+
const diffContent = [
76+
"<<<<<<< SEARCH",
77+
":start_line:1",
78+
"-------",
79+
"function test() {",
80+
" return true;",
81+
"}",
82+
"=======",
83+
"function test() {",
84+
" return true;",
85+
"}",
86+
">>>>>>> REPLACE",
87+
].join("\n")
88+
89+
const result = await strategy.applyDiff(originalContent, diffContent)
90+
expect(result.success).toBe(false)
91+
if (!result.success && result.failParts && result.failParts.length > 0) {
92+
const firstError = result.failParts[0]
93+
if (!firstError.success && firstError.error) {
94+
expect(firstError.error).toContain("NULL EDIT DETECTED")
95+
expect(firstError.error).toContain("phantom")
96+
expect(firstError.error).toContain("Ensure the REPLACE block contains the actual modified content")
97+
}
98+
}
99+
})
100+
101+
it("should not trigger null edit detection for legitimate empty replacements (deletions)", async () => {
102+
const originalContent = "function test() {\n // Remove this comment\n return true;\n}\n"
103+
const diffContent = ["<<<<<<< SEARCH", " // Remove this comment", "=======", ">>>>>>> REPLACE"].join("\n")
104+
105+
const result = await strategy.applyDiff(originalContent, diffContent)
106+
expect(result.success).toBe(true)
107+
// Should not contain null edit error
108+
if (!result.success && result.error) {
109+
expect(result.error).not.toContain("NULL EDIT DETECTED")
110+
}
111+
})
112+
113+
it("should not trigger for actual changes even if similar", async () => {
114+
const originalContent = "function test() {\n return true;\n}\n"
115+
const diffContent = [
116+
"<<<<<<< SEARCH",
117+
"function test() {",
118+
" return true;",
119+
"}",
120+
"=======",
121+
"function test() {",
122+
" return false;",
123+
"}",
124+
">>>>>>> REPLACE",
125+
].join("\n")
126+
127+
const result = await strategy.applyDiff(originalContent, diffContent)
128+
expect(result.success).toBe(true)
129+
// Should not contain null edit error
130+
if (!result.success && result.error) {
131+
expect(result.error).not.toContain("NULL EDIT DETECTED")
132+
}
133+
})
134+
})

src/core/diff/strategies/multi-file-search-replace.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -515,10 +515,17 @@ Each file requires its own path, start_line, and diff elements.
515515
diffResults.push({
516516
success: false,
517517
error:
518-
`Search and replace content are identical - no changes would be made\n\n` +
518+
`NULL EDIT DETECTED: Search and replace content are identical - no changes would be made\n\n` +
519+
`This is a common issue with AI models (especially Gemini) producing "phantom" edits.\n\n` +
519520
`Debug Info:\n` +
520-
`- Search and replace must be different to make changes\n` +
521-
`- Use read_file to verify the content you want to change`,
521+
`- The SEARCH block and REPLACE block contain exactly the same content\n` +
522+
`- This means the edit would have zero effect on the file\n` +
523+
`- To fix: Ensure the REPLACE block contains the actual modified content\n` +
524+
`- Use read_file to verify the current file content before making changes\n\n` +
525+
`Common causes:\n` +
526+
`1. Model hallucination - thinking it made changes when it didn't\n` +
527+
`2. Incorrect copy/paste of content between blocks\n` +
528+
`3. Missing the actual modification in the REPLACE block`,
522529
})
523530
continue
524531
}

src/core/diff/strategies/multi-search-replace.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -432,10 +432,17 @@ Only use a single line of '=======' between search and replacement content, beca
432432
diffResults.push({
433433
success: false,
434434
error:
435-
`Search and replace content are identical - no changes would be made\n\n` +
435+
`NULL EDIT DETECTED: Search and replace content are identical - no changes would be made\n\n` +
436+
`This is a common issue with AI models (especially Gemini) producing "phantom" edits.\n\n` +
436437
`Debug Info:\n` +
437-
`- Search and replace must be different to make changes\n` +
438-
`- Use read_file to verify the content you want to change`,
438+
`- The SEARCH block and REPLACE block contain exactly the same content\n` +
439+
`- This means the edit would have zero effect on the file\n` +
440+
`- To fix: Ensure the REPLACE block contains the actual modified content\n` +
441+
`- Use read_file to verify the current file content before making changes\n\n` +
442+
`Common causes:\n` +
443+
`1. Model hallucination - thinking it made changes when it didn't\n` +
444+
`2. Incorrect copy/paste of content between blocks\n` +
445+
`3. Missing the actual modification in the REPLACE block`,
439446
})
440447
continue
441448
}

0 commit comments

Comments
 (0)