Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions src/core/diff/strategies/__tests__/multi-search-replace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
46 changes: 45 additions & 1 deletion src/core/diff/strategies/multi-file-search-replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The block that detects case differences (using toLowerCase and comparing word-by-word) is duplicated across strategies. Consider extracting this logic to a shared utility function to reduce duplication and ease future maintenance.

This comment was generated because it violated a code review rule: irule_tTqpIuNs8DV0QFGj.

// 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({
Expand Down Expand Up @@ -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(
Expand All @@ -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
}
Expand Down
46 changes: 45 additions & 1 deletion src/core/diff/strategies/multi-search-replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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
}
Expand Down