import { describe, it, expect } from "bun:test"; import { parseDiff, pairLines } from "../lib/diff"; const SIMPLE_DIFF = `diff --git a/src/foo.ts b/src/foo.ts index abc..def 100644 --- a/src/foo.ts +++ b/src/foo.ts @@ -1,5 +1,5 @@ import { bar } from './bar'; -const x = 1; +const x = 2; export default x; `; const MULTIFILE_DIFF = `diff --git a/a.ts b/a.ts index 000..111 100644 --- a/a.ts +++ b/a.ts @@ -1,2 +1,3 @@ line1 +line2 line3 diff --git a/b.ts b/b.ts index 222..333 100644 --- a/b.ts +++ b/b.ts @@ -1,2 +1,1 @@ hello -world `; const BINARY_DIFF = `diff --git a/image.png b/image.png index abc..def 100644 Binary files a/image.png and b/image.png differ `; const WS_DIFF = `diff --git a/x.ts b/x.ts index 000..111 100644 --- a/x.ts +++ b/x.ts @@ -1,3 +1,3 @@ ctx - foo + foo ctx `; describe("parseDiff", () => { it("parses a single-file diff with one del and one add", () => { const files = parseDiff(SIMPLE_DIFF); expect(files.length).toBe(1); expect(files[0].path).toBe("src/foo.ts"); expect(files[0].additions).toBe(1); expect(files[0].deletions).toBe(1); expect(files[0].isBinary).toBe(false); }); it("assigns correct line numbers", () => { const files = parseDiff(SIMPLE_DIFF); const lines = files[0].lines; const hunk = lines.find((l) => l.type === "hunk"); expect(hunk).toBeTruthy(); const ctx1 = lines.find((l) => l.type === "ctx"); expect(ctx1?.oldLine).toBe(1); expect(ctx1?.newLine).toBe(1); const del = lines.find((l) => l.type === "del"); expect(del?.oldLine).toBe(2); expect(del?.newLine).toBeNull(); const add = lines.find((l) => l.type === "add"); expect(add?.oldLine).toBeNull(); expect(add?.newLine).toBe(2); }); it("parses multiple files", () => { const files = parseDiff(MULTIFILE_DIFF); expect(files.length).toBe(2); expect(files[0].path).toBe("a.ts"); expect(files[1].path).toBe("b.ts"); expect(files[0].additions).toBe(1); expect(files[0].deletions).toBe(0); expect(files[1].additions).toBe(0); expect(files[1].deletions).toBe(1); }); it("marks binary files", () => { const files = parseDiff(BINARY_DIFF); expect(files.length).toBe(1); expect(files[0].isBinary).toBe(true); expect(files[0].lines.length).toBe(0); }); it("detects whitespace-only changes", () => { const files = parseDiff(WS_DIFF); const lines = files[0].lines; const del = lines.find((l) => l.type === "del"); const add = lines.find((l) => l.type === "add"); expect(del?.wsOnly).toBe(true); expect(add?.wsOnly).toBe(true); }); it("does NOT mark non-whitespace changes as wsOnly", () => { const files = parseDiff(SIMPLE_DIFF); const del = files[0].lines.find((l) => l.type === "del"); const add = files[0].lines.find((l) => l.type === "add"); expect(del?.wsOnly).toBeFalsy(); expect(add?.wsOnly).toBeFalsy(); }); it("returns empty array for empty input", () => { expect(parseDiff("")).toEqual([]); }); it("handles CRLF line endings", () => { const crlf = SIMPLE_DIFF.replace(/\n/g, "\r\n"); const files = parseDiff(crlf); expect(files.length).toBe(1); expect(files[0].additions).toBe(1); }); it("strips the +/-/space prefix from content", () => { const files = parseDiff(SIMPLE_DIFF); const add = files[0].lines.find((l) => l.type === "add")!; const del = files[0].lines.find((l) => l.type === "del")!; expect(add.content).not.toMatch(/^\+/); expect(del.content).not.toMatch(/^-/); expect(add.content).toBe("const x = 2;"); expect(del.content).toBe("const x = 1;"); }); }); describe("pairLines", () => { it("pairs a del/add block into a single row", () => { const files = parseDiff(SIMPLE_DIFF); const rows = pairLines(files[0].lines); const paired = rows.find((r) => r.left?.type === "del" && r.right?.type === "add"); expect(paired).toBeTruthy(); }); it("passes context lines as both left and right", () => { const files = parseDiff(SIMPLE_DIFF); const rows = pairLines(files[0].lines); const ctx = rows.find((r) => r.left?.type === "ctx"); expect(ctx?.left).toBe(ctx?.right); }); it("pairs hunk headers as both left and right", () => { const files = parseDiff(SIMPLE_DIFF); const rows = pairLines(files[0].lines); const hunk = rows.find((r) => r.left?.type === "hunk"); expect(hunk?.left).toBe(hunk?.right); }); it("creates null left for orphan add lines", () => { const files = parseDiff(MULTIFILE_DIFF); const rows = pairLines(files[0].lines); const orphanAdd = rows.find((r) => r.left === null && r.right?.type === "add"); expect(orphanAdd).toBeTruthy(); }); it("creates null right for orphan del lines", () => { const files = parseDiff(MULTIFILE_DIFF); const rows = pairLines(files[1].lines); const orphanDel = rows.find((r) => r.right === null && r.left?.type === "del"); expect(orphanDel).toBeTruthy(); }); it("total row count equals unique line count (not doubled)", () => { const files = parseDiff(SIMPLE_DIFF); const rows = pairLines(files[0].lines); // 1 hunk + 1 ctx + 1 paired del/add + 1 ctx = 4 rows expect(rows.length).toBe(4); // source has 5 lines: hunk, ctx, del, add, ctx expect(files[0].lines.length).toBe(5); }); });