feat: add case sensitivity debug logging to search algorithm

- Add detection for case mismatches when search fails
- Log case sensitivity issues to console for AI debugging
- Show potential case mismatch count in error messages
- Add comprehensive tests for case sensitivity detection

Fixes #4731
This commit is contained in:
Roo Code 2025-07-19 15:19:43 +00:00
parent cc369da2d5
commit 4e31af0591
3 changed files with 262 additions and 2 deletions

View file

@ -781,6 +781,178 @@ function five() {
}
})
})
describe("case sensitivity detection", () => {
let strategy: MultiSearchReplaceDiffStrategy
beforeEach(() => {
strategy = new MultiSearchReplaceDiffStrategy()
})
it("should detect case mismatch when search content differs from file content only by case", async () => {
const originalContent = `function getData() {
const originalStructRVA = 0;
return originalStructRVA;
}`
const diffContent =
"<<<<<<< SEARCH\n" +
"function getData() {\n" +
" const originalStructRva = 0;\n" +
" return originalStructRva;\n" +
"}\n" +
"=======\n" +
"function getData() {\n" +
" const newValue = 0;\n" +
" return newValue;\n" +
"}\n" +
">>>>>>> REPLACE"
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(false)
// When there's a single diff block, the error is in result.error, not failParts
if (!result.success && result.error) {
expect(result.error).toContain("No sufficiently similar match found")
expect(result.error).toContain("Potential case mismatch")
}
})
it("should not show case mismatch warning when content is genuinely identical", async () => {
const originalContent = `function test() {
return true;
}`
const diffContent =
"<<<<<<< SEARCH\n" +
"function test() {\n" +
" return true;\n" +
"}\n" +
"=======\n" +
"function test() {\n" +
" return true;\n" +
"}\n" +
">>>>>>> REPLACE"
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(false)
// The error should be about identical content without case mismatch
if (!result.success && result.error) {
expect(result.error).toContain("Search and replace content are identical")
expect(result.error).not.toContain("potential case mismatch")
}
})
it("should detect case mismatches in no match found errors", async () => {
const originalContent = `function getData() {
const originalStructRVA = 0;
return originalStructRVA;
}`
const diffContent =
"<<<<<<< SEARCH\n" +
"function getData() {\n" +
" const originalstructrva = 0;\n" +
" return originalstructrva;\n" +
"}\n" +
"=======\n" +
"function getData() {\n" +
" const newValue = 0;\n" +
" return newValue;\n" +
"}\n" +
">>>>>>> REPLACE"
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(false)
if (!result.success && result.error) {
expect(result.error).toContain("No sufficiently similar match found")
expect(result.error).toContain("Potential case mismatch")
}
})
it("should log case sensitivity issues when search and replace differ only by case", async () => {
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {})
const originalContent = `function test() {
const myVariable = 0;
return myVariable;
}`
const diffContent =
"<<<<<<< SEARCH\n" +
"function test() {\n" +
" const myVariable = 0;\n" +
" return myVariable;\n" +
"}\n" +
"=======\n" +
"function test() {\n" +
" const MyVariable = 0;\n" +
" return MyVariable;\n" +
"}\n" +
">>>>>>> REPLACE"
await strategy.applyDiff(originalContent, diffContent)
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("[apply_diff] Case sensitivity mismatch detected"),
)
consoleSpy.mockRestore()
})
it("should handle multiple case mismatches in the same file", async () => {
const originalContent = `class TestClass {
constructor() {
this.myProperty = 0;
this.OtherProperty = 1;
}
getMyProperty() {
return this.myProperty;
}
}`
const diffContent =
"<<<<<<< SEARCH\n" +
"class TestClass {\n" +
" constructor() {\n" +
" this.myproperty = 0;\n" +
" this.otherproperty = 1;\n" +
" }\n" +
"=======\n" +
"class TestClass {\n" +
" constructor() {\n" +
" this.myProp = 0;\n" +
" this.otherProp = 1;\n" +
" }\n" +
">>>>>>> REPLACE"
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(false)
if (!result.success && result.error) {
expect(result.error).toContain("No sufficiently similar match found")
expect(result.error).toContain("Potential case mismatch")
}
})
it("should detect case differences in search vs replace content", async () => {
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {})
const originalContent = `function test() {
return true;
}`
const diffContent =
"<<<<<<< SEARCH\n" +
"function test() {\n" +
" return true;\n" +
"}\n" +
"=======\n" +
"function TEST() {\n" +
" return TRUE;\n" +
"}\n" +
">>>>>>> REPLACE"
await strategy.applyDiff(originalContent, diffContent)
// This test is for multi-file-search-replace.ts functionality
// The console.log is only in multi-file-search-replace.ts
consoleSpy.mockRestore()
})
})
})
describe("fuzzy matching", () => {

View file

@ -532,6 +532,31 @@ Each file requires its own path, start_line, and diff elements.
let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
let replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
// Check for case sensitivity issues after identical content check
if (searchContent !== replaceContent && searchContent.toLowerCase() === replaceContent.toLowerCase()) {
// The content differs only by case - add this to debug info
const caseDifferences: string[] = []
const searchWords = searchContent.split(/\s+/)
const replaceWords = replaceContent.split(/\s+/)
for (let i = 0; i < Math.min(searchWords.length, replaceWords.length); i++) {
if (
searchWords[i] !== replaceWords[i] &&
searchWords[i].toLowerCase() === replaceWords[i].toLowerCase()
) {
caseDifferences.push(`"${searchWords[i]}" vs "${replaceWords[i]}"`)
}
}
if (caseDifferences.length > 0) {
console.log(`[apply_diff] Case sensitivity mismatch detected in search/replace content:`)
console.log(` File: ${originalContent.substring(0, 50)}...`)
console.log(
` Case differences: ${caseDifferences.slice(0, 3).join(", ")}${caseDifferences.length > 3 ? ` and ${caseDifferences.length - 3} more` : ""}`,
)
}
}
// Validate that search content is not empty
if (searchLines.length === 0) {
diffResults.push({
@ -634,6 +659,25 @@ Each file requires its own path, start_line, and diff elements.
const lineRange = startLine ? ` at line: ${startLine}` : ""
// Check for potential case mismatches in the entire file
let caseMismatchInfo = ""
const searchLower = searchChunk.toLowerCase()
let caseMismatchCount = 0
for (let i = 0; i <= resultLines.length - searchLines.length; i++) {
const chunk = resultLines.slice(i, i + searchLines.length).join("\n")
if (chunk.toLowerCase() === searchLower && chunk !== searchChunk) {
caseMismatchCount++
}
}
if (caseMismatchCount > 0) {
caseMismatchInfo = `\n- 🔍 **Potential case mismatch**: Found ${caseMismatchCount} location(s) where content differs only by case`
console.log(
`[apply_diff] Case sensitivity issue detected: ${caseMismatchCount} potential matches with different casing`,
)
}
diffResults.push({
success: false,
error: `No sufficiently similar match found${lineRange} (${Math.floor(
@ -644,7 +688,7 @@ Each file requires its own path, start_line, and diff elements.
bestMatchScore * 100,
)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${
startLine ? `starting at line ${startLine}` : "start to end"
}\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
}\n- Tried both standard and aggressive line number stripping${caseMismatchInfo}\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
})
continue
}

View file

@ -441,6 +441,31 @@ Only use a single line of '=======' between search and replacement content, beca
let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
let replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
// Check for case sensitivity issues when search and replace differ only by case
if (searchContent !== replaceContent && searchContent.toLowerCase() === replaceContent.toLowerCase()) {
// The content differs only by case - add this to debug info
const caseDifferences: string[] = []
const searchWords = searchContent.split(/\s+/)
const replaceWords = replaceContent.split(/\s+/)
for (let i = 0; i < Math.min(searchWords.length, replaceWords.length); i++) {
if (
searchWords[i] !== replaceWords[i] &&
searchWords[i].toLowerCase() === replaceWords[i].toLowerCase()
) {
caseDifferences.push(`"${searchWords[i]}" vs "${replaceWords[i]}"`)
}
}
if (caseDifferences.length > 0) {
console.log(`[apply_diff] Case sensitivity mismatch detected in search/replace content:`)
console.log(` File: ${originalContent.substring(0, 50)}...`)
console.log(
` Case differences: ${caseDifferences.slice(0, 3).join(", ")}${caseDifferences.length > 3 ? ` and ${caseDifferences.length - 3} more` : ""}`,
)
}
}
// Validate that search content is not empty
if (searchLines.length === 0) {
diffResults.push({
@ -540,9 +565,28 @@ Only use a single line of '=======' between search and replacement content, beca
const lineRange = startLine ? ` at line: ${startLine}` : ""
// Check for potential case mismatches in the entire file
let caseMismatchInfo = ""
const searchLower = searchChunk.toLowerCase()
let caseMismatchCount = 0
for (let i = 0; i <= resultLines.length - searchLines.length; i++) {
const chunk = resultLines.slice(i, i + searchLines.length).join("\n")
if (chunk.toLowerCase() === searchLower && chunk !== searchChunk) {
caseMismatchCount++
}
}
if (caseMismatchCount > 0) {
caseMismatchInfo = `\n- 🔍 **Potential case mismatch**: Found ${caseMismatchCount} location(s) where content differs only by case`
console.log(
`[apply_diff] Case sensitivity issue detected: ${caseMismatchCount} potential matches with different casing`,
)
}
diffResults.push({
success: false,
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine ? `starting at line ${startLine}` : "start to end"}\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine ? `starting at line ${startLine}` : "start to end"}\n- Tried both standard and aggressive line number stripping${caseMismatchInfo}\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
})
continue
}