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