// Fold tests for the Linear companion app's reducers (pure, no DOM): // pick-projected rows normalise into plain fields (flattened dot-path keys // like "state.name" become plain ones), malformed rows drop, the per-team // issues map stays bounded. node test/linear.test.mjs import assert from "node:assert/strict"; import { app } from "../web/static/apps/linear.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" }) || { teams: [], fetchedAt: 0, issues: {} }; const fold = (state, event) => app.reduce(state, event); await test("linear.teams replaces the list and normalises projected rows", () => { const s = fold( { teams: [{ id: "old", key: "OLD", name: "gone" }], fetchedAt: 1, issues: {} }, { type: "linear.teams", ts: 42, teams: [ { id: "9cfb482a-81e3-4154-b5b9-2c805e70a02d", key: "ENG", name: "Engineering" }, { id: "u2", key: "OPS" }, // sparse row survives; name falls back to the key { key: "NOID" }, // no id — dropped (issue.create needs the UUID) "not-an-object", ], }, ); assert.equal(s.teams.length, 2); assert.deepEqual(s.teams[0], { id: "9cfb482a-81e3-4154-b5b9-2c805e70a02d", key: "ENG", name: "Engineering" }); assert.equal(s.teams[1].name, "OPS"); assert.equal(s.fetchedAt, 42); }); await test("a malformed linear.teams folds to a no-op", () => { const before = { teams: [{ id: "u1", key: "KEEP", name: "Keep" }], fetchedAt: 5, issues: {} }; assert.equal(fold(before, { type: "linear.teams", teams: "nope" }), before); }); await test("linear.issues normalises flattened dot-path keys into plain fields", () => { const s = fold(initial(), { type: "linear.issues", team: "ENG", ts: 9, issues: [ { identifier: "ENG-412", title: "Crash on login", url: "https://linear.app/x/issue/ENG-412", "state.name": "In Progress", "assignee.displayName": "Amy", priorityLabel: "Urgent", updatedAt: "2026-07-08T00:00:00Z" }, { identifier: "ENG-9", title: "Flicker" }, // sparse row keeps defaults { title: "no identifier — dropped" }, ], }); const items = s.issues["ENG"].items; assert.equal(items.length, 2); assert.equal(items[0].state, "In Progress"); assert.equal(items[0].assignee, "Amy"); assert.equal(items[0].priority, "Urgent"); assert.equal(items[1].state, ""); assert.equal(items[1].url, ""); }); await test("the issues map is trimmed to the most recently fetched teams", () => { let s = initial(); for (let i = 0; i < 9; i++) { s = fold(s, { type: "linear.issues", team: `T${i}`, ts: i + 1, issues: [{ identifier: `T${i}-1`, title: "t" }] }); } const keys = Object.keys(s.issues); assert.equal(keys.length, 6); // bounded — the snapshot can't balloon assert.ok(!keys.includes("T0") && !keys.includes("T1") && !keys.includes("T2")); // oldest dropped assert.ok(keys.includes("T8")); }); await test("the app is gated on the linear connected service", () => { assert.equal(app.requiresService, "linear"); assert.deepEqual([...app.types].sort(), ["linear.issues", "linear.teams"]); }); console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); process.exit(failures === 0 ? 0 : 1);