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

repo-size.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-size.test.tsBlame329 lines · 1 contributor
ce08e97Claude1/**
2 * Block J31 — Repository size audit. Pure helper tests.
3 */
4
5import { describe, it, expect } from "bun:test";
6import {
7 DEFAULT_TOP_N,
8 SIZE_CLASSES,
9 topLevelDir,
10 classifyFileSize,
11 summariseSize,
12 bucketBySize,
13 topLargestFiles,
14 summariseByTopDir,
15 buildSizeReport,
16 __internal,
17 type RepoSizeEntry,
18} from "../lib/repo-size";
19
20const KB = 1024;
21const MB = 1024 * 1024;
22
23describe("repo-size — topLevelDir", () => {
24 it("extracts the first segment", () => {
25 expect(topLevelDir("src/app.ts")).toBe("src");
26 expect(topLevelDir("apps/web/main.ts")).toBe("apps");
27 });
28 it("returns '.' for root-level files", () => {
29 expect(topLevelDir("README.md")).toBe(".");
30 expect(topLevelDir("package.json")).toBe(".");
31 });
32 it("handles leading slash", () => {
33 expect(topLevelDir("/src/app.ts")).toBe("src");
34 expect(topLevelDir("/README.md")).toBe(".");
35 });
36 it("handles empty / non-string", () => {
37 expect(topLevelDir("")).toBe(".");
38 // @ts-expect-error
39 expect(topLevelDir(null)).toBe(".");
40 // @ts-expect-error
41 expect(topLevelDir(undefined)).toBe(".");
42 });
43});
44
45describe("repo-size — classifyFileSize", () => {
46 it("maps to the five classes by boundary", () => {
47 expect(classifyFileSize(0)).toBe("tiny");
48 expect(classifyFileSize(KB - 1)).toBe("tiny");
49 expect(classifyFileSize(KB)).toBe("small");
50 expect(classifyFileSize(50 * KB)).toBe("small");
51 expect(classifyFileSize(100 * KB)).toBe("medium");
52 expect(classifyFileSize(500 * KB)).toBe("medium");
53 expect(classifyFileSize(MB)).toBe("large");
54 expect(classifyFileSize(5 * MB)).toBe("large");
55 expect(classifyFileSize(10 * MB)).toBe("xlarge");
56 expect(classifyFileSize(50 * MB)).toBe("xlarge");
57 });
58 it("defaults bogus values to tiny", () => {
59 expect(classifyFileSize(Number.NaN)).toBe("tiny");
60 expect(classifyFileSize(-5)).toBe("tiny");
61 });
62 it("has exactly five ordered classes", () => {
63 expect(SIZE_CLASSES).toHaveLength(5);
64 expect(SIZE_CLASSES.map((c) => c.key)).toEqual([
65 "tiny",
66 "small",
67 "medium",
68 "large",
69 "xlarge",
70 ]);
71 });
72});
73
74describe("repo-size — summariseSize", () => {
75 it("aggregates bytes and computes mean/median", () => {
76 const entries: RepoSizeEntry[] = [
77 { path: "a", size: 100 },
78 { path: "b", size: 200 },
79 { path: "c", size: 300 },
80 { path: "d", size: 400 },
81 ];
82 const s = summariseSize(entries);
83 expect(s.totalBytes).toBe(1000);
84 expect(s.totalFiles).toBe(4);
85 expect(s.countedFiles).toBe(4);
86 expect(s.averageBytes).toBe(250);
87 expect(s.medianBytes).toBe(250); // (200+300)/2
88 expect(s.largestBytes).toBe(400);
89 expect(s.smallestBytes).toBe(100);
90 });
91 it("median picks middle of odd-length series", () => {
92 const s = summariseSize([
93 { path: "a", size: 10 },
94 { path: "b", size: 50 },
95 { path: "c", size: 500 },
96 ]);
97 expect(s.medianBytes).toBe(50);
98 });
99 it("drops invalid entries but keeps totalFiles on raw count", () => {
100 const s = summariseSize([
101 { path: "a", size: 100 },
102 // @ts-expect-error
103 { path: 42, size: 100 },
104 { path: "b", size: Number.NaN },
105 { path: "c", size: -5 },
106 { path: "d", size: 900 },
107 ]);
108 expect(s.totalFiles).toBe(5);
109 expect(s.countedFiles).toBe(2);
110 expect(s.totalBytes).toBe(1000);
111 });
112 it("handles empty input", () => {
113 const s = summariseSize([]);
114 expect(s.totalBytes).toBe(0);
115 expect(s.totalFiles).toBe(0);
116 expect(s.countedFiles).toBe(0);
117 expect(s.averageBytes).toBe(0);
118 expect(s.medianBytes).toBe(0);
119 expect(s.largestBytes).toBe(0);
120 expect(s.smallestBytes).toBe(0);
121 });
122});
123
124describe("repo-size — bucketBySize", () => {
125 it("distributes into five class buckets", () => {
126 const entries: RepoSizeEntry[] = [
127 { path: "a", size: 500 }, // tiny
128 { path: "b", size: 10 * KB }, // small
129 { path: "c", size: 500 * KB }, // medium
130 { path: "d", size: 5 * MB }, // large
131 { path: "e", size: 20 * MB }, // xlarge
132 { path: "f", size: 5 * KB }, // small
133 ];
134 const b = bucketBySize(entries);
135 const by = Object.fromEntries(b.map((x) => [x.key, x]));
136 expect(by.tiny!.fileCount).toBe(1);
137 expect(by.small!.fileCount).toBe(2);
138 expect(by.medium!.fileCount).toBe(1);
139 expect(by.large!.fileCount).toBe(1);
140 expect(by.xlarge!.fileCount).toBe(1);
141 expect(by.small!.bytes).toBe(10 * KB + 5 * KB);
142 });
143 it("returns all zero buckets for empty input", () => {
144 const b = bucketBySize([]);
145 expect(b).toHaveLength(5);
146 expect(b.every((x) => x.fileCount === 0 && x.bytes === 0)).toBe(true);
147 });
148});
149
150describe("repo-size — topLargestFiles", () => {
151 const entries: RepoSizeEntry[] = [
152 { path: "src/a.ts", size: 100 },
153 { path: "src/b.ts", size: 500 },
154 { path: "bundle.js", size: 10_000 },
155 { path: "img.png", size: 2_000 },
156 { path: "tiny.md", size: 10 },
157 ];
158
159 it("returns sorted-desc by size", () => {
160 const out = topLargestFiles(entries);
161 expect(out.map((f) => f.path)).toEqual([
162 "bundle.js",
163 "img.png",
164 "src/b.ts",
165 "src/a.ts",
166 "tiny.md",
167 ]);
168 });
169
170 it("respects limit", () => {
171 const out = topLargestFiles(entries, { limit: 2 });
172 expect(out).toHaveLength(2);
173 expect(out[0]!.path).toBe("bundle.js");
174 expect(out[1]!.path).toBe("img.png");
175 });
176
177 it("applies minBytes floor", () => {
178 const out = topLargestFiles(entries, { minBytes: 1_000 });
179 expect(out.map((f) => f.path)).toEqual(["bundle.js", "img.png"]);
180 });
181
182 it("percentages sum to 100 across all returned files when limit is generous", () => {
183 const out = topLargestFiles(entries, { limit: 99 });
184 const sum = out.reduce((acc, f) => acc + f.percent, 0);
185 expect(Math.round(sum)).toBe(100);
186 });
187
188 it("tie-breaks by path alphabetical", () => {
189 const same: RepoSizeEntry[] = [
190 { path: "zebra.bin", size: 100 },
191 { path: "alpha.bin", size: 100 },
192 ];
193 const out = topLargestFiles(same);
194 expect(out[0]!.path).toBe("alpha.bin");
195 expect(out[1]!.path).toBe("zebra.bin");
196 });
197
198 it("populates topDir", () => {
199 const out = topLargestFiles(entries);
200 expect(out.find((f) => f.path === "src/b.ts")!.topDir).toBe("src");
201 expect(out.find((f) => f.path === "bundle.js")!.topDir).toBe(".");
202 });
203
204 it("does not mutate the input array", () => {
205 const copy = [...entries];
206 topLargestFiles(entries);
207 expect(entries).toEqual(copy);
208 });
209
210 it("returns [] for empty input", () => {
211 expect(topLargestFiles([])).toEqual([]);
212 });
213
214 it("defaults limit to DEFAULT_TOP_N when missing or invalid", () => {
215 const many: RepoSizeEntry[] = Array.from({ length: 100 }, (_, i) => ({
216 path: `f${i}.bin`,
217 size: i + 1,
218 }));
219 expect(topLargestFiles(many)).toHaveLength(DEFAULT_TOP_N);
220 expect(topLargestFiles(many, { limit: 0 })).toHaveLength(DEFAULT_TOP_N);
221 expect(topLargestFiles(many, { limit: -5 })).toHaveLength(DEFAULT_TOP_N);
222 });
223});
224
225describe("repo-size — summariseByTopDir", () => {
226 const entries: RepoSizeEntry[] = [
227 { path: "src/a.ts", size: 100 },
228 { path: "src/sub/b.ts", size: 400 },
229 { path: "tests/t.ts", size: 50 },
230 { path: "README.md", size: 10 },
231 { path: "package.json", size: 40 },
232 ];
233
234 it("groups by first segment", () => {
235 const out = summariseByTopDir(entries);
236 const by = Object.fromEntries(out.map((d) => [d.name, d]));
237 expect(by.src!.bytes).toBe(500);
238 expect(by.src!.fileCount).toBe(2);
239 expect(by.tests!.bytes).toBe(50);
240 expect(by["."]!.bytes).toBe(50);
241 expect(by["."]!.fileCount).toBe(2);
242 });
243
244 it("sorts by bytes desc", () => {
245 const out = summariseByTopDir(entries);
246 expect(out.map((d) => d.name)).toEqual(["src", "tests", "."]);
247 });
248
249 it("percentages sum to 100", () => {
250 const out = summariseByTopDir(entries);
251 const sum = out.reduce((acc, d) => acc + d.percent, 0);
252 expect(Math.round(sum)).toBe(100);
253 });
254
255 it("root bucket sorts last on byte ties", () => {
256 const tied: RepoSizeEntry[] = [
257 { path: "src/a.ts", size: 100 },
258 { path: "README.md", size: 100 },
259 ];
260 const out = summariseByTopDir(tied);
261 expect(out.map((d) => d.name)).toEqual(["src", "."]);
262 });
263
264 it("empty input gives empty array", () => {
265 expect(summariseByTopDir([])).toEqual([]);
266 });
267});
268
269describe("repo-size — buildSizeReport", () => {
270 const entries: RepoSizeEntry[] = [
271 { path: "src/a.ts", size: 100 },
272 { path: "src/big.bin", size: 5 * MB },
273 { path: "docs/readme.md", size: 800 },
274 ];
275
276 it("assembles summary + buckets + directories + largest", () => {
277 const r = buildSizeReport({ entries });
278 expect(r.summary.countedFiles).toBe(3);
279 expect(r.summary.totalBytes).toBe(100 + 5 * MB + 800);
280 expect(r.buckets).toHaveLength(5);
281 expect(r.directories.map((d) => d.name)).toEqual(["src", "docs"]);
282 expect(r.largest[0]!.path).toBe("src/big.bin");
283 });
284
285 it("honours topN", () => {
286 const r = buildSizeReport({ entries, topN: 1 });
287 expect(r.largest).toHaveLength(1);
288 expect(r.largest[0]!.path).toBe("src/big.bin");
289 });
290
291 it("honours minBytesForLargest", () => {
292 const r = buildSizeReport({
293 entries,
294 minBytesForLargest: 1000,
295 });
296 expect(r.largest.map((f) => f.path)).toEqual(["src/big.bin"]);
297 });
298});
299
300describe("repo-size — routes", () => {
301 it("GET /:o/:r/insights/size returns 200 or 404 (never 500)", async () => {
302 const { default: app } = await import("../app");
303 const res = await app.request("/alice/repo/insights/size");
304 expect([200, 404]).toContain(res.status);
305 });
306 it("ignores bogus query params", async () => {
307 const { default: app } = await import("../app");
308 const res = await app.request(
309 "/alice/repo/insights/size?top=wow&min=nope"
310 );
311 expect([200, 404]).toContain(res.status);
312 });
313});
314
315describe("repo-size — __internal parity", () => {
316 it("re-exports every helper", () => {
317 expect(__internal.SIZE_CLASSES).toBe(SIZE_CLASSES);
318 expect(__internal.DEFAULT_TOP_N).toBe(DEFAULT_TOP_N);
319 expect(__internal.topLevelDir).toBe(topLevelDir);
320 expect(__internal.classifyFileSize).toBe(classifyFileSize);
321 expect(__internal.summariseSize).toBe(summariseSize);
322 expect(__internal.bucketBySize).toBe(bucketBySize);
323 expect(__internal.topLargestFiles).toBe(topLargestFiles);
324 expect(__internal.summariseByTopDir).toBe(summariseByTopDir);
325 expect(__internal.buildSizeReport).toBe(buildSizeReport);
326 expect(typeof __internal.median).toBe("function");
327 expect(typeof __internal.validEntries).toBe("function");
328 });
329});