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.

244 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 {
106 claims: [claim(2, "cy")],
107 sessions: [live(3, { pull: 9, pullStatus: "open" }), live(1, { state: "ended" })],
108 pulls: pullsByItem([
109 { repo: "acme/web", number: 20, item: 4, status: "draft" },
110 { repo: "acme/web", number: 21, item: null, status: "open" },
111 ]),
112 },
113 NOW,
114 );
115 assert.deepEqual(
116 board.backlog.map((card) => card.item.number),
117 [1],
118 );
119 assert.deepEqual(
120 board.progress.map((card) => card.item.number),
121 [2, 4],
122 );
123 assert.deepEqual(board.progress[0]!.on.people, ["cy"]);
124 // A live session's open pull request puts its item in review, the session on the card.
125 assert.deepEqual(
126 board.review.map((card) => card.item.number),
127 [3],
128 );
129 assert.deepEqual(
130 board.review[0]!.on.sessions.map((session) => session.id),
131 ["ses_3"],
132 );
133 assert.deepEqual(board.review[0]!.on.people, ["bo"]);
134 // Done: the most recently closed first.
135 assert.deepEqual(
136 board.done.map((card) => card.item.number),
137 [5, 6],
138 );
139});
140
141test("a phone shows the stage asked for, else the first with something in it", () => {
142 const board = boardOf([item(1, { assignees: ["ana"] })], {}, NOW);
143 assert.equal(shownStage("review", board), "review");
144 assert.equal(shownStage("nonsense", board), "progress");
145 assert.equal(shownStage(null, boardOf([], {}, NOW)), "backlog");
146});
147
148test("the board reads open pull requests in every repository an open work item is linked to, each once", () => {
149 const items = [
150 { state: "open", repos: [{ path: "acme/web" }, { path: "acme/api" }] },
151 { state: "open", repos: [{ path: "Acme/Web" }, { path: "acme/repo-21" }] },
152 // A closed item's repositories are not read: Done does not need its pull requests.
153 { state: "closed", repos: [{ path: "acme/old" }] },
154 // A repository deleted before work items kept no path.
155 { state: "open", repos: [{ path: "" }] },
156 // One deleted and not yet purged keeps its path, and has nothing to read.
157 { state: "open", repos: [{ path: "acme/hello", deleted: true }] },
158 ];
159 assert.deepEqual(pullRepos(items), ["acme/web", "acme/api", "acme/repo-21"]);
160 // However many there are: none is left out past a limit.
161 const many = Array.from({ length: 30 }, (_, index) => ({ state: "open", repos: [{ path: `acme/r${index}` }] }));
162 assert.equal(pullRepos(many).length, 30);
163});
164
165test("closed as not planned, a duplicate among them, is not Done: it did not ship", () => {
166 const closed = { state: "closed" as const, closedAt: new Date(NOW - DAY).toISOString() };
167 assert.equal(stageOf(item(1, { ...closed, reason: "completed" }), {}, NOW), "done");
168 assert.equal(stageOf(item(2, { ...closed, reason: "not_planned" }), {}, NOW), null);
169 const board = boardOf([item(1, { ...closed, reason: "completed" }), item(2, { ...closed, reason: "not_planned" })], {}, NOW);
170 assert.deepEqual(
171 board.done.map((card) => card.item.number),
172 [1],
173 );
174});
175
176test("a merged pull request keeps an open item on the board: Landing with a target to land, else In progress", () => {
177 const target = [repo("acme/web", "origin"), repo("acme/api", "target")];
178 // It closes the item and merged, but not in the target: still to land there.
179 assert.equal(stageOf(item(1, { repos: target }), { pulls: pr("merged") }, NOW), "landing");
180 // Only part of it merged: more work to come.
181 assert.equal(stageOf(item(2, { repos: target }), { pulls: pr("part:merged") }, NOW), "progress");
182 // No target left to land, still open (merged into another branch, or reopened).
183 assert.equal(stageOf(item(3), { pulls: pr("merged") }, NOW), "progress");
184 // An open pull request beside a merged part: In review.
185 assert.equal(stageOf(item(4), { pulls: pr("part:merged", "open") }, NOW), "review");
186 // Nothing merged or open: where its claims put it.
187 assert.equal(stageOf(item(5, { repos: target }), { pulls: pr("closed") }, NOW), "backlog");
188});
189
190test("a pull request counts for its item, the items it closes and those it is part of, once each", () => {
191 const by = pullsByItem([
192 { repo: "acme/web", number: 158, item: 7, status: "merged", closes: [7, 8], parts: [9, 8] },
193 { repo: "Acme/Web", number: 158, item: 7, status: "merged", closes: [7] },
194 { repo: "acme/api", number: 3, item: null, status: "open", parts: [9] },
195 ]);
196 assert.deepEqual(by[7], [{ key: "acme/web#158", status: "merged", part: false }]);
197 assert.deepEqual(by[8], [{ key: "acme/web#158", status: "merged", part: false }]);
198 assert.deepEqual(by[9], [
199 { key: "acme/web#158", status: "merged", part: true },
200 { key: "acme/api#3", status: "open", part: true },
201 ]);
202});
203
204test("a card says how many of its pull requests merged, counting a live session's once", () => {
205 const items = [item(1), item(2, { state: "closed", closedAt: new Date(NOW).toISOString() })];
206 const pulls = pullsByItem([
207 { repo: "acme/web", number: 30, item: 1, status: "merged" },
208 { repo: "acme/web", number: 31, item: null, status: "merged", parts: [1] },
209 { repo: "acme/web", number: 32, item: 2, status: "merged" },
210 ]);
211 const board = boardOf(items, { sessions: [live(1, { pull: 30, pullStatus: "merged" })], pulls }, NOW);
212 assert.equal(board.progress[0]!.merged, 2);
213 // A closed one's are not counted: it is Done.
214 assert.equal(board.done[0]!.merged, 0);
215});
216
217test("every page is read, each below the last one's lowest number, up to a ceiling the board then names", async () => {
218 const numbers = Array.from({ length: 250 }, (_, index) => ({ number: 250 - index }));
219 const asked: (number | undefined)[] = [];
220 const read = async (before: number | undefined) => {
221 asked.push(before);
222 return numbers.filter((row) => before == null || row.number < before).slice(0, 100);
223 };
224 const all = await readPages(read, 100, 10);
225 assert.equal(all.rows.length, 250);
226 assert.equal(all.capped, false);
227 assert.deepEqual(asked, [undefined, 151, 51]);
228 const some = await readPages(read, 100, 2);
229 assert.equal(some.rows.length, 200);
230 assert.equal(some.capped, true);
231 // An exact page then nothing: one more read says it is the end.
232 const exact = await readPages(async (before) => (before == null ? numbers.slice(0, 100) : []), 100, 10);
233 assert.deepEqual([exact.rows.length, exact.capped], [100, false]);
234});
235
236test("what the board read only in part, it says", () => {
237 assert.deepEqual(cappedNotes({ items: null, done: null, pulls: [] }), []);
238 assert.deepEqual(cappedNotes({ items: 1000, done: 100, pulls: ["acme/web"] }), [
239 "Showing the newest 1,000 open work items. Older ones are on the Work list.",
240 "Done shows the 100 most recently closed.",
241 "Only the newest pull requests in acme/web were read, so some work items may stand a column early.",
242 ]);
243 assert.match(cappedNotes({ items: null, done: null, pulls: ["acme/web", "acme/api"] })[0]!, /in 2 repositories/);
244});