mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
* 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>
347 lines
7.8 KiB
TypeScript
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)
|
|
})
|
|
})
|