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