Skip to content

Commit 4e31af0

Browse files
committed
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
1 parent cc369da commit 4e31af0

File tree

3 files changed

+262
-2
lines changed

3 files changed

+262
-2
lines changed

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

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,178 @@ function five() {
781781
}
782782
})
783783
})
784+
785+
describe("case sensitivity detection", () => {
786+
let strategy: MultiSearchReplaceDiffStrategy
787+
788+
beforeEach(() => {
789+
strategy = new MultiSearchReplaceDiffStrategy()
790+
})
791+
792+
it("should detect case mismatch when search content differs from file content only by case", async () => {
793+
const originalContent = `function getData() {
794+
const originalStructRVA = 0;
795+
return originalStructRVA;
796+
}`
797+
const diffContent =
798+
"<<<<<<< SEARCH\n" +
799+
"function getData() {\n" +
800+
" const originalStructRva = 0;\n" +
801+
" return originalStructRva;\n" +
802+
"}\n" +
803+
"=======\n" +
804+
"function getData() {\n" +
805+
" const newValue = 0;\n" +
806+
" return newValue;\n" +
807+
"}\n" +
808+
">>>>>>> REPLACE"
809+
810+
const result = await strategy.applyDiff(originalContent, diffContent)
811+
expect(result.success).toBe(false)
812+
// When there's a single diff block, the error is in result.error, not failParts
813+
if (!result.success && result.error) {
814+
expect(result.error).toContain("No sufficiently similar match found")
815+
expect(result.error).toContain("Potential case mismatch")
816+
}
817+
})
818+
819+
it("should not show case mismatch warning when content is genuinely identical", async () => {
820+
const originalContent = `function test() {
821+
return true;
822+
}`
823+
const diffContent =
824+
"<<<<<<< SEARCH\n" +
825+
"function test() {\n" +
826+
" return true;\n" +
827+
"}\n" +
828+
"=======\n" +
829+
"function test() {\n" +
830+
" return true;\n" +
831+
"}\n" +
832+
">>>>>>> REPLACE"
833+
834+
const result = await strategy.applyDiff(originalContent, diffContent)
835+
expect(result.success).toBe(false)
836+
// The error should be about identical content without case mismatch
837+
if (!result.success && result.error) {
838+
expect(result.error).toContain("Search and replace content are identical")
839+
expect(result.error).not.toContain("potential case mismatch")
840+
}
841+
})
842+
843+
it("should detect case mismatches in no match found errors", async () => {
844+
const originalContent = `function getData() {
845+
const originalStructRVA = 0;
846+
return originalStructRVA;
847+
}`
848+
const diffContent =
849+
"<<<<<<< SEARCH\n" +
850+
"function getData() {\n" +
851+
" const originalstructrva = 0;\n" +
852+
" return originalstructrva;\n" +
853+
"}\n" +
854+
"=======\n" +
855+
"function getData() {\n" +
856+
" const newValue = 0;\n" +
857+
" return newValue;\n" +
858+
"}\n" +
859+
">>>>>>> REPLACE"
860+
861+
const result = await strategy.applyDiff(originalContent, diffContent)
862+
expect(result.success).toBe(false)
863+
if (!result.success && result.error) {
864+
expect(result.error).toContain("No sufficiently similar match found")
865+
expect(result.error).toContain("Potential case mismatch")
866+
}
867+
})
868+
869+
it("should log case sensitivity issues when search and replace differ only by case", async () => {
870+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {})
871+
872+
const originalContent = `function test() {
873+
const myVariable = 0;
874+
return myVariable;
875+
}`
876+
const diffContent =
877+
"<<<<<<< SEARCH\n" +
878+
"function test() {\n" +
879+
" const myVariable = 0;\n" +
880+
" return myVariable;\n" +
881+
"}\n" +
882+
"=======\n" +
883+
"function test() {\n" +
884+
" const MyVariable = 0;\n" +
885+
" return MyVariable;\n" +
886+
"}\n" +
887+
">>>>>>> REPLACE"
888+
889+
await strategy.applyDiff(originalContent, diffContent)
890+
891+
expect(consoleSpy).toHaveBeenCalledWith(
892+
expect.stringContaining("[apply_diff] Case sensitivity mismatch detected"),
893+
)
894+
895+
consoleSpy.mockRestore()
896+
})
897+
898+
it("should handle multiple case mismatches in the same file", async () => {
899+
const originalContent = `class TestClass {
900+
constructor() {
901+
this.myProperty = 0;
902+
this.OtherProperty = 1;
903+
}
904+
905+
getMyProperty() {
906+
return this.myProperty;
907+
}
908+
}`
909+
const diffContent =
910+
"<<<<<<< SEARCH\n" +
911+
"class TestClass {\n" +
912+
" constructor() {\n" +
913+
" this.myproperty = 0;\n" +
914+
" this.otherproperty = 1;\n" +
915+
" }\n" +
916+
"=======\n" +
917+
"class TestClass {\n" +
918+
" constructor() {\n" +
919+
" this.myProp = 0;\n" +
920+
" this.otherProp = 1;\n" +
921+
" }\n" +
922+
">>>>>>> REPLACE"
923+
924+
const result = await strategy.applyDiff(originalContent, diffContent)
925+
expect(result.success).toBe(false)
926+
if (!result.success && result.error) {
927+
expect(result.error).toContain("No sufficiently similar match found")
928+
expect(result.error).toContain("Potential case mismatch")
929+
}
930+
})
931+
932+
it("should detect case differences in search vs replace content", async () => {
933+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {})
934+
935+
const originalContent = `function test() {
936+
return true;
937+
}`
938+
const diffContent =
939+
"<<<<<<< SEARCH\n" +
940+
"function test() {\n" +
941+
" return true;\n" +
942+
"}\n" +
943+
"=======\n" +
944+
"function TEST() {\n" +
945+
" return TRUE;\n" +
946+
"}\n" +
947+
">>>>>>> REPLACE"
948+
949+
await strategy.applyDiff(originalContent, diffContent)
950+
951+
// This test is for multi-file-search-replace.ts functionality
952+
// The console.log is only in multi-file-search-replace.ts
953+
consoleSpy.mockRestore()
954+
})
955+
})
784956
})
785957

