2026-02-16 16:32:33 +09:00
|
|
|
import { describe, expect, it } from "bun:test"
|
2026-02-20 11:02:07 +09:00
|
|
|
import { applyHashlineEdits, applyInsertAfter, applyReplace, applyReplaceLines, applySetLine } from "./edit-operations"
|
2026-02-19 18:19:16 +09:00
|
|
|
import { computeLineHash } from "./hash-computation"
|
2026-02-20 11:02:07 +09:00
|
|
|
import type { HashlineEdit } from "./types"
|
2026-02-16 16:32:33 +09:00
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
function anchorFor(lines: string[], line: number): string {
|
|
|
|
|
return `${line}#${computeLineHash(line, lines[line - 1])}`
|
|
|
|
|
}
|
2026-02-19 18:19:16 +09:00
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
describe("hashline edit operations", () => {
|
|
|
|
|
it("applies set_line with LINE#ID anchor", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
|
|
|
|
const lines = ["line 1", "line 2", "line 3"]
|
|
|
|
|
|
|
|
|
|
//#when
|
2026-02-20 11:02:07 +09:00
|
|
|
const result = applySetLine(lines, anchorFor(lines, 2), "new line 2")
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toEqual(["line 1", "new line 2", "line 3"])
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
it("applies replace_lines with LINE#ID anchors", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
2026-02-20 11:02:07 +09:00
|
|
|
const lines = ["line 1", "line 2", "line 3", "line 4"]
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#when
|
2026-02-20 11:02:07 +09:00
|
|
|
const result = applyReplaceLines(lines, anchorFor(lines, 2), anchorFor(lines, 3), "replaced")
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#then
|
2026-02-20 11:02:07 +09:00
|
|
|
expect(result).toEqual(["line 1", "replaced", "line 4"])
|
2026-02-16 16:32:33 +09:00
|
|
|
})
|
|
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
it("applies insert_after with LINE#ID anchor", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
|
|
|
|
const lines = ["line 1", "line 2", "line 3"]
|
|
|
|
|
|
|
|
|
|
//#when
|
2026-02-20 11:02:07 +09:00
|
|
|
const result = applyInsertAfter(lines, anchorFor(lines, 2), "inserted")
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toEqual(["line 1", "line 2", "inserted", "line 3"])
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-22 03:38:04 +09:00
|
|
|
it("applies insert_before with LINE#ID anchor", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const lines = ["line 1", "line 2", "line 3"]
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = applyHashlineEdits(
|
|
|
|
|
lines.join("\n"),
|
|
|
|
|
[{ type: "insert_before", line: anchorFor(lines, 2), text: "before 2" }]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toEqual("line 1\nbefore 2\nline 2\nline 3")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("applies insert_between with dual anchors", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const lines = ["line 1", "line 2", "line 3"]
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = applyHashlineEdits(
|
|
|
|
|
lines.join("\n"),
|
|
|
|
|
[{
|
|
|
|
|
type: "insert_between",
|
|
|
|
|
after_line: anchorFor(lines, 1),
|
|
|
|
|
before_line: anchorFor(lines, 2),
|
|
|
|
|
text: ["between"],
|
|
|
|
|
}]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toEqual("line 1\nbetween\nline 2\nline 3")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("throws when insert_after receives empty text array", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const lines = ["line 1", "line 2"]
|
|
|
|
|
|
|
|
|
|
//#when / #then
|
|
|
|
|
expect(() => applyInsertAfter(lines, anchorFor(lines, 1), [])).toThrow(/non-empty/i)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("throws when insert_before receives empty text array", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const lines = ["line 1", "line 2"]
|
|
|
|
|
|
|
|
|
|
//#when / #then
|
|
|
|
|
expect(() =>
|
|
|
|
|
applyHashlineEdits(lines.join("\n"), [{ type: "insert_before", line: anchorFor(lines, 1), text: [] }])
|
|
|
|
|
).toThrow(/non-empty/i)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("throws when insert_between receives empty text array", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const lines = ["line 1", "line 2"]
|
|
|
|
|
|
|
|
|
|
//#when / #then
|
|
|
|
|
expect(() =>
|
|
|
|
|
applyHashlineEdits(
|
|
|
|
|
lines.join("\n"),
|
|
|
|
|
[{
|
|
|
|
|
type: "insert_between",
|
|
|
|
|
after_line: anchorFor(lines, 1),
|
|
|
|
|
before_line: anchorFor(lines, 2),
|
|
|
|
|
text: [],
|
|
|
|
|
}]
|
|
|
|
|
)
|
|
|
|
|
).toThrow(/non-empty/i)
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
it("applies replace operation", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
2026-02-20 11:02:07 +09:00
|
|
|
const content = "hello world foo"
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#when
|
2026-02-20 11:02:07 +09:00
|
|
|
const result = applyReplace(content, "world", "universe")
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#then
|
2026-02-20 11:02:07 +09:00
|
|
|
expect(result).toEqual("hello universe foo")
|
2026-02-16 16:32:33 +09:00
|
|
|
})
|
|
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
it("applies mixed edits in one pass", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
|
|
|
|
const content = "line 1\nline 2\nline 3"
|
2026-02-20 11:02:07 +09:00
|
|
|
const lines = content.split("\n")
|
|
|
|
|
const edits: HashlineEdit[] = [
|
|
|
|
|
{ type: "insert_after", line: anchorFor(lines, 1), text: "inserted" },
|
|
|
|
|
{ type: "set_line", line: anchorFor(lines, 3), text: "modified" },
|
2026-02-16 16:32:33 +09:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = applyHashlineEdits(content, edits)
|
|
|
|
|
|
|
|
|
|
//#then
|
2026-02-20 11:02:07 +09:00
|
|
|
expect(result).toEqual("line 1\ninserted\nline 2\nmodified")
|
2026-02-16 16:32:33 +09:00
|
|
|
})
|
|
|
|
|
|
2026-02-22 03:38:04 +09:00
|
|
|
it("deduplicates identical insert edits in one pass", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const content = "line 1\nline 2"
|
|
|
|
|
const lines = content.split("\n")
|
|
|
|
|
const edits: HashlineEdit[] = [
|
|
|
|
|
{ type: "insert_after", line: anchorFor(lines, 1), text: "inserted" },
|
|
|
|
|
{ type: "insert_after", line: anchorFor(lines, 1), text: "inserted" },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = applyHashlineEdits(content, edits)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result).toEqual("line 1\ninserted\nline 2")
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
it("keeps literal backslash-n in plain string text", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
2026-02-20 11:02:07 +09:00
|
|
|
const lines = ["line 1", "line 2", "line 3"]
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#when
|
2026-02-20 11:02:07 +09:00
|
|
|
const result = applySetLine(lines, anchorFor(lines, 2), "join(\\n)")
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#then
|
2026-02-20 11:02:07 +09:00
|
|
|
expect(result).toEqual(["line 1", "join(\\n)", "line 3"])
|
2026-02-16 16:32:33 +09:00
|
|
|
})
|
|
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
it("strips copied hashline prefixes from multiline text", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
2026-02-20 11:02:07 +09:00
|
|
|
const lines = ["line 1", "line 2", "line 3"]
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#when
|
2026-02-20 11:02:07 +09:00
|
|
|
const result = applySetLine(lines, anchorFor(lines, 2), "1#VK:first\n2#NP:second")
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#then
|
2026-02-20 11:02:07 +09:00
|
|
|
expect(result).toEqual(["line 1", "first", "second", "line 3"])
|
2026-02-16 16:32:33 +09:00
|
|
|
})
|
|
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
it("autocorrects anchor echo for insert_after payload", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
2026-02-20 11:02:07 +09:00
|
|
|
const lines = ["line 1", "line 2"]
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#when
|
2026-02-20 11:02:07 +09:00
|
|
|
const result = applyInsertAfter(lines, anchorFor(lines, 1), ["line 1", "inserted"])
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#then
|
2026-02-20 11:02:07 +09:00
|
|
|
expect(result).toEqual(["line 1", "inserted", "line 2"])
|
2026-02-16 16:32:33 +09:00
|
|
|
})
|
|
|
|
|
|
2026-02-22 03:54:31 +09:00
|
|
|
it("throws when insert_after payload only repeats anchor line", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const lines = ["line 1", "line 2"]
|
|
|
|
|
|
|
|
|
|
//#when / #then
|
|
|
|
|
expect(() => applyInsertAfter(lines, anchorFor(lines, 1), ["line 1"])).toThrow(/non-empty/i)
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
it("restores indentation for paired single-line replacement", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
2026-02-20 11:02:07 +09:00
|
|
|
const lines = ["if (x) {", " return 1", "}"]
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#when
|
2026-02-20 11:02:07 +09:00
|
|
|
const result = applySetLine(lines, anchorFor(lines, 2), "return 2")
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#then
|
2026-02-20 11:02:07 +09:00
|
|
|
expect(result).toEqual(["if (x) {", " return 2", "}"])
|
2026-02-16 16:32:33 +09:00
|
|
|
})
|
|
|
|
|
|
2026-02-20 11:02:07 +09:00
|
|
|
it("strips boundary echo around replace_lines content", () => {
|
2026-02-16 16:32:33 +09:00
|
|
|
//#given
|
2026-02-20 11:02:07 +09:00
|
|
|
const lines = ["before", "old 1", "old 2", "after"]
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#when
|
2026-02-20 11:02:07 +09:00
|
|
|
const result = applyReplaceLines(
|
|
|
|
|
lines,
|
|
|
|
|
anchorFor(lines, 2),
|
|
|
|
|
anchorFor(lines, 3),
|
|
|
|
|
["before", "new 1", "new 2", "after"]
|
|
|
|
|
)
|
2026-02-16 16:32:33 +09:00
|
|
|
|
|
|
|
|
//#then
|
2026-02-20 11:02:07 +09:00
|
|
|
expect(result).toEqual(["before", "new 1", "new 2", "after"])
|
2026-02-16 16:32:33 +09:00
|
|
|
})
|
2026-02-20 17:47:37 +09:00
|
|
|
|
2026-02-22 03:54:31 +09:00
|
|
|
it("throws when insert_between payload contains only boundary echoes", () => {
|
|
|
|
|
//#given
|
|
|
|
|
const lines = ["line 1", "line 2", "line 3"]
|
|
|
|
|
|
|
|
|
|
//#when / #then
|
|
|
|
|
expect(() =>
|
|
|
|
|
applyHashlineEdits(lines.join("\n"), [
|
|
|
|
|
{
|
|
|
|
|
type: "insert_between",
|
|
|
|
|
after_line: anchorFor(lines, 1),
|
|
|
|
|
before_line: anchorFor(lines, 2),
|
|
|
|
|
text: ["line 1", "line 2"],
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
).toThrow(/non-empty/i)
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-21 02:16:14 +09:00
|
|
|
it("restores indentation for first replace_lines entry", () => {
|
2026-02-20 17:47:37 +09:00
|
|
|
//#given
|
|
|
|
|
const lines = ["if (x) {", " return 1", " return 2", "}"]
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = applyReplaceLines(lines, anchorFor(lines, 2), anchorFor(lines, 3), ["return 3", "return 4"])
|
|
|
|
|
|
|
|
|
|
//#then
|
2026-02-21 02:16:14 +09:00
|
|
|
expect(result).toEqual(["if (x) {", " return 3", "return 4", "}"])
|
2026-02-20 17:47:37 +09:00
|
|
|
})
|
2026-02-16 16:32:33 +09:00
|
|
|
})
|