Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

diff-view.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

diff-view.test.tsBlame175 lines · 1 contributor
82c3a38ccanty labs1import { describe, it, expect } from "bun:test";
2import { parseDiff, pairLines } from "../lib/diff";
3
4const SIMPLE_DIFF = `diff --git a/src/foo.ts b/src/foo.ts
5index abc..def 100644
6--- a/src/foo.ts
7+++ b/src/foo.ts
8@@ -1,5 +1,5 @@
9 import { bar } from './bar';
10-const x = 1;
11+const x = 2;
12 export default x;
13`;
14
15const MULTIFILE_DIFF = `diff --git a/a.ts b/a.ts
16index 000..111 100644
17--- a/a.ts
18+++ b/a.ts
19@@ -1,2 +1,3 @@
20 line1
21+line2
22 line3
23diff --git a/b.ts b/b.ts
24index 222..333 100644
25--- a/b.ts
26+++ b/b.ts
27@@ -1,2 +1,1 @@
28 hello
29-world
30`;
31
32const BINARY_DIFF = `diff --git a/image.png b/image.png
33index abc..def 100644
34Binary files a/image.png and b/image.png differ
35`;
36
37const WS_DIFF = `diff --git a/x.ts b/x.ts
38index 000..111 100644
39--- a/x.ts
40+++ b/x.ts
41@@ -1,3 +1,3 @@
42 ctx
43- foo
44+ foo
45 ctx
46`;
47
48describe("parseDiff", () => {
49 it("parses a single-file diff with one del and one add", () => {
50 const files = parseDiff(SIMPLE_DIFF);
51 expect(files.length).toBe(1);
52 expect(files[0].path).toBe("src/foo.ts");
53 expect(files[0].additions).toBe(1);
54 expect(files[0].deletions).toBe(1);
55 expect(files[0].isBinary).toBe(false);
56 });
57
58 it("assigns correct line numbers", () => {
59 const files = parseDiff(SIMPLE_DIFF);
60 const lines = files[0].lines;
61 const hunk = lines.find((l) => l.type === "hunk");
62 expect(hunk).toBeTruthy();
63 const ctx1 = lines.find((l) => l.type === "ctx");
64 expect(ctx1?.oldLine).toBe(1);
65 expect(ctx1?.newLine).toBe(1);
66 const del = lines.find((l) => l.type === "del");
67 expect(del?.oldLine).toBe(2);
68 expect(del?.newLine).toBeNull();
69 const add = lines.find((l) => l.type === "add");
70 expect(add?.oldLine).toBeNull();
71 expect(add?.newLine).toBe(2);
72 });
73
74 it("parses multiple files", () => {
75 const files = parseDiff(MULTIFILE_DIFF);
76 expect(files.length).toBe(2);
77 expect(files[0].path).toBe("a.ts");
78 expect(files[1].path).toBe("b.ts");
79 expect(files[0].additions).toBe(1);
80 expect(files[0].deletions).toBe(0);
81 expect(files[1].additions).toBe(0);
82 expect(files[1].deletions).toBe(1);
83 });
84
85 it("marks binary files", () => {
86 const files = parseDiff(BINARY_DIFF);
87 expect(files.length).toBe(1);
88 expect(files[0].isBinary).toBe(true);
89 expect(files[0].lines.length).toBe(0);
90 });
91
92 it("detects whitespace-only changes", () => {
93 const files = parseDiff(WS_DIFF);
94 const lines = files[0].lines;
95 const del = lines.find((l) => l.type === "del");
96 const add = lines.find((l) => l.type === "add");
97 expect(del?.wsOnly).toBe(true);
98 expect(add?.wsOnly).toBe(true);
99 });
100
101 it("does NOT mark non-whitespace changes as wsOnly", () => {
102 const files = parseDiff(SIMPLE_DIFF);
103 const del = files[0].lines.find((l) => l.type === "del");
104 const add = files[0].lines.find((l) => l.type === "add");
105 expect(del?.wsOnly).toBeFalsy();
106 expect(add?.wsOnly).toBeFalsy();
107 });
108
109 it("returns empty array for empty input", () => {
110 expect(parseDiff("")).toEqual([]);
111 });
112
113 it("handles CRLF line endings", () => {
114 const crlf = SIMPLE_DIFF.replace(/\n/g, "\r\n");
115 const files = parseDiff(crlf);
116 expect(files.length).toBe(1);
117 expect(files[0].additions).toBe(1);
118 });
119
120 it("strips the +/-/space prefix from content", () => {
121 const files = parseDiff(SIMPLE_DIFF);
122 const add = files[0].lines.find((l) => l.type === "add")!;
123 const del = files[0].lines.find((l) => l.type === "del")!;
124 expect(add.content).not.toMatch(/^\+/);
125 expect(del.content).not.toMatch(/^-/);
126 expect(add.content).toBe("const x = 2;");
127 expect(del.content).toBe("const x = 1;");
128 });
129});
130
131describe("pairLines", () => {
132 it("pairs a del/add block into a single row", () => {
133 const files = parseDiff(SIMPLE_DIFF);
134 const rows = pairLines(files[0].lines);
135 const paired = rows.find((r) => r.left?.type === "del" && r.right?.type === "add");
136 expect(paired).toBeTruthy();
137 });
138
139 it("passes context lines as both left and right", () => {
140 const files = parseDiff(SIMPLE_DIFF);
141 const rows = pairLines(files[0].lines);
142 const ctx = rows.find((r) => r.left?.type === "ctx");
143 expect(ctx?.left).toBe(ctx?.right);
144 });
145
146 it("pairs hunk headers as both left and right", () => {
147 const files = parseDiff(SIMPLE_DIFF);
148 const rows = pairLines(files[0].lines);
149 const hunk = rows.find((r) => r.left?.type === "hunk");
150 expect(hunk?.left).toBe(hunk?.right);
151 });
152
153 it("creates null left for orphan add lines", () => {
154 const files = parseDiff(MULTIFILE_DIFF);
155 const rows = pairLines(files[0].lines);
156 const orphanAdd = rows.find((r) => r.left === null && r.right?.type === "add");
157 expect(orphanAdd).toBeTruthy();
158 });
159
160 it("creates null right for orphan del lines", () => {
161 const files = parseDiff(MULTIFILE_DIFF);
162 const rows = pairLines(files[1].lines);
163 const orphanDel = rows.find((r) => r.right === null && r.left?.type === "del");
164 expect(orphanDel).toBeTruthy();
165 });
166
167 it("total row count equals unique line count (not doubled)", () => {
168 const files = parseDiff(SIMPLE_DIFF);
169 const rows = pairLines(files[0].lines);
170 // 1 hunk + 1 ctx + 1 paired del/add + 1 ctx = 4 rows
171 expect(rows.length).toBe(4);
172 // source has 5 lines: hunk, ctx, del, add, ctx
173 expect(files[0].lines.length).toBe(5);
174 });
175});