CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | 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);
});
});
|