mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-12 23:01:21 +00:00
fix: prevent apply_diff from hanging on large/complex XML files
- Add performance optimizations to fuzzySearch function with early termination for exact matches - Add bounds checking and maximum iteration limit to prevent infinite loops - Add 30-second timeout mechanism to prevent indefinite hanging - Add integration tests to verify performance on large XML files Fixes #4852
This commit is contained in:
parent
2eb586b422
commit
2410003e4d
4 changed files with 716 additions and 6 deletions
|
|
@ -0,0 +1,274 @@
|
|||
import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace"
|
||||
import { MultiFileSearchReplaceDiffStrategy } from "../multi-file-search-replace"
|
||||
import { DiffResult } from "../../../../shared/tools"
|
||||
|
||||
describe("MultiSearchReplaceDiffStrategy Hanging Issue #4852", () => {
|
||||
describe("reproduce exact issue scenario", () => {
|
||||
let strategy: MultiSearchReplaceDiffStrategy
|
||||
let multiFileStrategy: MultiFileSearchReplaceDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
// Use exact settings that might cause the issue
|
||||
strategy = new MultiSearchReplaceDiffStrategy(1.0, 40) // Exact match, 40 line buffer
|
||||
multiFileStrategy = new MultiFileSearchReplaceDiffStrategy(1.0, 40)
|
||||
})
|
||||
|
||||
it("should handle the exact XML from issue #4852 without hanging", async () => {
|
||||
// This is the exact XML content from the issue
|
||||
const issueXmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<level1>
|
||||
<level2>
|
||||
<level3>
|
||||
<level4>
|
||||
<level5>
|
||||
<level6>
|
||||
<level7>
|
||||
<level8>
|
||||
<level9>
|
||||
<level10>
|
||||
<data>
|
||||
<item>Value 1</item>
|
||||
<item>Value 2</item>
|
||||
<item>Value 3</item>
|
||||
<nested>
|
||||
<subnested>
|
||||
<subsubnested>
|
||||
<deep>This is deeply nested content</deep>
|
||||
<deep>More content here</deep>
|
||||
<deep>Even more content</deep>
|
||||
</subsubnested>
|
||||
<subsubnested>
|
||||
<deep>Another deep element</deep>
|
||||
<deep>And another one</deep>
|
||||
</subsubnested>
|
||||
</subnested>
|
||||
<subnested>
|
||||
<subsubnested>
|
||||
<deep>More deeply nested</deep>
|
||||
<deep>Content continues</deep>
|
||||
</subsubnested>
|
||||
</subnested>
|
||||
</nested>
|
||||
<item>Value 4</item>
|
||||
<item>Value 5</item>
|
||||
<complexPattern>
|
||||
<!-- This pattern is designed to cause backtracking -->
|
||||
<a><b><c><d><e><f><g><h><i><j>
|
||||
<content>Complex nested structure</content>
|
||||
</j></i></h></g></f></e></d></c></b></a>
|
||||
<a><b><c><d><e><f><g><h><i><j>
|
||||
<content>Another complex structure</content>
|
||||
</j></i></h></g></f></e></d></c></b></a>
|
||||
</complexPattern>
|
||||
</data>
|
||||
<moreData>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<ambiguousContent>
|
||||
This content has multiple possible matches
|
||||
and can cause the regex to try many combinations
|
||||
especially when looking for specific patterns
|
||||
=======
|
||||
This looks like a separator but it's not
|
||||
>>>>>>>
|
||||
These patterns can confuse the regex
|
||||
<<<<<<<
|
||||
Causing it to backtrack extensively
|
||||
</ambiguousContent>
|
||||
</moreData>
|
||||
</level10>
|
||||
</level9>
|
||||
</level8>
|
||||
</level7>
|
||||
</level6>
|
||||
</level5>
|
||||
</level4>
|
||||
</level3>
|
||||
</level2>
|
||||
</level1>
|
||||
</root>`
|
||||
|
||||
// Test multiple concurrent edits as described in the issue
|
||||
const diffItems = [
|
||||
{
|
||||
content: `<<<<<<< SEARCH
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
=======
|
||||
<repeatingPattern>Pattern X</repeatingPattern>
|
||||
>>>>>>> REPLACE`,
|
||||
startLine: undefined,
|
||||
},
|
||||
{
|
||||
content: `<<<<<<< SEARCH
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
=======
|
||||
<repeatingPattern>Pattern Y</repeatingPattern>
|
||||
>>>>>>> REPLACE`,
|
||||
startLine: undefined,
|
||||
},
|
||||
{
|
||||
content: `<<<<<<< SEARCH
|
||||
<content>Complex nested structure</content>
|
||||
=======
|
||||
<content>Updated nested structure</content>
|
||||
>>>>>>> REPLACE`,
|
||||
startLine: undefined,
|
||||
},
|
||||
]
|
||||
|
||||
console.log("Starting multi-file diff application...")
|
||||
const startTime = Date.now()
|
||||
|
||||
// Apply all diffs using the multi-file strategy
|
||||
const result = await multiFileStrategy.applyDiff(issueXmlContent, diffItems)
|
||||
|
||||
const endTime = Date.now()
|
||||
const duration = endTime - startTime
|
||||
console.log(`Multi-file diff completed in ${duration}ms`)
|
||||
|
||||
// Check that it completed in reasonable time
|
||||
expect(duration).toBeLessThan(2000) // 2 seconds max
|
||||
|
||||
// Verify the result
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success && result.content) {
|
||||
// Count occurrences
|
||||
const patternACount = (result.content.match(/Pattern A/g) || []).length
|
||||
const patternBCount = (result.content.match(/Pattern B/g) || []).length
|
||||
const patternXCount = (result.content.match(/Pattern X/g) || []).length
|
||||
const patternYCount = (result.content.match(/Pattern Y/g) || []).length
|
||||
|
||||
// Should have replaced all occurrences
|
||||
expect(patternACount).toBe(0)
|
||||
expect(patternBCount).toBe(0)
|
||||
expect(patternXCount).toBe(3)
|
||||
expect(patternYCount).toBe(3)
|
||||
expect(result.content).toContain("Updated nested structure")
|
||||
}
|
||||
}, 10000) // 10 second timeout
|
||||
|
||||
it("should handle worst-case scenario with ambiguous patterns", async () => {
|
||||
// Create a pathological case with many ambiguous patterns
|
||||
const lines = []
|
||||
|
||||
// Add many similar lines that could match
|
||||
for (let i = 0; i < 200; i++) {
|
||||
lines.push(` <pattern>Similar content with slight variation ${i % 5}</pattern>`)
|
||||
}
|
||||
|
||||
// Add the target in the middle
|
||||
lines.splice(100, 0, ` <pattern>Target pattern to replace</pattern>`)
|
||||
|
||||
// Add more similar lines
|
||||
for (let i = 0; i < 200; i++) {
|
||||
lines.push(` <pattern>More similar content ${i % 5}</pattern>`)
|
||||
}
|
||||
|
||||
const pathologicalContent = lines.join("\n")
|
||||
|
||||
// Try to replace without line number hint (worst case)
|
||||
const diffContent = `<<<<<<< SEARCH
|
||||
<pattern>Target pattern to replace</pattern>
|
||||
=======
|
||||
<pattern>Successfully replaced target</pattern>
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
console.log("Starting pathological case test...")
|
||||
const startTime = Date.now()
|
||||
|
||||
const result = await strategy.applyDiff(pathologicalContent, diffContent)
|
||||
|
||||
const endTime = Date.now()
|
||||
const duration = endTime - startTime
|
||||
console.log(`Pathological case completed in ${duration}ms`)
|
||||
|
||||
// Should complete even in worst case
|
||||
expect(duration).toBeLessThan(5000) // 5 seconds max
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success && result.content) {
|
||||
expect(result.content).toContain("Successfully replaced target")
|
||||
expect(result.content).not.toContain("Target pattern to replace")
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
it("should handle extremely deep nesting efficiently", async () => {
|
||||
// Create extremely deep nesting that could cause stack issues
|
||||
const depth = 100
|
||||
let content = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
|
||||
// Open tags
|
||||
for (let i = 0; i < depth; i++) {
|
||||
content += `${" ".repeat(i)}<level${i}>\n`
|
||||
}
|
||||
|
||||
// Add content at deepest level
|
||||
content += `${" ".repeat(depth)}<data>Deep content to replace</data>\n`
|
||||
|
||||
// Close tags
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
content += `${" ".repeat(i)}</level${i}>\n`
|
||||
}
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH
|
||||
${" ".repeat(depth)}<data>Deep content to replace</data>
|
||||
=======
|
||||
${" ".repeat(depth)}<data>Replaced deep content</data>
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
console.log("Starting deep nesting test...")
|
||||
const startTime = Date.now()
|
||||
|
||||
const result = await strategy.applyDiff(content, diffContent)
|
||||
|
||||
const endTime = Date.now()
|
||||
const duration = endTime - startTime
|
||||
console.log(`Deep nesting test completed in ${duration}ms`)
|
||||
|
||||
expect(duration).toBeLessThan(1000) // Should be fast
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success && result.content) {
|
||||
expect(result.content).toContain("Replaced deep content")
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
it("should handle the ambiguous content markers that look like diff markers", async () => {
|
||||
const contentWithFakeMarkers = `<root>
|
||||
<data>
|
||||
<item>Normal content</item>
|
||||
<ambiguous>
|
||||
This has fake markers
|
||||
=======
|
||||
Not a real separator
|
||||
>>>>>>>
|
||||
Also not real
|
||||
<<<<<<<
|
||||
Just content
|
||||
</ambiguous>
|
||||
<target>Replace this content</target>
|
||||
</data>
|
||||
</root>`
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH
|
||||
<target>Replace this content</target>
|
||||
=======
|
||||
<target>Successfully replaced</target>
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = await strategy.applyDiff(contentWithFakeMarkers, diffContent)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success && result.content) {
|
||||
expect(result.content).toContain("Successfully replaced")
|
||||
// Should not have affected the fake markers
|
||||
expect(result.content).toContain("=======")
|
||||
expect(result.content).toContain(">>>>>>>")
|
||||
expect(result.content).toContain("<<<<<<<")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace"
|
||||
import { MultiFileSearchReplaceDiffStrategy } from "../multi-file-search-replace"
|
||||
import { DiffResult } from "../../../../shared/tools"
|
||||
|
||||
describe("MultiSearchReplaceDiffStrategy Performance", () => {
|
||||
describe("large XML file handling", () => {
|
||||
let strategy: MultiSearchReplaceDiffStrategy
|
||||
let multiFileStrategy: MultiFileSearchReplaceDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new MultiSearchReplaceDiffStrategy(1.0, 40) // Default settings
|
||||
multiFileStrategy = new MultiFileSearchReplaceDiffStrategy(1.0, 40)
|
||||
})
|
||||
|
||||
it("should handle large complex XML files without hanging", async () => {
|
||||
// Generate the large XML content from the issue
|
||||
const largeXmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<level1>
|
||||
<level2>
|
||||
<level3>
|
||||
<level4>
|
||||
<level5>
|
||||
<level6>
|
||||
<level7>
|
||||
<level8>
|
||||
<level9>
|
||||
<level10>
|
||||
<data>
|
||||
<item>Value 1</item>
|
||||
<item>Value 2</item>
|
||||
<item>Value 3</item>
|
||||
<nested>
|
||||
<subnested>
|
||||
<subsubnested>
|
||||
<deep>This is deeply nested content</deep>
|
||||
<deep>More content here</deep>
|
||||
<deep>Even more content</deep>
|
||||
</subsubnested>
|
||||
<subsubnested>
|
||||
<deep>Another deep element</deep>
|
||||
<deep>And another one</deep>
|
||||
</subsubnested>
|
||||
</subnested>
|
||||
<subnested>
|
||||
<subsubnested>
|
||||
<deep>More deeply nested</deep>
|
||||
<deep>Content continues</deep>
|
||||
</subsubnested>
|
||||
</subnested>
|
||||
</nested>
|
||||
<item>Value 4</item>
|
||||
<item>Value 5</item>
|
||||
<complexPattern>
|
||||
<!-- This pattern is designed to cause backtracking -->
|
||||
<a><b><c><d><e><f><g><h><i><j>
|
||||
<content>Complex nested structure</content>
|
||||
</j></i></h></g></f></e></d></c></b></a>
|
||||
<a><b><c><d><e><f><g><h><i><j>
|
||||
<content>Another complex structure</content>
|
||||
</j></i></h></g></f></e></d></c></b></a>
|
||||
</complexPattern>
|
||||
</data>
|
||||
<moreData>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<ambiguousContent>
|
||||
This content has multiple possible matches
|
||||
and can cause the regex to try many combinations
|
||||
especially when looking for specific patterns
|
||||
=======
|
||||
This looks like a separator but it's not
|
||||
>>>>>>>
|
||||
These patterns can confuse the regex
|
||||
<<<<<<<
|
||||
Causing it to backtrack extensively
|
||||
</ambiguousContent>
|
||||
</moreData>
|
||||
</level10>
|
||||
</level9>
|
||||
</level8>
|
||||
</level7>
|
||||
</level6>
|
||||
</level5>
|
||||
</level4>
|
||||
</level3>
|
||||
</level2>
|
||||
</level1>
|
||||
</root>`
|
||||
|
||||
// Create diff content to change Pattern A to Pattern X and Pattern B to Pattern Y
|
||||
const diffContent = `
|
||||
<<<<<<< SEARCH
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
=======
|
||||
<repeatingPattern>Pattern X</repeatingPattern>
|
||||
<repeatingPattern>Pattern X</repeatingPattern>
|
||||
<repeatingPattern>Pattern X</repeatingPattern>
|
||||
<repeatingPattern>Pattern Y</repeatingPattern>
|
||||
<repeatingPattern>Pattern Y</repeatingPattern>
|
||||
<repeatingPattern>Pattern Y</repeatingPattern>
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
<content>Complex nested structure</content>
|
||||
=======
|
||||
<content>Updated nested structure</content>
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
// Set a timeout to ensure the test doesn't hang indefinitely
|
||||
const startTime = Date.now()
|
||||
const timeout = 5000 // 5 seconds timeout
|
||||
|
||||
const resultPromise = strategy.applyDiff(largeXmlContent, diffContent)
|
||||
|
||||
// Use Promise.race to implement timeout
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Operation timed out")), timeout)
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await Promise.race([resultPromise, timeoutPromise])
|
||||
const endTime = Date.now()
|
||||
const duration = endTime - startTime
|
||||
|
||||
// Ensure the operation completed within reasonable time
|
||||
expect(duration).toBeLessThan(timeout)
|
||||
|
||||
// Verify the result
|
||||
const diffResult = result as DiffResult
|
||||
expect(diffResult).toBeDefined()
|
||||
expect(diffResult.success).toBe(true)
|
||||
if (diffResult.success && diffResult.content) {
|
||||
expect(diffResult.content).toContain("Pattern X")
|
||||
expect(diffResult.content).toContain("Pattern Y")
|
||||
expect(diffResult.content).toContain("Updated nested structure")
|
||||
expect(diffResult.content).not.toContain("Pattern A")
|
||||
expect(diffResult.content).not.toContain("Pattern B")
|
||||
expect(diffResult.content).not.toContain("Complex nested structure")
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "Operation timed out") {
|
||||
throw new Error("applyDiff operation timed out - this indicates the hanging issue")
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}, 10000) // Jest timeout of 10 seconds
|
||||
|
||||
it("should handle multiple simultaneous edits on large XML files", async () => {
|
||||
// Test the multi-file strategy with the same large XML content
|
||||
const largeXmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<level1>
|
||||
<level2>
|
||||
<level3>
|
||||
<level4>
|
||||
<level5>
|
||||
<level6>
|
||||
<level7>
|
||||
<level8>
|
||||
<level9>
|
||||
<level10>
|
||||
<data>
|
||||
<item>Value 1</item>
|
||||
<item>Value 2</item>
|
||||
<item>Value 3</item>
|
||||
<nested>
|
||||
<subnested>
|
||||
<subsubnested>
|
||||
<deep>This is deeply nested content</deep>
|
||||
<deep>More content here</deep>
|
||||
<deep>Even more content</deep>
|
||||
</subsubnested>
|
||||
</subnested>
|
||||
</nested>
|
||||
<item>Value 4</item>
|
||||
<item>Value 5</item>
|
||||
<complexPattern>
|
||||
<a><b><c><d><e><f><g><h><i><j>
|
||||
<content>Complex nested structure</content>
|
||||
</j></i></h></g></f></e></d></c></b></a>
|
||||
</complexPattern>
|
||||
</data>
|
||||
<moreData>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
</moreData>
|
||||
</level10>
|
||||
</level9>
|
||||
</level8>
|
||||
</level7>
|
||||
</level6>
|
||||
</level5>
|
||||
</level4>
|
||||
</level3>
|
||||
</level2>
|
||||
</level1>
|
||||
</root>`
|
||||
|
||||
// Create multiple diff items
|
||||
const diffItems = [
|
||||
{
|
||||
content: `<<<<<<< SEARCH
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
<repeatingPattern>Pattern A</repeatingPattern>
|
||||
=======
|
||||
<repeatingPattern>Pattern X</repeatingPattern>
|
||||
<repeatingPattern>Pattern X</repeatingPattern>
|
||||
<repeatingPattern>Pattern X</repeatingPattern>
|
||||
>>>>>>> REPLACE`,
|
||||
startLine: undefined,
|
||||
},
|
||||
{
|
||||
content: `<<<<<<< SEARCH
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
<repeatingPattern>Pattern B</repeatingPattern>
|
||||
=======
|
||||
<repeatingPattern>Pattern Y</repeatingPattern>
|
||||
<repeatingPattern>Pattern Y</repeatingPattern>
|
||||
<repeatingPattern>Pattern Y</repeatingPattern>
|
||||
>>>>>>> REPLACE`,
|
||||
startLine: undefined,
|
||||
},
|
||||
{
|
||||
content: `<<<<<<< SEARCH
|
||||
<content>Complex nested structure</content>
|
||||
=======
|
||||
<content>Updated nested structure</content>
|
||||
>>>>>>> REPLACE`,
|
||||
startLine: undefined,
|
||||
},
|
||||
]
|
||||
|
||||
const startTime = Date.now()
|
||||
const timeout = 5000 // 5 seconds timeout
|
||||
|
||||
const resultPromise = multiFileStrategy.applyDiff(largeXmlContent, diffItems)
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Operation timed out")), timeout)
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await Promise.race([resultPromise, timeoutPromise])
|
||||
const endTime = Date.now()
|
||||
const duration = endTime - startTime
|
||||
|
||||
expect(duration).toBeLessThan(timeout)
|
||||
const diffResult = result as DiffResult
|
||||
expect(diffResult).toBeDefined()
|
||||
expect(diffResult.success).toBe(true)
|
||||
if (diffResult.success && diffResult.content) {
|
||||
expect(diffResult.content).toContain("Pattern X")
|
||||
expect(diffResult.content).toContain("Pattern Y")
|
||||
expect(diffResult.content).toContain("Updated nested structure")
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "Operation timed out") {
|
||||
throw new Error("Multi-file applyDiff operation timed out - this indicates the hanging issue")
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
it("should handle pathological cases with many similar patterns", async () => {
|
||||
// Create content with many similar patterns that could cause excessive backtracking
|
||||
const lines = []
|
||||
for (let i = 0; i < 100; i++) {
|
||||
lines.push(` <pattern>Similar content ${i % 10}</pattern>`)
|
||||
}
|
||||
const pathologicalContent = lines.join("\n")
|
||||
|
||||
// Try to replace a pattern in the middle
|
||||
const diffContent = `
|
||||
<<<<<<< SEARCH
|
||||
<pattern>Similar content 5</pattern>
|
||||
=======
|
||||
<pattern>Updated content 5</pattern>
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const startTime = Date.now()
|
||||
const result = await strategy.applyDiff(pathologicalContent, diffContent)
|
||||
const endTime = Date.now()
|
||||
const duration = endTime - startTime
|
||||
|
||||
// Should complete quickly even with many similar patterns
|
||||
expect(duration).toBeLessThan(1000) // 1 second max
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success && result.content) {
|
||||
// Should update exactly one occurrence
|
||||
const updatedCount = (result.content.match(/Updated content 5/g) || []).length
|
||||
expect(updatedCount).toBe(1)
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle deeply nested content with line number hints efficiently", async () => {
|
||||
const deeplyNestedXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
${Array(50)
|
||||
.fill(0)
|
||||
.map((_, i) => ` ${" ".repeat(i)}<level${i}>`)
|
||||
.join("\n")}
|
||||
${Array(50)
|
||||
.fill(0)
|
||||
.map((_, i) => ` ${" ".repeat(49 - i)}<data>Content at level ${49 - i}</data>`)
|
||||
.join("\n")}
|
||||
${Array(50)
|
||||
.fill(0)
|
||||
.map((_, i) => ` ${" ".repeat(49 - i)}</level${49 - i}>`)
|
||||
.join("\n")}
|
||||
</root>`
|
||||
|
||||
// Try to replace content at a specific level with line number hint
|
||||
const diffContent = `
|
||||
<<<<<<< SEARCH
|
||||
:start_line:30
|
||||
-------
|
||||
<data>Content at level 20</data>
|
||||
=======
|
||||
<data>Updated content at level 20</data>
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const startTime = Date.now()
|
||||
const result = await strategy.applyDiff(deeplyNestedXml, diffContent)
|
||||
const endTime = Date.now()
|
||||
const duration = endTime - startTime
|
||||
|
||||
// Should be fast with line number hint
|
||||
expect(duration).toBeLessThan(500) // 500ms max
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success && result.content) {
|
||||
expect(result.content).toContain("Updated content at level 20")
|
||||
expect(result.content).not.toContain("<data>Content at level 20</data>")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -40,34 +40,61 @@ function fuzzySearch(lines: string[], searchChunk: string, startIndex: number, e
|
|||
|
||||
const searchLen = searchChunk.split(/\r?\n/).length
|
||||
|
||||
// Early return if search range is invalid
|
||||
if (startIndex < 0 || endIndex > lines.length || startIndex >= endIndex || searchLen > endIndex - startIndex) {
|
||||
return { bestScore, bestMatchIndex, bestMatchContent }
|
||||
}
|
||||
|
||||
// Middle-out from the midpoint
|
||||
const midPoint = Math.floor((startIndex + endIndex) / 2)
|
||||
let leftIndex = midPoint
|
||||
let rightIndex = midPoint + 1
|
||||
|
||||
while (leftIndex >= startIndex || rightIndex <= endIndex - searchLen) {
|
||||
if (leftIndex >= startIndex) {
|
||||
// Add a maximum iteration count to prevent infinite loops
|
||||
const maxIterations = endIndex - startIndex
|
||||
let iterations = 0
|
||||
|
||||
while ((leftIndex >= startIndex || rightIndex <= endIndex - searchLen) && iterations < maxIterations) {
|
||||
iterations++
|
||||
|
||||
// Check left side
|
||||
if (leftIndex >= startIndex && leftIndex + searchLen <= endIndex) {
|
||||
const originalChunk = lines.slice(leftIndex, leftIndex + searchLen).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
|
||||
// Early termination if we find an exact match
|
||||
if (similarity === 1.0) {
|
||||
return { bestScore: similarity, bestMatchIndex: leftIndex, bestMatchContent: originalChunk }
|
||||
}
|
||||
|
||||
if (similarity > bestScore) {
|
||||
bestScore = similarity
|
||||
bestMatchIndex = leftIndex
|
||||
bestMatchContent = originalChunk
|
||||
}
|
||||
leftIndex--
|
||||
} else {
|
||||
leftIndex = startIndex - 1 // Force it out of bounds
|
||||
}
|
||||
|
||||
if (rightIndex <= endIndex - searchLen) {
|
||||
// Check right side
|
||||
if (rightIndex <= endIndex - searchLen && rightIndex >= startIndex) {
|
||||
const originalChunk = lines.slice(rightIndex, rightIndex + searchLen).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
|
||||
// Early termination if we find an exact match
|
||||
if (similarity === 1.0) {
|
||||
return { bestScore: similarity, bestMatchIndex: rightIndex, bestMatchContent: originalChunk }
|
||||
}
|
||||
|
||||
if (similarity > bestScore) {
|
||||
bestScore = similarity
|
||||
bestMatchIndex = rightIndex
|
||||
bestMatchContent = originalChunk
|
||||
}
|
||||
rightIndex++
|
||||
} else {
|
||||
rightIndex = endIndex + 1 // Force it out of bounds
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -446,6 +473,10 @@ Each file requires its own path, start_line, and diff elements.
|
|||
diffContent: string,
|
||||
_paramStartLine?: number,
|
||||
): Promise<DiffResult> {
|
||||
// Add a timeout mechanism to prevent indefinite hanging
|
||||
const DIFF_TIMEOUT_MS = 30000 // 30 seconds timeout
|
||||
const startTime = Date.now()
|
||||
|
||||
const validseq = this.validateMarkerSequencing(diffContent)
|
||||
if (!validseq.success) {
|
||||
return {
|
||||
|
|
@ -494,6 +525,15 @@ Each file requires its own path, start_line, and diff elements.
|
|||
.sort((a, b) => a.startLine - b.startLine)
|
||||
|
||||
for (const replacement of replacements) {
|
||||
// Check for timeout
|
||||
if (Date.now() - startTime > DIFF_TIMEOUT_MS) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Operation timed out after ${DIFF_TIMEOUT_MS / 1000} seconds. This may indicate the file is too large or complex for the current search pattern.`,
|
||||
failParts: diffResults,
|
||||
}
|
||||
}
|
||||
|
||||
let { searchContent, replaceContent } = replacement
|
||||
let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,32 +42,61 @@ function fuzzySearch(lines: string[], searchChunk: string, startIndex: number, e
|
|||
let bestMatchContent = ""
|
||||
const searchLen = searchChunk.split(/\r?\n/).length
|
||||
|
||||
// Early return if search range is invalid
|
||||
if (startIndex < 0 || endIndex > lines.length || startIndex >= endIndex || searchLen > endIndex - startIndex) {
|
||||
return { bestScore, bestMatchIndex, bestMatchContent }
|
||||
}
|
||||
|
||||
// Middle-out from the midpoint
|
||||
const midPoint = Math.floor((startIndex + endIndex) / 2)
|
||||
let leftIndex = midPoint
|
||||
let rightIndex = midPoint + 1
|
||||
|
||||
while (leftIndex >= startIndex || rightIndex <= endIndex - searchLen) {
|
||||
if (leftIndex >= startIndex) {
|
||||
// Add a maximum iteration count to prevent infinite loops
|
||||
const maxIterations = endIndex - startIndex
|
||||
let iterations = 0
|
||||
|
||||
while ((leftIndex >= startIndex || rightIndex <= endIndex - searchLen) && iterations < maxIterations) {
|
||||
iterations++
|
||||
|
||||
// Check left side
|
||||
if (leftIndex >= startIndex && leftIndex + searchLen <= endIndex) {
|
||||
const originalChunk = lines.slice(leftIndex, leftIndex + searchLen).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
|
||||
// Early termination if we find an exact match
|
||||
if (similarity === 1.0) {
|
||||
return { bestScore: similarity, bestMatchIndex: leftIndex, bestMatchContent: originalChunk }
|
||||
}
|
||||
|
||||
if (similarity > bestScore) {
|
||||
bestScore = similarity
|
||||
bestMatchIndex = leftIndex
|
||||
bestMatchContent = originalChunk
|
||||
}
|
||||
leftIndex--
|
||||
} else {
|
||||
leftIndex = startIndex - 1 // Force it out of bounds
|
||||
}
|
||||
|
||||
if (rightIndex <= endIndex - searchLen) {
|
||||
// Check right side
|
||||
if (rightIndex <= endIndex - searchLen && rightIndex >= startIndex) {
|
||||
const originalChunk = lines.slice(rightIndex, rightIndex + searchLen).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
|
||||
// Early termination if we find an exact match
|
||||
if (similarity === 1.0) {
|
||||
return { bestScore: similarity, bestMatchIndex: rightIndex, bestMatchContent: originalChunk }
|
||||
}
|
||||
|
||||
if (similarity > bestScore) {
|
||||
bestScore = similarity
|
||||
bestMatchIndex = rightIndex
|
||||
bestMatchContent = originalChunk
|
||||
}
|
||||
rightIndex++
|
||||
} else {
|
||||
rightIndex = endIndex + 1 // Force it out of bounds
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,6 +366,10 @@ Only use a single line of '=======' between search and replacement content, beca
|
|||
_paramStartLine?: number,
|
||||
_paramEndLine?: number,
|
||||
): Promise<DiffResult> {
|
||||
// Add a timeout mechanism to prevent indefinite hanging
|
||||
const DIFF_TIMEOUT_MS = 30000 // 30 seconds timeout
|
||||
const startTime = Date.now()
|
||||
|
||||
const validseq = this.validateMarkerSequencing(diffContent)
|
||||
if (!validseq.success) {
|
||||
return {
|
||||
|
|
@ -403,6 +436,15 @@ Only use a single line of '=======' between search and replacement content, beca
|
|||
.sort((a, b) => a.startLine - b.startLine)
|
||||
|
||||
for (const replacement of replacements) {
|
||||
// Check for timeout
|
||||
if (Date.now() - startTime > DIFF_TIMEOUT_MS) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Operation timed out after ${DIFF_TIMEOUT_MS / 1000} seconds. This may indicate the file is too large or complex for the current search pattern.`,
|
||||
failParts: diffResults,
|
||||
}
|
||||
}
|
||||
|
||||
let { searchContent, replaceContent } = replacement
|
||||
let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue