Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

repo-pulse.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.

repo-pulse.test.tsBlame329 lines · 1 contributor
7c975c6Claude1/**
2 * Block J18 — Repository pulse. Pure rollup unit tests + a route-auth
3 * smoke to make sure the pulse page is wired up.
4 */
5
6import { describe, it, expect } from "bun:test";
7import app from "../app";
8import {
9 PULSE_WINDOWS,
10 DEFAULT_WINDOW,
11 parseWindow,
12 windowStart,
13 windowDays,
14 summariseCommits,
15 summarisePrs,
16 summariseIssues,
17 buildPulseReport,
18 __internal,
19 type PulseCommit,
20 type PulsePr,
21 type PulseIssue,
22} from "../lib/repo-pulse";
23
24const NOW = new Date("2026-04-15T12:00:00Z");
25
26describe("repo-pulse — parseWindow", () => {
27 it("accepts the four canonical windows", () => {
28 for (const w of PULSE_WINDOWS) expect(parseWindow(w)).toBe(w);
29 });
30
31 it("falls back to DEFAULT_WINDOW on garbage", () => {
32 expect(parseWindow("")).toBe(DEFAULT_WINDOW);
33 expect(parseWindow("forever")).toBe(DEFAULT_WINDOW);
34 expect(parseWindow(null)).toBe(DEFAULT_WINDOW);
35 expect(parseWindow(undefined)).toBe(DEFAULT_WINDOW);
36 expect(parseWindow(42)).toBe(DEFAULT_WINDOW);
37 });
38});
39
40describe("repo-pulse — windowStart + windowDays", () => {
41 it("subtracts the correct day count from `now`", () => {
42 expect(windowDays("1d")).toBe(1);
43 expect(windowDays("7d")).toBe(7);
44 expect(windowDays("30d")).toBe(30);
45 expect(windowDays("90d")).toBe(90);
46 const s = windowStart(NOW, "7d");
47 expect(NOW.getTime() - s.getTime()).toBe(7 * 24 * 3600 * 1000);
48 });
49
50 it("does not mutate `now`", () => {
51 const t0 = NOW.getTime();
52 windowStart(NOW, "30d");
53 expect(NOW.getTime()).toBe(t0);
54 });
55});
56
57describe("repo-pulse — summariseCommits", () => {
58 const commits: PulseCommit[] = [
59 // In window
60 { sha: "a", author: "Alice", authorEmail: "a@x", date: "2026-04-14T00:00:00Z" },
61 { sha: "b", author: "Bob", authorEmail: "b@x", date: "2026-04-13T00:00:00Z" },
62 { sha: "c", author: "Alice", authorEmail: "a@x", date: "2026-04-12T00:00:00Z" },
63 // Out of window (older than 7d)
64 { sha: "d", author: "Alice", authorEmail: "a@x", date: "2026-04-01T00:00:00Z" },
65 ];
66
67 it("counts only commits in [start, end]", () => {
68 const start = windowStart(NOW, "7d");
69 const r = summariseCommits(commits, start, NOW);
70 expect(r.total).toBe(3);
71 expect(r.byAuthor.length).toBe(2);
72 });
73
74 it("sorts contributors by count desc, name asc as tiebreak", () => {
75 const start = windowStart(NOW, "7d");
76 const r = summariseCommits(commits, start, NOW);
77 expect(r.byAuthor[0].author).toBe("Alice");
78 expect(r.byAuthor[0].count).toBe(2);
79 expect(r.byAuthor[1].author).toBe("Bob");
80 });
81
82 it("groups by lowercased email, falling back to name", () => {
83 const cs: PulseCommit[] = [
84 { sha: "1", author: "Alice", authorEmail: "A@X", date: "2026-04-14T00:00:00Z" },
85 { sha: "2", author: "Alice", authorEmail: "a@x", date: "2026-04-13T00:00:00Z" },
86 ];
87 const r = summariseCommits(cs, windowStart(NOW, "7d"), NOW);
88 expect(r.byAuthor.length).toBe(1);
89 expect(r.byAuthor[0].count).toBe(2);
90 });
91
92 it("tracks firstSha + lastSha", () => {
93 const start = windowStart(NOW, "7d");
94 const r = summariseCommits(commits, start, NOW);
95 // Commits are newest-first, so lastSha is the newest (first in array).
96 expect(r.lastSha).toBe("a");
97 expect(r.firstSha).toBe("c");
98 });
99
100 it("returns zero counts on empty input", () => {
101 const r = summariseCommits([], windowStart(NOW, "7d"), NOW);
102 expect(r.total).toBe(0);
103 expect(r.byAuthor).toEqual([]);
104 expect(r.firstSha).toBeNull();
105 expect(r.lastSha).toBeNull();
106 });
107
108 it("ignores commits with invalid dates", () => {
109 const cs: PulseCommit[] = [
110 { sha: "1", author: "X", authorEmail: "x@x", date: "not-a-date" },
111 { sha: "2", author: "Y", authorEmail: "y@x", date: "2026-04-14T00:00:00Z" },
112 ];
113 const r = summariseCommits(cs, windowStart(NOW, "7d"), NOW);
114 expect(r.total).toBe(1);
115 });
116
117 it("falls back to (unknown) when author + email are both empty", () => {
118 const cs: PulseCommit[] = [
119 { sha: "1", author: "", authorEmail: "", date: "2026-04-14T00:00:00Z" },
120 ];
121 const r = summariseCommits(cs, windowStart(NOW, "7d"), NOW);
122 expect(r.byAuthor[0].author).toBe("(unknown)");
123 });
124});
125
126describe("repo-pulse — summarisePrs", () => {
127 const iso = (s: string) => s;
128 const prs: PulsePr[] = [
129 // Opened + merged in window
130 {
131 number: 1,
132 title: "A",
133 state: "merged",
134 createdAt: iso("2026-04-14T00:00:00Z"),
135 updatedAt: iso("2026-04-14T00:00:00Z"),
136 closedAt: iso("2026-04-14T00:00:00Z"),
137 mergedAt: iso("2026-04-14T00:00:00Z"),
138 },
139 // Opened in window, still open
140 {
141 number: 2,
142 title: "B",
143 state: "open",
144 createdAt: iso("2026-04-13T00:00:00Z"),
145 updatedAt: iso("2026-04-14T00:00:00Z"),
146 closedAt: null,
147 mergedAt: null,
148 },
149 // Closed (not merged) in window
150 {
151 number: 3,
152 title: "C",
153 state: "closed",
154 createdAt: iso("2026-01-01T00:00:00Z"),
155 updatedAt: iso("2026-04-12T00:00:00Z"),
156 closedAt: iso("2026-04-12T00:00:00Z"),
157 mergedAt: null,
158 },
159 // Entirely outside window
160 {
161 number: 4,
162 title: "D",
163 state: "merged",
164 createdAt: iso("2026-01-01T00:00:00Z"),
165 updatedAt: iso("2026-01-02T00:00:00Z"),
166 closedAt: iso("2026-01-02T00:00:00Z"),
167 mergedAt: iso("2026-01-02T00:00:00Z"),
168 },
169 ];
170
171 const start = windowStart(NOW, "7d");
172
173 it("counts opened/merged/closed correctly", () => {
174 const r = summarisePrs(prs, start, NOW);
175 expect(r.opened).toBe(2);
176 expect(r.mergedCount).toBe(1);
177 expect(r.closed).toBe(1);
178 });
179
180 it("reports `active` only for state=open with updatedAt in window", () => {
181 const r = summarisePrs(prs, start, NOW);
182 expect(r.active).toBe(1);
183 });
184
185 it("includes the merged PR in mergedList + opened PRs in openedList", () => {
186 const r = summarisePrs(prs, start, NOW);
187 expect(r.mergedList.map((p) => p.number)).toEqual([1]);
188 expect(r.openedList.map((p) => p.number).sort()).toEqual([1, 2]);
189 });
190
191 it("accepts Date instances as well as ISO strings", () => {
192 const withDates: PulsePr[] = [
193 {
194 number: 5,
195 title: "E",
196 state: "merged",
197 createdAt: new Date("2026-04-14T00:00:00Z"),
198 updatedAt: new Date("2026-04-14T00:00:00Z"),
199 closedAt: new Date("2026-04-14T00:00:00Z"),
200 mergedAt: new Date("2026-04-14T00:00:00Z"),
201 },
202 ];
203 const r = summarisePrs(withDates, start, NOW);
204 expect(r.opened).toBe(1);
205 expect(r.mergedCount).toBe(1);
206 });
207
208 it("handles empty list", () => {
209 const r = summarisePrs([], start, NOW);
210 expect(r.opened).toBe(0);
211 expect(r.mergedCount).toBe(0);
212 expect(r.closed).toBe(0);
213 expect(r.active).toBe(0);
214 });
215});
216
217describe("repo-pulse — summariseIssues", () => {
218 const start = windowStart(NOW, "7d");
219 const issueList: PulseIssue[] = [
220 {
221 number: 1,
222 title: "x",
223 state: "open",
224 createdAt: "2026-04-14T00:00:00Z",
225 updatedAt: "2026-04-14T00:00:00Z",
226 closedAt: null,
227 },
228 {
229 number: 2,
230 title: "y",
231 state: "closed",
232 createdAt: "2026-04-01T00:00:00Z",
233 updatedAt: "2026-04-13T00:00:00Z",
234 closedAt: "2026-04-13T00:00:00Z",
235 },
236 // Out of window
237 {
238 number: 3,
239 title: "z",
240 state: "closed",
241 createdAt: "2026-01-01T00:00:00Z",
242 updatedAt: "2026-01-02T00:00:00Z",
243 closedAt: "2026-01-02T00:00:00Z",
244 },
245 ];
246
247 it("buckets opened / closed / active", () => {
248 const r = summariseIssues(issueList, start, NOW);
249 expect(r.opened).toBe(1);
250 expect(r.closed).toBe(1);
251 expect(r.active).toBe(1);
252 });
253
254 it("populates openedList + closedList", () => {
255 const r = summariseIssues(issueList, start, NOW);
256 expect(r.openedList.map((i) => i.number)).toEqual([1]);
257 expect(r.closedList.map((i) => i.number)).toEqual([2]);
258 });
259
260 it("tolerates null closedAt / weird dates", () => {
261 const bad: PulseIssue[] = [
262 {
263 number: 99,
264 title: "bad",
265 state: "open",
266 createdAt: "oops",
267 updatedAt: "also oops",
268 closedAt: null,
269 },
270 ];
271 const r = summariseIssues(bad, start, NOW);
272 expect(r.opened).toBe(0);
273 expect(r.closed).toBe(0);
274 expect(r.active).toBe(0);
275 });
276});
277
278describe("repo-pulse — buildPulseReport", () => {
279 it("bundles all three rollups with a resolved window", () => {
280 const r = buildPulseReport({
281 window: "7d",
282 now: NOW,
283 commits: [
284 {
285 sha: "a",
286 author: "A",
287 authorEmail: "a@x",
288 date: "2026-04-14T00:00:00Z",
289 },
290 ],
291 prs: [],
292 issues: [],
293 });
294 expect(r.window).toBe("7d");
295 expect(r.days).toBe(7);
296 expect(r.start < r.end).toBe(true);
297 expect(r.commits.total).toBe(1);
298 expect(r.prs.opened).toBe(0);
299 expect(r.issues.opened).toBe(0);
300 });
301});
302
303describe("repo-pulse — __internal", () => {
304 it("re-exports the pure helpers for parity", () => {
305 expect(__internal.parseWindow).toBe(parseWindow);
306 expect(__internal.windowStart).toBe(windowStart);
307 expect(__internal.windowDays).toBe(windowDays);
308 expect(__internal.summariseCommits).toBe(summariseCommits);
309 expect(__internal.summarisePrs).toBe(summarisePrs);
310 expect(__internal.summariseIssues).toBe(summariseIssues);
311 expect(__internal.buildPulseReport).toBe(buildPulseReport);
312 expect(__internal.PULSE_WINDOWS).toBe(PULSE_WINDOWS);
313 expect(__internal.DEFAULT_WINDOW as string).toBe(DEFAULT_WINDOW);
314 });
315});
316
317describe("repo-pulse — routes", () => {
318 it("GET /:o/:r/pulse returns 404 for unknown repo", async () => {
319 const res = await app.request("/alice/nope/pulse");
320 expect(res.status).toBe(404);
321 });
322
323 it("GET /:o/:r/pulse?window=bogus normalises to default", async () => {
324 // Route resolves repo first, so we still expect 404 for unknown — this
325 // asserts the route accepts the query without crashing.
326 const res = await app.request("/alice/nope/pulse?window=bogus");
327 expect(res.status).toBe(404);
328 });
329});