Files
cline/src/core/assistant-message/diff.test.ts
T
8d133d9031 supporting legacy search and replace blocks too (#4264)
* supporting legacy search and replace blocks too

* changeset

* Handle legacy search/replace chars for first/last partial lines

* error recovery + fixing

* throwing error if malformed search block - previously this would cause entire file to be deleted

* Update cyan-books-cry.md

* fixing broken test

---------

Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-06-18 01:14:28 +02:00

347 lines
7.8 KiB
TypeScript

import { constructNewFileContent as cnfc } from "./diff"
import { describe, it } from "mocha"
import { expect } from "chai"
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
return cnfc(diffContent, originalContent, isFinal, "v2")
}
describe("constructNewFileContent", () => {
const testCases = [
{
name: "empty file",
original: "",
diff: `------- SEARCH
=======
new content
+++++++ REPLACE`,
expected: "new content\n",
isFinal: true,
},
{
name: "malformed search - mixed symbols",
original: "line1\nline2\nline3",
diff: `<<-- SEARCH
line2
=======
replaced
+++++++ REPLACE`,
shouldThrow: true,
},
{
name: "malformed search - insufficient dashes",
original: "line1\nline2\nline3",
diff: `-- SEARCH
line2
=======
replaced
+++++++ REPLACE`,
shouldThrow: true,
},
{
name: "malformed search - missing space",
original: "line1\nline2\nline3",
diff: `-------SEARCH
line2
=======
replaced
+++++++ REPLACE`,
shouldThrow: true,
},
{
name: "exact match replacement",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
=======
replaced
+++++++ REPLACE`,
expected: "line1\nreplaced\nline3",
isFinal: true,
},
{
name: "line-trimmed match replacement",
original: "line1\n line2 \nline3",
diff: `------- SEARCH
line2
=======
replaced
+++++++ REPLACE`,
expected: "line1\nreplaced\nline3",
isFinal: true,
},
{
name: "block anchor match replacement",
original: "line1\nstart\nmiddle\nend\nline5",
diff: `------- SEARCH
start
middle
end
=======
replaced
+++++++ REPLACE`,
expected: "line1\nreplaced\nline5",
isFinal: true,
},
{
name: "incremental processing",
original: "line1\nline2\nline3",
diff: [
`------- SEARCH
line2
=======`,
"replaced\n",
"+++++++ REPLACE",
].join("\n"),
expected: "line1\nreplaced\n\nline3",
isFinal: true,
},
{
name: "final chunk with remaining content",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
=======
replaced
+++++++ REPLACE`,
expected: "line1\nreplaced\nline3",
isFinal: true,
},
{
name: "multiple ordered replacements",
original: "First\nSecond\nThird\nFourth",
diff: `------- SEARCH
First
=======
1st
+++++++ REPLACE
------- SEARCH
Third
=======
3rd
+++++++ REPLACE`,
expected: "1st\nSecond\n3rd\nFourth",
isFinal: true,
},
{
name: "replace then delete",
original: "line1\nline2\nline3\nline4",
diff: `------- SEARCH
line2
=======
replaced
+++++++ REPLACE
------- SEARCH
line4
=======
+++++++ REPLACE`,
expected: "line1\nreplaced\nline3\n",
isFinal: true,
},
{
name: "delete then replace",
original: "line1\nline2\nline3\nline4",
diff: `------- SEARCH
line1
=======
+++++++ REPLACE
------- SEARCH
line3
=======
replaced
+++++++ REPLACE`,
expected: "line2\nreplaced\nline4",
isFinal: true,
},
]
//.filter(({name}) => name === "multiple ordered replacements")
//.filter(({name}) => name === "delete then replace")
testCases.forEach(({ name, original, diff, expected, isFinal, shouldThrow }) => {
it(`should handle ${name} case correctly`, async () => {
if (shouldThrow) {
try {
await cnfc(diff, original, isFinal ?? true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
try {
await cnfc2(diff, original, isFinal ?? true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
} else {
const result1 = await cnfc(diff, original, isFinal ?? true)
const result2 = await cnfc2(diff, original, isFinal ?? true)
const equal = result1 === result2
const equal2 = result1 === expected
// Verify both implementations produce same result
expect(result1).to.equal(result2)
// Verify result matches expected
expect(result1).to.equal(expected)
}
})
})
it("should throw error when no match found", async () => {
const original = "line1\nline2\nline3"
const diff = `------- SEARCH
non-existent
=======
replaced
+++++++ REPLACE`
try {
await cnfc(diff, original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
try {
await cnfc2(diff, original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
})
it("should handle missing final REPLACE marker when isFinal is true", async () => {
const original = "line1\nline2\nline3"
const diff = `------- SEARCH
line2
=======
replaced`
// Note: missing +++++++ REPLACE marker
const result1 = await cnfc(diff, original, true) // isFinal = true
// Should still work and replace line2 with "replaced"
const expected = "line1\nreplaced\nline3"
expect(result1).to.equal(expected)
})
it("should handle missing final REPLACE marker with multiple lines of replacement", async () => {
const original = "function test() {\n\tconst a = 1;\n\treturn a;\n}"
const diff = `------- SEARCH
const a = 1;
return a;
=======
const a = 42;
console.log('updated');
return a;`
// Note: missing +++++++ REPLACE marker
const result1 = await cnfc(diff, original, true) // isFinal = true
const expected = "function test() {\n\tconst a = 42;\n\tconsole.log('updated');\n\treturn a;\n}"
expect(result1).to.equal(expected)
})
// it("should NOT process incomplete replacement when isFinal is false", async () => {
// const original = "line1\nline2\nline3"
// const diff = `------- SEARCH
// line2
// =======
// replaced`
// // Note: missing +++++++ REPLACE marker AND isFinal = false
// const result1 = await cnfc(diff, original, false) // isFinal = false
// // Should not make any changes since the block is incomplete
// const expected = "line1\nline2\nline3"
// expect(result1).to.equal(expected)
// })
})
// Test cases for out-of-order search/replace blocks
describe("Diff Format Out of Order Cases", () => {
it("should handle out-of-order replacements with different positions", async () => {
const isFinal = true
const original = "first\nsecond\nthird\nfourth\n"
const diff = `------- SEARCH
fourth
=======
new fourth
+++++++ REPLACE
------- SEARCH
second
=======
new second
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "first\nnew second\nthird\nnew fourth\n"
expect(result1).to.equal(expectedResult)
})
it("should handle multiple out-of-order replacements", async () => {
const isFinal = true
const original = "one\ntwo\nthree\nfour\nfive\n"
const diff = `------- SEARCH
four
=======
fourth
+++++++ REPLACE
------- SEARCH
two
=======
second
+++++++ REPLACE
------- SEARCH
five
=======
fifth
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "one\nsecond\nthree\nfourth\nfifth\n"
expect(result1).to.equal(expectedResult)
})
it("should handle out-of-order replacements with indentation", async () => {
const isFinal = true
const original = "function test() {\n\tconst a = 1;\n\tconst b = 2;\n\tconst c = 3;\n\n}"
const diff = `------- SEARCH
const c = 3;
=======
const c = 30;
+++++++ REPLACE
------- SEARCH
const a = 1;
=======
const a = 10;
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "function test() {\n\tconst a = 10;\n\tconst b = 2;\n\tconst c = 30;\n\n}"
expect(result1).to.equal(expectedResult)
})
it("should handle out-of-order replacements with empty lines", async () => {
const isFinal = true
const original = "header\n\nbody\n\nfooter\n"
const diff = `------- SEARCH
footer
=======
new footer
+++++++ REPLACE
------- SEARCH
body
=======
new body content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "header\nnew body content\nnew footer\n"
expect(result1).to.equal(expectedResult)
})
})