786958
describe("fuzzy matching", () => {

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,31 @@ Each file requires its own path, start_line, and diff elements.
532532
let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
533533
let replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
534534

535+
// Check for case sensitivity issues after identical content check
536+
if (searchContent !== replaceContent && searchContent.toLowerCase() === replaceContent.toLowerCase()) {
537+
// The content differs only by case - add this to debug info
538+
const caseDifferences: string[] = []
539+
const searchWords = searchContent.split(/\s+/)
540+
const replaceWords = replaceContent.split(/\s+/)
541+
542+
for (let i = 0; i < Math.min(searchWords.length, replaceWords.length); i++) {
543+
if (
544+
searchWords[i] !== replaceWords[i] &&
545+
searchWords[i].toLowerCase() === replaceWords[i].toLowerCase()
546+
) {
547+
caseDifferences.push(`"${searchWords[i]}" vs "${replaceWords[i]}"`)
548+
}
549+
}
550+
551+
if (caseDifferences.length > 0) {
552+
console.log(`[apply_diff] Case sensitivity mismatch detected in search/replace content:`)
553+
console.log(` File: ${originalContent.substring(0, 50)}...`)
554+
console.log(
555+
` Case differences: ${caseDifferences.slice(0, 3).join(", ")}${caseDifferences.length > 3 ? ` and ${caseDifferences.length - 3} more` : ""}`,
556+
)
557+
}
558+
}
559+
535560
// Validate that search content is not empty
536561
if (searchLines.length === 0) {
537562
diffResults.push({
@@ -634,6 +659,25 @@ Each file requires its own path, start_line, and diff elements.
634659

635660
const lineRange = startLine ? ` at line: ${startLine}` : ""
636661

662+
// Check for potential case mismatches in the entire file
663+
let caseMismatchInfo = ""
664+
const searchLower = searchChunk.toLowerCase()
665+
let caseMismatchCount = 0
666+
667+
for (let i = 0; i <= resultLines.length - searchLines.length; i++) {
668+
const chunk = resultLines.slice(i, i + searchLines.length).join("\n")
669+
if (chunk.toLowerCase() === searchLower && chunk !== searchChunk) {
670+
caseMismatchCount++
671+
}
672+
}
673+
674+
if (caseMismatchCount > 0) {
675+
caseMismatchInfo = `\n- 🔍 **Potential case mismatch**: Found ${caseMismatchCount} location(s) where content differs only by case`
676+
console.log(
677+
`[apply_diff] Case sensitivity issue detected: ${caseMismatchCount} potential matches with different casing`,
678+
)
679+
}
680+
637681
diffResults.push({
638682
success: false,
639683
error: `No sufficiently similar match found${lineRange} (${Math.floor(
@@ -644,7 +688,7 @@ Each file requires its own path, start_line, and diff elements.
644688
bestMatchScore * 100,
645689
)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${
646690
startLine ? `starting at line ${startLine}` : "start to end"
647-
}\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}`,
691+
}\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}`,
648692
})
649693
continue
650694
}

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,31 @@ Only use a single line of '=======' between search and replacement content, beca
441441
let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
442442
let replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
443443

444+
// Check for case sensitivity issues when search and replace differ only by case
445+
if (searchContent !== replaceContent && searchContent.toLowerCase() === replaceContent.toLowerCase()) {
446+
// The content differs only by case - add this to debug info
447+
const caseDifferences: string[] = []
448+
const searchWords = searchContent.split(/\s+/)
449+
const replaceWords = replaceContent.split(/\s+/)
450+
451+
for (let i = 0; i < Math.min(searchWords.length, replaceWords.length); i++) {
452+
if (
453+
searchWords[i] !== replaceWords[i] &&
454+
searchWords[i].toLowerCase() === replaceWords[i].toLowerCase()
455+
) {
456+
caseDifferences.push(`"${searchWords[i]}" vs "${replaceWords[i]}"`)
457+
}
458+
}
459+
460+
if (caseDifferences.length > 0) {
461+
console.log(`[apply_diff] Case sensitivity mismatch detected in search/replace content:`)
462+
console.log(` File: ${originalContent.substring(0, 50)}...`)
463+
console.log(
464+
` Case differences: ${caseDifferences.slice(0, 3).join(", ")}${caseDifferences.length > 3 ? ` and ${caseDifferences.length - 3} more` : ""}`,
465+
)
466+
}
467+
}
468+
444469
// Validate that search content is not empty
445470
if (searchLines.length === 0) {
446471
diffResults.push({
@@ -540,9 +565,28 @@ Only use a single line of '=======' between search and replacement content, beca
540565

541566
const lineRange = startLine ? ` at line: ${startLine}` : ""
542567

568+
// Check for potential case mismatches in the entire file
569+
let caseMismatchInfo = ""
570+
const searchLower = searchChunk.toLowerCase()
571+
let caseMismatchCount = 0
572+
573+
for (let i = 0; i <= resultLines.length - searchLines.length; i++) {
574+
const chunk = resultLines.slice(i, i + searchLines.length).join("\n")
575+
if (chunk.toLowerCase() === searchLower && chunk !== searchChunk) {
576+
caseMismatchCount++
577+
}
578+
}
579+
580+
if (caseMismatchCount > 0) {
581+
caseMismatchInfo = `\n- 🔍 **Potential case mismatch**: Found ${caseMismatchCount} location(s) where content differs only by case`
582+
console.log(
583+
`[apply_diff] Case sensitivity issue detected: ${caseMismatchCount} potential matches with different casing`,
584+
)
585+
}
586+
543587
diffResults.push({
544588
success: false,
545-
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}`,
589+
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}`,
546590
})
547591
continue
548592
}

0 commit comments

Comments
 (0)