// Fold tests for the GitHub companion app's reducers (pure, no DOM): // pick-projected rows normalise into plain fields, malformed rows drop, the // per-repo issues map stays bounded. node test/github.test.mjs import assert from "node:assert/strict"; import { app } from "../web/static/apps/github.js"; let failures = 0; async function test(name, fn) { try { await fn(); console.log(`PASS ${name}`); } catch (e) { failures++; console.log(`FAIL ${name} — ${e.message.split("\n")[0]}`); } } const initial = () => app.reduce(undefined, { type: "noop" }) || { repos: [], fetchedAt: 0, issues: {} }; const fold = (state, event) => app.reduce(state, event); await test("github.repos replaces the list and normalises projected rows", () => { const s = fold( { repos: [{ full_name: "old/gone" }], fetchedAt: 1, issues: {} }, { type: "github.repos", ts: 42, repos: [ { full_name: "paul/lchat", description: "chat", private: true, archived: false, fork: false, html_url: "https://github.com/paul/lchat", open_issues_count: 3, stargazers_count: 7, pushed_at: "2026-07-01T00:00:00Z" }, { full_name: "paul/min" }, // sparse row survives with defaults { description: "no name — dropped" }, "not-an-object", ], }, ); assert.equal(s.repos.length, 2); assert.deepEqual(s.repos[0], { full_name: "paul/lchat", description: "chat", private: true, archived: false, fork: false, html_url: "https://github.com/paul/lchat", open_issues: 3, stars: 7, pushed_at: "2026-07-01T00:00:00Z", }); assert.equal(s.repos[1].full_name, "paul/min"); assert.equal(s.repos[1].open_issues, null); // absent count reads "—", never 0 assert.equal(s.fetchedAt, 42); }); await test("a malformed github.repos folds to a no-op", () => { const before = { repos: [{ full_name: "keep/me" }], fetchedAt: 5, issues: {} }; assert.equal(fold(before, { type: "github.repos", repos: "nope" }), before); }); await test("github.issues normalises flattened dot-path keys into plain fields", () => { const s = fold(initial(), { type: "github.issues", repo: "paul/lchat", ts: 9, issues: [ { number: 12, title: "Fix login", html_url: "https://github.com/paul/lchat/issues/12", "user.login": "amy", comments: 4, updated_at: "2026-07-08T00:00:00Z" }, { number: 13, title: "Ship it", "pull_request.url": "https://api.github.com/x", draft: true }, { title: "no number — dropped" }, ], }); const items = s.issues["paul/lchat"].items; assert.equal(items.length, 2); assert.equal(items[0].by, "amy"); assert.equal(items[0].is_pr, false); assert.equal(items[1].is_pr, true); assert.equal(items[1].draft, true); }); await test("the issues map is trimmed to the most recently fetched repos", () => { let s = initial(); for (let i = 0; i < 9; i++) { s = fold(s, { type: "github.issues", repo: `o/r${i}`, ts: i + 1, issues: [{ number: 1, title: "t" }] }); } const keys = Object.keys(s.issues); assert.equal(keys.length, 6); // bounded — the snapshot can't balloon assert.ok(!keys.includes("o/r0") && !keys.includes("o/r1") && !keys.includes("o/r2")); // oldest dropped assert.ok(keys.includes("o/r8")); }); await test("the app is gated on the github connected service", () => { assert.equal(app.requiresService, "github"); assert.deepEqual([...app.types].sort(), ["github.issues", "github.repos"]); }); console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); process.exit(failures === 0 ? 0 : 1);