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.

g1t.sh
639 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { ActivityDigest, ActorCount, ChatActivity, Deployment, InboxItem, RepoDigest, WorkflowRun } from "@g1t/contracts";
5
6import {
7 type AttentionRow,
8 type CodeNeed,
9 type CodePull,
10 actorKind,
11 attention,
12 chatRow,
13 codeRow,
14 dayIn,
15 deploysIn,
16 digestOf,
17 duration,
18 landed,
19 landedDeploys,
20 landedFromActivity,
21 landedIn,
22 notificationRow,
23 parseWindow,
24 rankAttention,
25 running,
26 runningDeploys,
27 runningWorkflows,
28 sinceWords,
29 spanFor,
30 spanMonths,
31 spanSentence,
32 spendIn,
33 startHere,
34 waitingSentence,
35} from "./home.ts";
36
37const HOUR = 3_600_000;
38const DAY = 24 * HOUR;
39// Friday 2026-10-09, 18:00 UTC; 11:00 in Los Angeles.
40const NOW = Date.parse("2026-10-09T18:00:00Z");
41const LA = "America/Los_Angeles";
42const repo = { namespace: "acme", name: "web" };
43const iso = (ms: number) => new Date(ms).toISOString();
44
45function pull(over: Partial<CodePull> = {}): CodePull {
46 return {
47 repo,
48 number: 1,
49 title: "Fix the flaky test",
50 status: "merged",
51 mergedAt: iso(NOW - HOUR),
52 createdAt: iso(NOW - 3 * HOUR),
53 updatedAt: iso(NOW - HOUR),
54 author: { username: "g1t", kind: "agent" },
55 ...over,
56 };
57}
58
59const need = (over: Partial<CodeNeed>): CodeNeed => ({
60 key: "pull:p1",
61 reason: "checks_failing",
62 repo,
63 ref: "#12",
64 title: "Add retries",
65 ask: "It failed in the merge queue.",
66 by: { name: "bo", agent: false },
67 at: NOW - 2 * HOUR,
68 to: "/acme/web/pull/12",
69 facts: [{ label: "Files changed", value: "4", tone: null }],
70 ...over,
71});
72
73test("Code's needs become rows with what is at stake", () => {
74 const failing = codeRow(need({}));
75 assert.equal(failing.kind, "pull_blocked");
76 assert.equal(failing.stake?.text, "Checks failing");
77 assert.equal(failing.action.label, "Look");
78 const deploy = codeRow(need({ key: "deploy:site", reason: "blocking", facts: [{ label: "Production", value: "Serving the build before", tone: "good" }] }));
79 assert.equal(deploy.kind, "deploy");
80 assert.equal(deploy.stake?.text, "Production on an older build");
81 assert.equal(codeRow(need({ key: "review:p2", reason: "needs_review", facts: [{ label: "Lines", value: "+10 −2", tone: null }] })).stake?.text, "+10 −2");
82 assert.equal(codeRow(need({ key: "runner:j1", reason: "stalled", facts: [{ label: "Waiting", value: "2 h", tone: "warn" }] })).stake?.text, "Waiting 2 h");
83});
84
85const row = (over: Partial<AttentionRow>): AttentionRow => ({
86 key: "k",
87 kind: "chat",
88 title: "t",
89 detail: "d",
90 stake: null,
91 action: { label: "Open", to: "/x" },
92 owner: null,
93 at: NOW,
94 from: "chat",
95 ...over,
96});
97
98test("Start here: the most pressing tier first, then what has waited longest", () => {
99 const rows = [
100 row({ key: "chat:old", kind: "chat", at: NOW - 5 * DAY, action: { label: "Reply", to: "/acme/-/chat/general" } }),
101 row({ key: "review:new", kind: "review", at: NOW - HOUR, action: { label: "Review", to: "/acme/web/pull/2" } }),
102 row({ key: "review:old", kind: "review", at: NOW - 3 * HOUR, action: { label: "Review", to: "/acme/web/pull/1" } }),
103 row({ key: "pull:p", kind: "pull_blocked", at: NOW - 10 * 60_000, action: { label: "Look", to: "/acme/web/pull/3" } }),
104 ];
105 const ranked = rankAttention(rows);
106 assert.deepEqual(ranked.map((r) => r.key), ["pull:p", "review:old", "review:new", "chat:old"]);
107 const start = startHere(ranked);
108 assert.equal(start?.row.key, "pull:p");
109 assert.match(start?.why ?? "", /can't land/);
110 // A failed production deploy beats everything.
111 const withDeploy = rankAttention([...rows, row({ key: "deploy:site", kind: "deploy", at: NOW, action: { label: "See the build", to: "/acme/site/deployments/1" } })]);
112 assert.equal(startHere(withDeploy)?.row.key, "deploy:site");
113 // Ties in tier say so.
114 const two = startHere(rankAttention(rows.filter((r) => r.kind === "review")));
115 assert.equal(two?.row.key, "review:old");
116 assert.match(two?.why ?? "", /Of the 2 like it, it has waited longest/);
117 assert.equal(startHere([]), null);
118});
119
120test("the same order whatever order the rows came in", () => {
121 const rows = [row({ key: "b", kind: "review", at: NOW }), row({ key: "a", kind: "review", at: NOW, action: { label: "Review", to: "/y" } })];
122 assert.deepEqual(rankAttention(rows).map((r) => r.key), ["a", "b"]);
123 assert.deepEqual(rankAttention([...rows].reverse()).map((r) => r.key), ["a", "b"]);
124});
125
126const item = (over: Partial<InboxItem>): InboxItem => ({
127 id: "n1",
128 reason: "ci_activity",
129 severity: "error",
130 title: "Checks failed on #12",
131 body: "Add retries",
132 event: "check.failed",
133 repo: "acme/web",
134 workspace: "acme",
135 subject: "pull",
136 number: 12,
137 url: "/acme/web/pull/12",
138 actor: "g1t",
139 count: 1,
140 createdAt: iso(NOW - HOUR),
141 updatedAt: iso(NOW - HOUR),
142 readAt: null,
143 doneAt: null,
144 saved: false,
145 snoozedUntil: null,
146 ...over,
147});
148
149test("notifications: warnings and failures in this workspace; none about repositories without Code", () => {
150 assert.equal(notificationRow(item({}), "acme", true)?.kind, "notification");
151 assert.equal(notificationRow(item({}), "acme", false), null);
152 assert.equal(notificationRow(item({ repo: null, url: "/acme/-/chat/general" }), "acme", false)?.kind, "notification");
153 // A reason this page does not know, older ones included, reads as a plain notification.
154 assert.equal(notificationRow(item({ reason: "agent" as string as InboxItem["reason"] }), "acme", true)?.stake?.text, "Notification");
155 assert.equal(notificationRow(item({ severity: "info" }), "acme", true), null);
156 assert.equal(notificationRow(item({ workspace: "other", repo: "other/web" }), "acme", true), null);
157 assert.equal(notificationRow(item({ readAt: iso(NOW) }), "acme", true), null);
158});
159
160test("a notification about something already listed is left out", () => {
161 const out = attention({
162 slug: "acme",
163 code: true,
164 codeNeeds: [need({})],
165 notifications: [item({})],
166 chat: [],
167 });
168 assert.deepEqual(out.rows.map((r) => r.key), ["pull:p1"]);
169 assert.deepEqual(out.missing, []);
170});
171
172test("without Code access no Code row is asked for or shown, and a silent source is named", () => {
173 const out = attention({
174 slug: "acme",
175 code: false,
176 codeNeeds: [need({})],
177 notifications: [item({}), item({ id: "n2", repo: null, reason: "mention", severity: "warning", url: "/acme/-/chat/general" })],
178 chat: null,
179 });
180 assert.deepEqual(out.rows.map((r) => r.key), ["notification:n2"]);
181 assert.equal(out.rows.some((r) => r.from === "code"), false);
182 assert.deepEqual(out.missing, ["Chat"]);
183});
184
185test("chat: mentions anywhere and unread direct messages, never muted", () => {
186 const channel = { id: "c1", kind: "channel" as const, name: "general", last_message_at: iso(NOW) };
187 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 0 }, "acme"), null);
188 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 2 }, "acme")?.stake?.text, "2 mentions");
189 assert.equal(chatRow({ channel, title: "general", muted: true, unread: 4, mentions: 2 }, "acme"), null);
190 const dm = chatRow({ channel: { ...channel, kind: "dm", name: null }, title: "Bo Reed", muted: false, unread: 1, mentions: 0 }, "acme");
191 assert.equal(dm?.action.to, "/acme/-/chat/dm/c1");
192});
193
194// The span is NOW back to 3 days ago unless a test says otherwise.
195const SPAN = { from: NOW - 3 * DAY, now: NOW };
196
197test("a day is the reader's day", () => {
198 assert.equal(dayIn(NOW, LA), "2026-10-09");
199 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), LA), "2026-10-09");
200 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), null), "2026-10-10");
201 assert.equal(dayIn(NOW, "Not/AZone"), "2026-10-09");
202});
203
204test("since your last visit, in words, in the reader's time zone", () => {
205 // NOW is Friday 11:00 in Los Angeles.
206 assert.equal(sinceWords(NOW - 20_000, NOW, LA), "a moment ago");
207 assert.equal(sinceWords(NOW - 40 * 60_000, NOW, LA), "40 minutes ago");
208 assert.equal(sinceWords(NOW - HOUR - 60_000, NOW, LA), "an hour ago");
209 assert.equal(sinceWords(NOW - 2 * HOUR, NOW, LA), "2 hours ago");
210 // 07:00 Friday.
211 assert.equal(sinceWords(NOW - 4 * HOUR, NOW, LA), "this morning");
212 // 02:00 Friday is Thursday night.
213 assert.equal(sinceWords(NOW - 9 * HOUR, NOW, LA), "last night");
214 // 19:00 Thursday.
215 assert.equal(sinceWords(NOW - 16 * HOUR, NOW, LA), "yesterday evening");
216 // 19:00 Tuesday.
217 assert.equal(sinceWords(NOW - 2 * DAY - 16 * HOUR, NOW, LA), "Tuesday evening");
218 // 02:00 Wednesday is Tuesday night.
219 assert.equal(sinceWords(NOW - 2 * DAY - 9 * HOUR, NOW, LA), "Tuesday night");
220 assert.equal(sinceWords(NOW - 9 * DAY, NOW, LA), "Sep 30");
221});
222
223test("how long a span is", () => {
224 assert.equal(duration(30_000), "a moment");
225 assert.equal(duration(45 * 60_000), "45 minutes");
226 assert.equal(duration(HOUR), "1 hour");
227 assert.equal(duration(3 * DAY + HOUR), "3 days");
228 assert.equal(duration(15 * DAY), "2 weeks");
229});
230
231test("the span: since your last visit, capped, or a fixed window", () => {
232 assert.equal(parseWindow("24h"), "24h");
233 assert.equal(parseWindow("7d"), "7d");
234 assert.equal(parseWindow("nonsense"), "last");
235 assert.equal(parseWindow(null), "last");
236 const away = spanFor("last", NOW, NOW - 2 * DAY - 16 * HOUR, LA);
237 assert.equal(away.from, NOW - 2 * DAY - 16 * HOUR);
238 assert.equal(away.words, "Since Tuesday evening");
239 assert.equal(away.length, "2 days");
240 assert.equal(away.note, null);
241 // Never visited: the last 24 hours, and it says why.
242 const first = spanFor("last", NOW, null, LA);
243 assert.equal(first.from, NOW - DAY);
244 assert.equal(first.note, "first");
245 assert.equal(spanFor("last", NOW, undefined, LA).note, "unknown");
246 // Away a month: 14 days at most.
247 const capped = spanFor("last", NOW, NOW - 30 * DAY, LA);
248 assert.equal(capped.from, NOW - 14 * DAY);
249 assert.equal(capped.note, "capped");
250 assert.equal(capped.words, "In the last 14 days");
251 // The fixed windows ignore the last visit.
252 assert.equal(spanFor("24h", NOW, NOW - 3 * DAY, LA).from, NOW - DAY);
253 assert.deepEqual([spanFor("7d", NOW, NOW - HOUR, LA).from, spanFor("7d", NOW, NOW - HOUR, LA).words], [NOW - 7 * DAY, "In the last 7 days"]);
254});
255
256// --- People ------------------------------------------------------------
257
258const ANA = "user:usr_ana";
259const BO = "user:usr_bo";
260const CY = "user:usr_cy";
261const MARGO = "agent:agt_margo";
262const G1T = "user:usr_g1t_agent";
263const NAMES = { [ANA]: "ana", [BO]: "bo", [CY]: "cy", [MARGO]: "margo" };
264const REPOS = {
265 rep_web: { namespace: "acme", name: "web", defaultBranch: "main" },
266 rep_api: { namespace: "acme", name: "api", defaultBranch: "main" },
267 rep_docs: { namespace: "acme", name: "docs", defaultBranch: "main" },
268 rep_cli: { namespace: "acme", name: "cli", defaultBranch: "trunk" },
269};
270
271const count = (actor: string, n: number): ActorCount => ({ actor, count: n });
272const quietRepo = (repo_id: string): RepoDigest => ({
273 repo_id,
274 pushes: { count: 0, commits: 0, branches: [], by: [], default_branch: { count: 0, commits: 0, by: [], last_at: null } },
275 pulls: { opened: [], merged: [], closed: [] },
276 issues: { opened: [], closed: [] },
277 reviews: [],
278 comments: [],
279 deployments: { succeeded: [], failed: [], production: 0 },
280 releases: [],
281 packages: [],
282});
283
284/**
285 * A day on a busy workspace: 3 people pushed 41 commits to 4 projects,
286 * merged 5 pull requests and opened 2; older agent rows (g1t and Margo)
287 * pushed 6, opened 3 pull requests and reviewed 2, and are not people;
288 * 2 production deploys went out; 120 messages were sent, 15 of them
289 * Margo's; 7 docs were edited.
290 */
291function busyDay(): ActivityDigest {
292 return {
293 from: iso(SPAN.from),
294 until: iso(SPAN.now),
295 complete: true,
296 repos: [
297 {
298 ...quietRepo("rep_web"),
299 pushes: {
300 count: 14,
301 commits: 33,
302 branches: ["fix-login", "main"],
303 by: [
304 { actor: ANA, count: 8, commits: 24 },
305 { actor: BO, count: 4, commits: 5 },
306 { actor: G1T, count: 2, commits: 4 },
307 ],
308 default_branch: { count: 3, commits: 9, by: [{ actor: ANA, count: 3, commits: 9 }], last_at: iso(NOW - 2 * HOUR) },
309 },
310 pulls: { opened: [count(ANA, 1), count(G1T, 2), count(MARGO, 1)], merged: [count(ANA, 3), count(BO, 1)], closed: [] },
311 issues: { opened: [count(BO, 2)], closed: [count(ANA, 1), count(G1T, 1)] },
312 reviews: [count(BO, 2), count(G1T, 2)],
313 comments: [count(ANA, 4), count(MARGO, 3)],
314 deployments: { succeeded: [count(ANA, 2)], failed: [], production: 2 },
315 releases: [{ tag: "v1.4.0", name: "Autumn", actor: ANA, at: iso(NOW - 3 * HOUR) }],
316 packages: [],
317 },
318 {
319 ...quietRepo("rep_api"),
320 pushes: { count: 5, commits: 8, branches: ["main"], by: [{ actor: CY, count: 5, commits: 8 }], default_branch: { count: 5, commits: 8, by: [{ actor: CY, count: 5, commits: 8 }], last_at: iso(NOW - 5 * HOUR) } },
321 pulls: { opened: [count(CY, 1)], merged: [count(CY, 1)], closed: [] },
322 packages: [{ ecosystem: "container", name: "api", version: "1.4.0", actor: G1T, at: iso(NOW - HOUR) }],
323 },
324 {
325 ...quietRepo("rep_docs"),
326 pushes: { count: 2, commits: 2, branches: ["typo"], by: [{ actor: BO, count: 2, commits: 2 }], default_branch: { count: 0, commits: 0, by: [], last_at: null } },
327 },
328 {
329 ...quietRepo("rep_cli"),
330 pushes: { count: 3, commits: 4, branches: ["trunk"], by: [{ actor: ANA, count: 1, commits: 2 }, { actor: MARGO, count: 2, commits: 2 }], default_branch: { count: 3, commits: 4, by: [{ actor: ANA, count: 1, commits: 2 }, { actor: MARGO, count: 2, commits: 2 }], last_at: iso(NOW - 30 * 60_000) } },
331 },
332 ],
333 folios: {
334 created: [count(ANA, 1)],
335 edited_count: 7,
336 edited: [
337 { folio_id: "fol_1", kind: "doc", authors: [ANA, MARGO] },
338 { folio_id: "fol_2", kind: "doc", authors: [ANA] },
339 { folio_id: "fol_3", kind: "doc", authors: [BO] },
340 { folio_id: "fol_4", kind: "doc", authors: [BO] },
341 { folio_id: "fol_5", kind: "doc", authors: [CY] },
342 { folio_id: "fol_6", kind: "doc", authors: [CY] },
343 { folio_id: "fol_7", kind: "doc", authors: [MARGO] },
344 ],
345 },
346 };
347}
348
349const busyChat: ChatActivity = {
350 from: iso(SPAN.from),
351 until: iso(SPAN.now),
352 messages: 120,
353 channels: 5,
354 authors: [
355 { key: ANA, messages: 60 },
356 { key: BO, messages: 30 },
357 { key: CY, messages: 15 },
358 { key: MARGO, messages: 15 },
359 ],
360};
361
362test("an actor is not a person when it is g1t or an agent of older rows; everyone else is", () => {
363 assert.equal(actorKind(MARGO), "agent");
364 assert.equal(actorKind(G1T), "agent");
365 assert.equal(actorKind("user:agt_01abc"), "agent", "an agent named by its own id under user: is still a robot");
366 assert.equal(actorKind("user:wsp_01abc"), "agent", "the workspace's own upkeep is g1t's");
367 assert.equal(actorKind("user:g1t"), "agent");
368 assert.equal(actorKind(ANA), "person");
369 assert.equal(actorKind(""), "person");
370 assert.equal(actorKind("user:usr_bot", new Set(["user:usr_bot"])), "agent");
371});
372
373test("the digest: what people did, in lines with counts, and who", () => {
374 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true });
375 assert.deepEqual(
376 digest.people.lines.map((line) => [line.text, line.to]),
377 [
378 ["3 people pushed 41 commits to 4 projects", "/acme/-/repositories"],
379 ["opened 2 pull requests, merged 5", "/acme/-/repositories"],
380 ["reviewed 2 pull requests", "/acme/web/pulls"],
381 ["opened 2 issues, closed 1", "/acme/web/issues"],
382 ["left 4 comments", "/acme/web/issues"],
383 ["2 deploys went out", "/acme/web/deployments"],
384 ["published 1 release", "/acme/web/releases"],
385 ["created 1 doc, edited 6 docs", "/acme/-/artifacts"],
386 ["sent 105 messages in 5 conversations", "/acme/-/chat"],
387 ],
388 );
389 // Faces: most active first, by name.
390 assert.deepEqual(
391 digest.people.actors.map((actor) => actor.name),
392 ["ana", "bo", "cy"],
393 );
394 assert.deepEqual(digest.summary, { people: 3, commits: 41, merged: 5, said: 105, anything: true, peopleRead: true });
395 assert.deepEqual(digest.missing, []);
396 assert.equal(digest.partial, false);
397});
398
399test("the digest says which part didn't answer, and counts what could be read", () => {
400 const noChat = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: null, names: NAMES, code: true });
401 assert.deepEqual(noChat.missing, ["Chat"]);
402 assert.equal(noChat.people.lines.some((line) => line.key === "chat"), false);
403 const noCode = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: busyChat, names: NAMES, code: true });
404 assert.deepEqual(noCode.missing, ["Code"]);
405 assert.deepEqual(
406 noCode.people.lines.map((line) => line.text),
407 ["sent 105 messages in 5 conversations"],
408 );
409 assert.equal(noCode.summary.peopleRead, true);
410 // Without Code access, events are read for artifacts only: no repositories, nothing missing.
411 const member = digestOf({ slug: "acme", repos: {}, activity: { ...busyDay(), repos: [] }, chat: busyChat, names: NAMES, code: false });
412 assert.deepEqual(member.missing, []);
413 assert.deepEqual(
414 member.people.lines.map((line) => line.key),
415 ["docs", "chat"],
416 );
417 // A digest cut short says so.
418 const cut = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), complete: false }, chat: busyChat, names: NAMES, code: true });
419 assert.equal(cut.partial, true);
420 // Nobody named: an import's pushes are still commits.
421 const imported = digestOf({
422 slug: "acme",
423 repos: REPOS,
424 activity: { ...busyDay(), folios: null, repos: [{ ...quietRepo("rep_web"), pushes: { count: 2, commits: 7, branches: ["main"], by: [{ actor: "", count: 2, commits: 7 }], default_branch: { count: 2, commits: 7, by: [{ actor: "", count: 2, commits: 7 }], last_at: iso(NOW - HOUR) } } }] },
425 chat: null,
426 names: {},
427 code: true,
428 });
429 assert.deepEqual(
430 imported.people.lines.map((line) => [line.text, line.to]),
431 [["7 commits pushed to 1 project", "/acme/web/activity"]],
432 );
433 assert.equal(imported.summary.people, 0);
434});
435
436test("the sentence: people first, then what landed; honest when quiet or unread", () => {
437 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true });
438 assert.equal(
439 spanSentence({ words: "Since Tuesday evening" }, digest, 12),
440 "Since Tuesday evening, 3 people pushed 41 commits and merged 5 changes and 12 changes landed.",
441 );
442 const quiet = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: { created: [], edited: [], edited_count: 0 } }, chat: { ...busyChat, messages: 0, channels: 0, authors: [] }, names: {}, code: true });
443 assert.equal(spanSentence({ words: "Since Tuesday evening" }, quiet, 0), "Quiet since Tuesday evening: nothing pushed, merged, deployed or said.");
444 assert.equal(spanSentence({ words: "In the last 24 hours" }, quiet, 0), "Quiet in the last 24 hours: nothing pushed, merged, deployed or said.");
445 // Only merges.
446 const mergesOnly = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: { ...busyChat, authors: [], messages: 0, channels: 0 }, names: {}, code: true });
447 assert.equal(spanSentence({ words: "Since this morning" }, mergesOnly, 5), "Since this morning, 5 changes landed.");
448 // People only talked.
449 const talked = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: busyChat, names: NAMES, code: true });
450 assert.equal(spanSentence({ words: "Since an hour ago" }, talked, 0), "Since an hour ago, 3 people sent 105 messages.");
451 // Neither events nor chat answered.
452 const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true });
453 assert.equal(spanSentence({ words: "Since this morning" }, unread, 5), "Since this morning, 5 changes landed; what people did couldn't be read.");
454 const nothingRead = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true });
455 assert.equal(spanSentence({ words: "Since this morning" }, nothingRead, 0), "Since this morning, what people did couldn't be read.");
456 assert.equal(spanSentence({ words: "Since this morning" }, null, null), "Since this morning, what happened couldn't be read.");
457 assert.equal(waitingSentence(0), "Nothing needs you right now.");
458 assert.equal(waitingSentence(1), "1 thing needs you.");
459 assert.equal(waitingSentence(8), "8 things need you.");
460});
461
462test("what landed: every merge in the span, newest first, g1t's older ones as @g1t", () => {
463 const merged = landedIn(
464 [
465 pull({ number: 1, mergedAt: iso(NOW - 2 * DAY), mergedBy: "bo" }),
466 pull({ number: 2, author: { username: "bo", kind: "user" }, mergedAt: iso(NOW - HOUR) }),
467 pull({ number: 3, mergedAt: iso(NOW - 5 * DAY) }),
468 pull({ number: 4, status: "open", mergedAt: null }),
469 ],
470 SPAN,
471 );
472 assert.deepEqual(merged.map((change) => [change.kind, change.title, change.detail]), [
473 ["merge", "Fix the flaky test", "web#2 · by bo"],
474 ["merge", "Fix the flaky test", "web#1 · by @g1t, merged by bo"],
475 ]);
476});
477
478test("what landed, besides merges: direct pushes, releases, packages and production builds, newest first", () => {
479 const fromActivity = landedFromActivity(busyDay(), REPOS, NAMES, "acme");
480 assert.deepEqual(
481 fromActivity.map((change) => [change.kind, change.title, change.detail, change.to]),
482 [
483 ["push", "9 commits pushed to main", "web · 3 pushes by ana", "/acme/web/activity"],
484 ["release", "v1.4.0 · Autumn", "web · released by ana", "/acme/web/releases/tag/v1.4.0"],
485 ["push", "8 commits pushed to main", "api · 5 pushes by cy", "/acme/api/activity"],
486 ["package", "api 1.4.0", "container package · published by @g1t", "/acme/-/packages"],
487 ["push", "4 commits pushed to trunk", "cli · 3 pushes by ana and @margo", "/acme/cli/activity"],
488 ],
489 );
490 const builds = landedDeploys([
491 { key: "deploy:site:d1", project: "site", kind: "production", outcome: "live", commit: "abcdef1", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d1", by: "@g1t" },
492 { key: "deploy:site:d2", project: "site", kind: "preview", outcome: "live", commit: "abcdef2", branch: "fix", at: NOW - HOUR, to: "/acme/site/deployments/d2", by: "bo" },
493 { key: "deploy:site:d3", project: "site", kind: "production", outcome: "failed", commit: "abcdef3", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d3", by: "bo" },
494 ]);
495 assert.deepEqual(builds.map((change) => [change.kind, change.title]), [["deploy", "Production of site went live"]]);
496 const all = landed([fromActivity, builds]);
497 assert.deepEqual(
498 all.map((change) => change.key),
499 ["push:rep_cli", "landed:deploy:site:d1", "package:rep_api:container:api:1.4.0", "push:rep_web", "release:rep_web:v1.4.0", "push:rep_api"],
500 );
501});
502
503const deploy = (over: Partial<Deployment>): Deployment => ({
504 id: "d1",
505 kind: "production",
506 branch: null,
507 number: null,
508 commit: "abcdef1234",
509 status: "ready",
510 url: "https://site.g1t.page",
511 error: null,
512 warnings: [],
513 buildSeconds: 40,
514 createdBy: "bo",
515 createdAt: iso(NOW - 2 * HOUR),
516 finishedAt: iso(NOW - HOUR),
517 ...over,
518});
519
520test("builds that finished in the span: live or failed, never skipped", () => {
521 const rows = deploysIn(
522 [
523 {
524 slug: "site",
525 deployments: [
526 deploy({}),
527 deploy({ id: "d2", status: "replaced", finishedAt: iso(NOW - DAY) }),
528 deploy({ id: "d3", status: "failed", finishedAt: iso(NOW - 2 * DAY) }),
529 deploy({ id: "d4", status: "skipped", finishedAt: iso(NOW - HOUR) }),
530 deploy({ id: "d5", status: "building", finishedAt: null }),
531 deploy({ id: "d6", finishedAt: iso(NOW - 5 * DAY) }),
532 ],
533 },
534 ],
535 "acme",
536 SPAN,
537 { "user:bo": "bo" },
538 );
539 assert.deepEqual(rows.map((row) => [row.key, row.outcome]), [
540 ["deploy:site:d1", "live"],
541 ["deploy:site:d2", "live"],
542 ["deploy:site:d3", "failed"],
543 ]);
544 assert.equal(rows[0]!.to, "/acme/site/deployments/d1");
545 assert.equal(rows[0]!.commit, "abcdef1");
546 // The maker is named, never shown as an id; an older agent's by its handle; an id identity doesn't know as someone.
547 assert.equal(rows[0]!.by, "bo");
548 const unknown = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "usr_zzz" })] }], "acme", SPAN);
549 assert.equal(unknown[0]!.by, "someone");
550 const robot = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "agt_01x" })] }], "acme", SPAN, { "user:agt_01x": "otto" });
551 assert.equal(robot[0]!.by, "@otto");
552});
553
554test("running now: workflow runs and builds going", () => {
555 const builds = runningDeploys(
556 [
557 { slug: "site", enabled: true, production: null, previews: 0, latest: deploy({ status: "building", finishedAt: null }) },
558 { slug: "docs", enabled: true, production: null, previews: 0, latest: deploy({}) },
559 ],
560 "acme",
561 );
562 assert.deepEqual(builds.map((row) => row.status), ["Building"]);
563 const run = (over: Partial<WorkflowRun>): WorkflowRun => ({
564 id: "run_1",
565 workflowId: "wf_1",
566 path: ".g1t/workflows/ci.yml",
567 name: "CI",
568 title: "Fix the flaky test",
569 number: 12,
570 attempt: 1,
571 event: "push",
572 ref: "refs/heads/main",
573 sha: "abcdef1",
574 pull: null,
575 status: "in_progress",
576 conclusion: null,
577 error: null,
578 actor: "ana",
579 createdAt: iso(NOW - 10 * 60_000),
580 startedAt: iso(NOW - 9 * 60_000),
581 finishedAt: null,
582 ...over,
583 });
584 const workflows = runningWorkflows([
585 { repo, runs: [run({}), run({ id: "run_2", status: "queued", startedAt: null, title: "" }), run({ id: "run_3", status: "completed", conclusion: "success" }), run({ id: "run_4", status: "action_required" })] },
586 ]);
587 assert.deepEqual(
588 workflows.map((row) => [row.key, row.title, row.detail, row.status, row.to]),
589 [
590 ["workflow:acme/web:run_1", "CI: Fix the flaky test", "acme/web main · by ana", "Running", "/acme/web/actions/runs/run_1"],
591 ["workflow:acme/web:run_2", "CI", "acme/web main · by ana", "Queued", "/acme/web/actions/runs/run_2"],
592 ["workflow:acme/web:run_4", "CI: Fix the flaky test", "acme/web main · by ana", "Needs approval", "/acme/web/actions/runs/run_4"],
593 ],
594 );
595 // Longest-going first within a kind: workflow runs, then builds.
596 assert.deepEqual(running([builds, workflows.slice(0, 1)]).map((row) => row.key), ["workflow:acme/web:run_1", "deploy:site:d1"]);
597});
598
599
600test("the span's months: one, or the ones it crosses", () => {
601 assert.deepEqual(spanMonths(SPAN), ["2026-10"]);
602 assert.deepEqual(spanMonths({ from: Date.parse("2026-09-28T00:00:00Z"), now: NOW }), ["2026-09", "2026-10"]);
603 assert.deepEqual(spanMonths({ from: Date.parse("2025-12-30T00:00:00Z"), now: Date.parse("2026-01-02T00:00:00Z") }), ["2025-12", "2026-01"]);
604});
605
606test("spend in the span: the UTC days it covers across months, at price, without money in", () => {
607 const september = {
608 groups: [
609 { key: "2026-09-29", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 2, chargedMicros: 1_000_000, costMicros: 0 }] },
610 { key: "2026-09-20", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 9, chargedMicros: 9_000_000, costMicros: 0 }] },
611 ],
612 totals: { chargedMicros: 10_000_000, paidMicros: 0, costMicros: 0, entries: 2 },
613 };
614 const october = {
615 groups: [
616 {
617 key: "2026-10-09",
618 label: "2026-10-09",
619 chargedMicros: 0,
620 lines: [
621 { kind: "Workflow minutes", count: 12, chargedMicros: 3_000_000, costMicros: 2_500_000, priceMicros: 4_000_000 },
622 { kind: "Sandbox time", count: 12, chargedMicros: 600_000, costMicros: 500_000 },
623 { kind: "Payments", count: 1, chargedMicros: -50_000_000, costMicros: 0, priceMicros: 0 },
624 ],
625 },
626 ],
627 totals: { chargedMicros: 12_600_000, paidMicros: 50_000_000, costMicros: 0, entries: 4, priceMicros: 13_600_000 },
628 };
629 const spend = spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, 13_609_000);
630 assert.deepEqual([spend.from, spend.to], ["2026-09-29", "2026-10-09"]);
631 assert.equal(spend.totalMicros, 5_600_000);
632 assert.deepEqual(spend.lines.map((l) => [l.kind, l.micros, l.count]), [
633 ["Workflow minutes", 5_000_000, 14],
634 ["Sandbox time", 600_000, 12],
635 ]);
636 // The month so far is billing's usage report, the top bar's figure, not the statement: it carries what is not yet closed.
637 assert.equal(spend.monthMicros, 13_609_000);
638 assert.equal(spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, null).monthMicros, null);
639});