| 1 | // Fold tests for the Linear companion app's reducers (pure, no DOM): |
| 2 | // pick-projected rows normalise into plain fields (flattened dot-path keys |
| 3 | // like "state.name" become plain ones), malformed rows drop, the per-team |
| 4 | // issues map stays bounded. node test/linear.test.mjs |
| 5 | import assert from "node:assert/strict"; |
| 6 | import { app } from "../web/static/apps/linear.js"; |
| 7 | |
| 8 | let failures = 0; |
| 9 | async function test(name, fn) { |
| 10 | try { |
| 11 | await fn(); |
| 12 | console.log(`PASS ${name}`); |
| 13 | } catch (e) { |
| 14 | failures++; |
| 15 | console.log(`FAIL ${name} — ${e.message.split("\n")[0]}`); |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | const initial = () => app.reduce(undefined, { type: "noop" }) || { teams: [], fetchedAt: 0, issues: {} }; |
| 20 | const fold = (state, event) => app.reduce(state, event); |
| 21 | |
| 22 | await test("linear.teams replaces the list and normalises projected rows", () => { |
| 23 | const s = fold( |
| 24 | { teams: [{ id: "old", key: "OLD", name: "gone" }], fetchedAt: 1, issues: {} }, |
| 25 | { |
| 26 | type: "linear.teams", |
| 27 | ts: 42, |
| 28 | teams: [ |
| 29 | { id: "9cfb482a-81e3-4154-b5b9-2c805e70a02d", key: "ENG", name: "Engineering" }, |
| 30 | { id: "u2", key: "OPS" }, // sparse row survives; name falls back to the key |
| 31 | { key: "NOID" }, // no id — dropped (issue.create needs the UUID) |
| 32 | "not-an-object", |
| 33 | ], |
| 34 | }, |
| 35 | ); |
| 36 | assert.equal(s.teams.length, 2); |
| 37 | assert.deepEqual(s.teams[0], { id: "9cfb482a-81e3-4154-b5b9-2c805e70a02d", key: "ENG", name: "Engineering" }); |
| 38 | assert.equal(s.teams[1].name, "OPS"); |
| 39 | assert.equal(s.fetchedAt, 42); |
| 40 | }); |
| 41 | |
| 42 | await test("a malformed linear.teams folds to a no-op", () => { |
| 43 | const before = { teams: [{ id: "u1", key: "KEEP", name: "Keep" }], fetchedAt: 5, issues: {} }; |
| 44 | assert.equal(fold(before, { type: "linear.teams", teams: "nope" }), before); |
| 45 | }); |
| 46 | |
| 47 | await test("linear.issues normalises flattened dot-path keys into plain fields", () => { |
| 48 | const s = fold(initial(), { |
| 49 | type: "linear.issues", |
| 50 | team: "ENG", |
| 51 | ts: 9, |
| 52 | issues: [ |
| 53 | { 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" }, |
| 54 | { identifier: "ENG-9", title: "Flicker" }, // sparse row keeps defaults |
| 55 | { title: "no identifier — dropped" }, |
| 56 | ], |
| 57 | }); |
| 58 | const items = s.issues["ENG"].items; |
| 59 | assert.equal(items.length, 2); |
| 60 | assert.equal(items[0].state, "In Progress"); |
| 61 | assert.equal(items[0].assignee, "Amy"); |
| 62 | assert.equal(items[0].priority, "Urgent"); |
| 63 | assert.equal(items[1].state, ""); |
| 64 | assert.equal(items[1].url, ""); |
| 65 | }); |
| 66 | |
| 67 | await test("the issues map is trimmed to the most recently fetched teams", () => { |
| 68 | let s = initial(); |
| 69 | for (let i = 0; i < 9; i++) { |
| 70 | s = fold(s, { type: "linear.issues", team: `T${i}`, ts: i + 1, issues: [{ identifier: `T${i}-1`, title: "t" }] }); |
| 71 | } |
| 72 | const keys = Object.keys(s.issues); |
| 73 | assert.equal(keys.length, 6); // bounded — the snapshot can't balloon |
| 74 | assert.ok(!keys.includes("T0") && !keys.includes("T1") && !keys.includes("T2")); // oldest dropped |
| 75 | assert.ok(keys.includes("T8")); |
| 76 | }); |
| 77 | |
| 78 | await test("the app is gated on the linear connected service", () => { |
| 79 | assert.equal(app.requiresService, "linear"); |
| 80 | assert.deepEqual([...app.types].sort(), ["linear.issues", "linear.teams"]); |
| 81 | }); |
| 82 | |
| 83 | console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); |
| 84 | process.exit(failures === 0 ? 0 : 1); |
| 85 | |