| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { type BoardItem, type BoardSession, boardOf, peopleOn, stageOf } from "./board.ts"; |
| 5 | |
| 6 | const NOW = Date.parse("2026-10-17T12:00:00Z"); |
| 7 | const DAY = 86_400_000; |
| 8 | |
| 9 | function item(number: number, change: Partial<BoardItem> = {}): BoardItem { |
| 10 | return { number, state: "open", assignees: [], repos: [], closed_at: null, ...change }; |
| 11 | } |
| 12 | |
| 13 | function target(path: string, landed: boolean) { |
| 14 | return { repo_id: path, path, role: "target" as const, legacy_number: null, landed }; |
| 15 | } |
| 16 | |
| 17 | function session(change: Partial<BoardSession>): BoardSession { |
| 18 | return { item: null, state: "active", actor: "ada", harness: "claude_code", pull: null, pull_status: null, ...change }; |
| 19 | } |
| 20 | |
| 21 | const none = { pulls: [], sessions: [], now: NOW }; |
| 22 | |
| 23 | test("an open item nobody is on is in the backlog", () => { |
| 24 | assert.equal(stageOf(item(1), none), "backlog"); |
| 25 | // A session that ended, or one on another item, puts nobody on it. |
| 26 | assert.equal(stageOf(item(1), { ...none, sessions: [session({ item: 1, state: "ended" }), session({ item: 2 })] }), "backlog"); |
| 27 | // So does a pull request that merged or closed. |
| 28 | assert.equal(stageOf(item(1), { ...none, pulls: [{ item: 1, status: "closed" }] }), "backlog"); |
| 29 | }); |
| 30 | |
| 31 | test("an item someone is on, or with a draft, is in progress", () => { |
| 32 | assert.equal(stageOf(item(1, { assignees: ["ada"] }), none), "in_progress"); |
| 33 | assert.equal(stageOf(item(1, { claimed: true }), none), "in_progress"); |
| 34 | assert.equal(stageOf(item(1), { ...none, sessions: [session({ item: 1 })] }), "in_progress"); |
| 35 | assert.equal(stageOf(item(1), { ...none, sessions: [session({ item: 1, state: "idle" })] }), "in_progress"); |
| 36 | assert.equal(stageOf(item(1), { ...none, pulls: [{ item: 1, status: "draft" }] }), "in_progress"); |
| 37 | }); |
| 38 | |
| 39 | test("an item with an open pull request is in review, even with someone on it", () => { |
| 40 | assert.equal(stageOf(item(1, { assignees: ["ada"] }), { ...none, pulls: [{ item: 1, status: "open" }] }), "in_review"); |
| 41 | // A session names its pull request's status too. |
| 42 | assert.equal(stageOf(item(1), { ...none, sessions: [session({ item: 1, pull: 4, pull_status: "open" })] }), "in_review"); |
| 43 | assert.equal(stageOf(item(1), { ...none, pulls: [{ item: 2, status: "open" }] }), "backlog"); |
| 44 | }); |
| 45 | |
| 46 | test("an item landed in some targets but not all is landing, ahead of review", () => { |
| 47 | const repos = [target("acme/web", true), target("acme/api", false)]; |
| 48 | assert.equal(stageOf(item(1, { repos }), { ...none, pulls: [{ item: 1, status: "open" }] }), "landing"); |
| 49 | // Landed in none of them is not landing. |
| 50 | assert.equal(stageOf(item(1, { repos: [target("acme/web", false), target("acme/api", false)] }), none), "backlog"); |
| 51 | // Where it only touched does not count. |
| 52 | const touched = { ...target("acme/docs", true), role: "touched" as const }; |
| 53 | assert.equal(stageOf(item(1, { repos: [touched, target("acme/api", false)] }), none), "backlog"); |
| 54 | }); |
| 55 | |
| 56 | test("an item closed in the last two weeks is done, and older ones are left out", () => { |
| 57 | assert.equal(stageOf(item(1, { state: "closed", closed_at: new Date(NOW - 3 * DAY).toISOString() }), none), "done"); |
| 58 | assert.equal(stageOf(item(1, { state: "closed", closed_at: new Date(NOW - 15 * DAY).toISOString() }), none), null); |
| 59 | assert.equal(stageOf(item(1, { state: "closed", closed_at: null }), none), null); |
| 60 | // Closed as not planned did not ship. |
| 61 | assert.equal(stageOf(item(1, { state: "closed", reason: "not_planned", closed_at: new Date(NOW - DAY).toISOString() }), none), null); |
| 62 | // Done wins over everything else. |
| 63 | const repos = [target("acme/web", true), target("acme/api", false)]; |
| 64 | assert.equal( |
| 65 | stageOf(item(1, { state: "closed", repos, closed_at: new Date(NOW - DAY).toISOString() }), { ...none, pulls: [{ item: 1, status: "open" }] }), |
| 66 | "done", |
| 67 | ); |
| 68 | }); |
| 69 | |
| 70 | test("the board puts each item in its stage once, done newest first", () => { |
| 71 | const items = [ |
| 72 | item(1), |
| 73 | item(2, { assignees: ["ada"] }), |
| 74 | item(3, { state: "closed", closed_at: new Date(NOW - 5 * DAY).toISOString() }), |
| 75 | item(4, { state: "closed", closed_at: new Date(NOW - DAY).toISOString() }), |
| 76 | item(5, { state: "closed", closed_at: new Date(NOW - 30 * DAY).toISOString() }), |
| 77 | item(1), |
| 78 | ]; |
| 79 | const board = boardOf(items, none); |
| 80 | assert.deepEqual( |
| 81 | board.backlog.map((card) => card.item.number), |
| 82 | [1], |
| 83 | ); |
| 84 | assert.deepEqual( |
| 85 | board.in_progress.map((card) => card.item.number), |
| 86 | [2], |
| 87 | ); |
| 88 | assert.deepEqual( |
| 89 | board.done.map((card) => card.item.number), |
| 90 | [4, 3], |
| 91 | ); |
| 92 | assert.deepEqual([...board.in_review, ...board.landing], []); |
| 93 | }); |
| 94 | |
| 95 | test("who is on an item: its assignees, then who runs a live session on it, in what", () => { |
| 96 | const sessions = [ |
| 97 | session({ item: 1, actor: "ada", harness: "claude_code" }), |
| 98 | session({ item: 1, actor: "bob", harness: "codex" }), |
| 99 | session({ item: 1, actor: "cy", state: "ended" }), |
| 100 | session({ item: 2, actor: "dee" }), |
| 101 | ]; |
| 102 | assert.deepEqual(peopleOn(item(1, { assignees: ["ada"] }), sessions), [ |
| 103 | { name: "ada", harness: "claude_code" }, |
| 104 | { name: "bob", harness: "codex" }, |
| 105 | ]); |
| 106 | }); |