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);
expect(rows.length).toBe(4);
expect(files[0].lines.length).toBe(5);
});
});
|