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.

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