Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

219 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { Claim, ItemRepo, WorkItem, WorkspaceSession } from "@g1t/contracts";
5
6import { type ItemPull, boardOf, cappedNotes, pullRepos, pullsByItem, readPages, shownStage, stageOf } from "./board.ts";
7
8const NOW = Date.parse("2026-10-17T12:00:00Z");
9const DAY = 86_400_000;
10
11function repo(path: string, role: ItemRepo["role"], landed = false): ItemRepo {
12 return { repoId: `rep_${path}`, path, role, legacyNumber: null, landed };
13}
14
15function item(number: number, more: Partial<WorkItem> = {}): WorkItem {
16 return {
17 id: `wki_${number}`,
18 workspace: "acme",
19 number,
20 title: `Item ${number}`,
21 body: "",
22 labels: [],
23 state: "open",
24 reason: null,
25 resolutions: [],
26 author: { id: "usr_1", username: "ana" } as WorkItem["author"],
27 requestedBy: null,
28 createdAt: new Date(NOW - 5 * DAY).toISOString(),
29 updatedAt: new Date(NOW - number * 1000).toISOString(),
30 closedAt: null,
31 pullCount: 0,
32 commentCount: 0,
33 assignees: [],
34 blockedBy: [],
35 agent: null,
36 repos: [repo("acme/web", "origin")],
37 ...more,
38 };
39}
40
41function live(item: number, more: Partial<WorkspaceSession> = {}): WorkspaceSession {
42 return {
43 kind: "harness",
44 id: `ses_${item}`,
45 repo: "acme/web",
46 repoId: "rep_acme/web",
47 title: null,
48 actor: "bo",
49 harness: "claude_code",
50 agent: null,
51 branch: "main",
52 item,
53 itemTitle: null,
54 pull: null,
55 pullTitle: null,
56 pullStatus: null,
57 state: "active",
58 startedAt: new Date(NOW).toISOString(),
59 lastActivityAt: new Date(NOW).toISOString(),
60 subagents: 0,
61 step: null,
62 session: null,
63 ...more,
64 };
65}
66
67/** Pull requests of these statuses, each its own, closing the item unless `part`. */
68const pr = (...statuses: (ItemPull["status"] | `part:${ItemPull["status"]}`)[]): ItemPull[] =>
69 statuses.map((status, index) => {
70 const part = status.startsWith("part:");
71 return { key: `acme/web#${index + 100}`, status: (part ? status.slice(5) : status) as ItemPull["status"], part };
72 });
73
74const claim = (number: number, who: string): Claim =>
75 ({ id: `clm_${number}`, item: number, workspace: "acme", state: "active", actor: { id: "usr_9", username: who } }) as Claim;
76
77test("each work item's stage is the first that holds: done, landing, review, progress, backlog", () => {
78 assert.equal(stageOf(item(1), {}, NOW), "backlog");
79 assert.equal(stageOf(item(2, { assignees: ["ana"] }), {}, NOW), "progress");
80 assert.equal(stageOf(item(3), { claimed: true }, NOW), "progress");
81 assert.equal(stageOf(item(4), { live: true }, NOW), "progress");
82 assert.equal(stageOf(item(5), { pulls: pr("draft") }, NOW), "progress");
83 assert.equal(stageOf(item(6, { assignees: ["ana"] }), { pulls: pr("draft", "open") }, NOW), "review");
84 // Landed in one target, not yet the other: Landing, even with a pull request open.
85 const landing = item(7, { repos: [repo("acme/web", "target", true), repo("acme/api", "target")] });
86 assert.equal(stageOf(landing, { pulls: pr("open") }, NOW), "landing");
87 // A touched repository's landing is not a target's.
88 assert.equal(stageOf(item(8, { repos: [repo("acme/web", "touched", true), repo("acme/api", "target")] }), {}, NOW), "backlog");
89 assert.equal(stageOf(item(9, { state: "closed", closedAt: new Date(NOW - 3 * DAY).toISOString() }), {}, NOW), "done");
90 // Closed long ago: off the board.
91 assert.equal(stageOf(item(10, { state: "closed", closedAt: new Date(NOW - 40 * DAY).toISOString() }), {}, NOW), null);
92});
93
94test("the board puts each item in its column with who and which session is on it", () => {
95 const items = [
96 item(1),
97 item(2),
98 item(3),
99 item(4),
100 item(5, { state: "closed", closedAt: new Date(NOW - DAY).toISOString() }),
101 item(6, { state: "closed", closedAt: new Date(NOW - 2 * DAY).toISOString() }),
102 ];
103 const board = boardOf(
104 items,
105 { claims: [claim(2, "cy")], sessions: [live(3, { pull: 9, pullStatus: "open" }), live(1, { state: "ended" })], pulls: pullsByItem([{ repo: "acme/web", number: 20, item: 4, status: "draft" }, { repo: "acme/web", number: 21, item: null, status: "open" }]) },
106 NOW,
107 );
108 assert.deepEqual(board.backlog.map((card) => card.item.number), [1]);
109 assert.deepEqual(board.progress.map((card) => card.item.number), [2, 4]);
110 assert.deepEqual(board.progress[0]!.on.people, ["cy"]);
111 // A live session's open pull request puts its item in review, the session on the card.
112 assert.deepEqual(board.review.map((card) => card.item.number), [3]);
113 assert.deepEqual(board.review[0]!.on.sessions.map((session) => session.id), ["ses_3"]);
114 assert.deepEqual(board.review[0]!.on.people, ["bo"]);
115 // Done: the most recently closed first.
116 assert.deepEqual(board.done.map((card) => card.item.number), [5, 6]);
117});
118
119test("a phone shows the stage asked for, else the first with something in it", () => {
120 const board = boardOf([item(1, { assignees: ["ana"] })], {}, NOW);
121 assert.equal(shownStage("review", board), "review");
122 assert.equal(shownStage("nonsense", board), "progress");
123 assert.equal(shownStage(null, boardOf([], {}, NOW)), "backlog");
124});
125
126test("the board reads open pull requests in every repository an open work item is linked to, each once", () => {
127 const items = [
128 { state: "open", repos: [{ path: "acme/web" }, { path: "acme/api" }] },
129 { state: "open", repos: [{ path: "Acme/Web" }, { path: "acme/repo-21" }] },
130 // A closed item's repositories are not read: Done does not need its pull requests.
131 { state: "closed", repos: [{ path: "acme/old" }] },
132 // A repository deleted before work items kept no path.
133 { state: "open", repos: [{ path: "" }] },
134 // One deleted and not yet purged keeps its path, and has nothing to read.
135 { state: "open", repos: [{ path: "acme/hello", deleted: true }] },
136 ];
137 assert.deepEqual(pullRepos(items), ["acme/web", "acme/api", "acme/repo-21"]);
138 // However many there are: none is left out past a limit.
139 const many = Array.from({ length: 30 }, (_, index) => ({ state: "open", repos: [{ path: `acme/r${index}` }] }));
140 assert.equal(pullRepos(many).length, 30);
141});
142
143test("closed as not planned, a duplicate among them, is not Done: it did not ship", () => {
144 const closed = { state: "closed" as const, closedAt: new Date(NOW - DAY).toISOString() };
145 assert.equal(stageOf(item(1, { ...closed, reason: "completed" }), {}, NOW), "done");
146 assert.equal(stageOf(item(2, { ...closed, reason: "not_planned" }), {}, NOW), null);
147 const board = boardOf([item(1, { ...closed, reason: "completed" }), item(2, { ...closed, reason: "not_planned" })], {}, NOW);
148 assert.deepEqual(board.done.map((card) => card.item.number), [1]);
149});
150
151test("a merged pull request keeps an open item on the board: Landing with a target to land, else In progress", () => {
152 const target = [repo("acme/web", "origin"), repo("acme/api", "target")];
153 // It closes the item and merged, but not in the target: still to land there.
154 assert.equal(stageOf(item(1, { repos: target }), { pulls: pr("merged") }, NOW), "landing");
155 // Only part of it merged: more work to come.
156 assert.equal(stageOf(item(2, { repos: target }), { pulls: pr("part:merged") }, NOW), "progress");
157 // No target left to land, still open (merged into another branch, or reopened).
158 assert.equal(stageOf(item(3), { pulls: pr("merged") }, NOW), "progress");
159 // An open pull request beside a merged part: In review.
160 assert.equal(stageOf(item(4), { pulls: pr("part:merged", "open") }, NOW), "review");
161 // Nothing merged or open: where its claims put it.
162 assert.equal(stageOf(item(5, { repos: target }), { pulls: pr("closed") }, NOW), "backlog");
163});
164
165test("a pull request counts for its item, the items it closes and those it is part of, once each", () => {
166 const by = pullsByItem([
167 { repo: "acme/web", number: 158, item: 7, status: "merged", closes: [7, 8], parts: [9, 8] },
168 { repo: "Acme/Web", number: 158, item: 7, status: "merged", closes: [7] },
169 { repo: "acme/api", number: 3, item: null, status: "open", parts: [9] },
170 ]);
171 assert.deepEqual(by[7], [{ key: "acme/web#158", status: "merged", part: false }]);
172 assert.deepEqual(by[8], [{ key: "acme/web#158", status: "merged", part: false }]);
173 assert.deepEqual(by[9], [
174 { key: "acme/web#158", status: "merged", part: true },
175 { key: "acme/api#3", status: "open", part: true },
176 ]);
177});
178
179test("a card says how many of its pull requests merged, counting a live session's once", () => {
180 const items = [item(1), item(2, { state: "closed", closedAt: new Date(NOW).toISOString() })];
181 const pulls = pullsByItem([
182 { repo: "acme/web", number: 30, item: 1, status: "merged" },
183 { repo: "acme/web", number: 31, item: null, status: "merged", parts: [1] },
184 { repo: "acme/web", number: 32, item: 2, status: "merged" },
185 ]);
186 const board = boardOf(items, { sessions: [live(1, { pull: 30, pullStatus: "merged" })], pulls }, NOW);
187 assert.equal(board.progress[0]!.merged, 2);
188 // A closed one's are not counted: it is Done.
189 assert.equal(board.done[0]!.merged, 0);
190});
191
192test("every page is read, each below the last one's lowest number, up to a ceiling the board then names", async () => {
193 const numbers = Array.from({ length: 250 }, (_, index) => ({ number: 250 - index }));
194 const asked: (number | undefined)[] = [];
195 const read = async (before: number | undefined) => {
196 asked.push(before);
197 return numbers.filter((row) => before == null || row.number < before).slice(0, 100);
198 };
199 const all = await readPages(read, 100, 10);
200 assert.equal(all.rows.length, 250);
201 assert.equal(all.capped, false);
202 assert.deepEqual(asked, [undefined, 151, 51]);
203 const some = await readPages(read, 100, 2);
204 assert.equal(some.rows.length, 200);
205 assert.equal(some.capped, true);
206 // An exact page then nothing: one more read says it is the end.
207 const exact = await readPages(async (before) => (before == null ? numbers.slice(0, 100) : []), 100, 10);
208 assert.deepEqual([exact.rows.length, exact.capped], [100, false]);
209});
210
211test("what the board read only in part, it says", () => {
212 assert.deepEqual(cappedNotes({ items: null, done: null, pulls: [] }), []);
213 assert.deepEqual(cappedNotes({ items: 1000, done: 100, pulls: ["acme/web"] }), [
214 "Showing the newest 1,000 open work items. Older ones are on the Work list.",
215 "Done shows the 100 most recently closed.",
216 "Only the newest pull requests in acme/web were read, so some work items may stand a column early.",
217 ]);
218 assert.match(cappedNotes({ items: null, done: null, pulls: ["acme/web", "acme/api"] })[0]!, /in 2 repositories/);
219});