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.

838 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 was done, by kind with counts, and who, agents marked", () => {
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.key, line.text, line.to]),
400 [
401 ["pushes", "47 commits to 4 projects", "/acme/-/repositories"],
402 ["pulls", "5 opened, 5 merged, 4 reviewed", "/acme/-/repositories"],
403 ["issues", "2 opened, 2 closed, 7 comments", "/acme/web/issues"],
404 ["shipped", "2 deploys, 1 release, 1 package version", "/acme/web/deployments"],
405 ["docs", "1 doc created, 7 docs edited", "/acme/-/artifacts"],
406 ["chat", "120 messages in 5 conversations", "/acme/-/chat"],
407 ],
408 );
409 // Rows: most active first, people and agents, each marked; nobody is "someone".
410 assert.deepEqual(
411 digest.people.actors.map((actor) => [actor.name, actor.kind]),
412 digest.people.actors.map((actor) => [actor.name, actor.name === "margo" || actor.name === "g1t" ? "agent" : "person"]),
413 );
414 assert.deepEqual(new Set(digest.people.actors.map((actor) => actor.name)), new Set(["ana", "bo", "cy", "margo", "g1t"]));
415 assert.equal(digest.people.actors[0]!.name, "ana");
416 // Each row's parts add up to its count, and the kinds' counts to every row's.
417 for (const actor of digest.people.actors) {
418 assert.equal(Object.values(actor.parts).reduce((a, b) => a + b, 0), actor.count, actor.name);
419 }
420 // The summary still counts people only.
421 assert.deepEqual(digest.summary, { people: 3, commits: 41, merged: 5, said: 105, anything: true, peopleRead: true });
422 assert.deepEqual(digest.missing, []);
423 assert.equal(digest.partial, false);
424});
425
426test("g1t is one row, named, whichever key it used: its account, a workspace, or chat's system:g1t", () => {
427 const chat: ChatActivity = { ...busyChat, messages: 5, channels: 1, authors: [{ key: "system:g1t", messages: 3 }, { key: ANA, messages: 2 }] };
428 const activity = { ...busyDay(), folios: null, repos: [{ ...quietRepo("rep_web"), comments: [count(G1T, 2), count("user:wsp_acme", 1)] }] };
429 const digest = digestOf({ slug: "acme", repos: REPOS, activity, chat, names: { [ANA]: "ana", "user:wsp_acme": "g1t", "system:g1t": "g1t" }, code: true });
430 assert.deepEqual(
431 digest.people.actors.map((actor) => [actor.name, actor.kind, actor.count, actor.parts]),
432 [
433 ["g1t", "agent", 6, { issues: 3, chat: 3 }],
434 ["ana", "person", 2, { chat: 2 }],
435 ],
436 );
437 assert.equal(digest.summary.people, 1);
438 // A person whose name could not be read is an unknown account, never "someone".
439 const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: { ...chat, authors: [{ key: "user:usr_gone", messages: 1 }] }, names: {}, code: true });
440 assert.deepEqual(unread.people.actors.map((actor) => actor.name), ["unknown account"]);
441});
442
443test("the digest says which part didn't answer, and counts what could be read", () => {
444 const noChat = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: null, names: NAMES, code: true });
445 assert.deepEqual(noChat.missing, ["Chat"]);
446 assert.equal(noChat.people.lines.some((line) => line.key === "chat"), false);
447 const noCode = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: busyChat, names: NAMES, code: true });
448 assert.deepEqual(noCode.missing, ["Code"]);
449 assert.deepEqual(
450 noCode.people.lines.map((line) => line.text),
451 ["120 messages in 5 conversations"],
452 );
453 assert.equal(noCode.summary.peopleRead, true);
454 // Without Code access, events are read for artifacts only: no repositories, nothing missing.
455 const member = digestOf({ slug: "acme", repos: {}, activity: { ...busyDay(), repos: [] }, chat: busyChat, names: NAMES, code: false });
456 assert.deepEqual(member.missing, []);
457 assert.deepEqual(
458 member.people.lines.map((line) => line.key),
459 ["docs", "chat"],
460 );
461 // A digest cut short says so.
462 const cut = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), complete: false }, chat: busyChat, names: NAMES, code: true });
463 assert.equal(cut.partial, true);
464 // Nobody named: an import's pushes are still commits.
465 const imported = digestOf({
466 slug: "acme",
467 repos: REPOS,
468 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) } } }] },
469 chat: null,
470 names: {},
471 code: true,
472 });
473 assert.deepEqual(
474 imported.people.lines.map((line) => [line.text, line.to]),
475 [["7 commits to 1 project", "/acme/web/activity"]],
476 );
477 assert.equal(imported.summary.people, 0);
478});
479
480test("the sentence: people first, then what landed; honest when quiet or unread", () => {
481 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true });
482 assert.equal(
483 spanSentence({ words: "Since Tuesday evening" }, digest, 12),
484 "Since Tuesday evening, 3 people pushed 41 commits and merged 5 changes and 12 changes landed.",
485 );
486 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 });
487 assert.equal(spanSentence({ words: "Since Tuesday evening" }, quiet, 0), "Quiet since Tuesday evening: nothing pushed, merged, deployed or said.");
488 assert.equal(spanSentence({ words: "In the last 24 hours" }, quiet, 0), "Quiet in the last 24 hours: nothing pushed, merged, deployed or said.");
489 // Only merges.
490 const mergesOnly = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: { ...busyChat, authors: [], messages: 0, channels: 0 }, names: {}, code: true });
491 assert.equal(spanSentence({ words: "Since this morning" }, mergesOnly, 5), "Since this morning, 5 changes landed.");
492 // People only talked.
493 const talked = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: busyChat, names: NAMES, code: true });
494 assert.equal(spanSentence({ words: "Since an hour ago" }, talked, 0), "Since an hour ago, 3 people sent 105 messages.");
495 // Neither events nor chat answered.
496 const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true });
497 assert.equal(spanSentence({ words: "Since this morning" }, unread, 5), "Since this morning, 5 changes landed; what people did couldn't be read.");
498 const nothingRead = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true });
499 assert.equal(spanSentence({ words: "Since this morning" }, nothingRead, 0), "Since this morning, what people did couldn't be read.");
500 assert.equal(spanSentence({ words: "Since this morning" }, null, null), "Since this morning, what happened couldn't be read.");
501 assert.equal(waitingSentence(0), "Nothing needs you right now.");
502 assert.equal(waitingSentence(1), "1 thing needs you.");
503 assert.equal(waitingSentence(8), "8 things need you.");
504});
505
506test("what landed: every merge in the span, newest first, g1t's older ones as @g1t", () => {
507 const merged = landedIn(
508 [
509 pull({ number: 1, mergedAt: iso(NOW - 2 * DAY), mergedBy: "bo" }),
510 pull({ number: 2, author: { username: "bo", kind: "user" }, mergedAt: iso(NOW - HOUR) }),
511 pull({ number: 3, mergedAt: iso(NOW - 5 * DAY) }),
512 pull({ number: 4, status: "open", mergedAt: null }),
513 ],
514 SPAN,
515 );
516 assert.deepEqual(merged.map((change) => [change.kind, change.title, change.detail]), [
517 ["merge", "Fix the flaky test", "web#2 · by bo"],
518 ["merge", "Fix the flaky test", "web#1 · by @g1t, merged by bo"],
519 ]);
520});
521
522test("what landed, besides merges: direct pushes, releases, packages and production builds, newest first", () => {
523 const fromActivity = landedFromActivity(busyDay(), REPOS, NAMES, "acme");
524 assert.deepEqual(
525 fromActivity.map((change) => [change.kind, change.title, change.detail, change.to]),
526 [
527 ["push", "9 commits pushed to main", "web · 3 pushes by ana", "/acme/web/activity"],
528 ["release", "v1.4.0 · Autumn", "web · released by ana", "/acme/web/releases/tag/v1.4.0"],
529 ["push", "8 commits pushed to main", "api · 5 pushes by cy", "/acme/api/activity"],
530 ["package", "api 1.4.0", "container package · published by @g1t", "/acme/-/packages"],
531 ["push", "4 commits pushed to trunk", "cli · 3 pushes by ana and @margo", "/acme/cli/activity"],
532 ],
533 );
534 const builds = landedDeploys([
535 { key: "deploy:site:d1", project: "site", kind: "production", outcome: "live", commit: "abcdef1", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d1", by: "@g1t" },
536 { key: "deploy:site:d2", project: "site", kind: "preview", outcome: "live", commit: "abcdef2", branch: "fix", at: NOW - HOUR, to: "/acme/site/deployments/d2", by: "bo" },
537 { key: "deploy:site:d3", project: "site", kind: "production", outcome: "failed", commit: "abcdef3", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d3", by: "bo" },
538 ]);
539 assert.deepEqual(
540 builds.map((change) => [change.kind, change.title, change.failed ?? false]),
541 [
542 ["deploy", "Production of site went live", false],
543 ["deploy", "Production of site failed to deploy", true],
544 ],
545 );
546 const all = landed([fromActivity, builds.slice(0, 1)]);
547 assert.deepEqual(
548 all.map((change) => change.key),
549 ["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"],
550 );
551});
552
553test("a production build is its own row only where no merge or push on the list says it", () => {
554 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" });
555 const merge: LandedChange = { key: "merge:1", kind: "merge", title: "Fix", detail: "", to: "/acme/site/pull/1", at: NOW - 2 * HOUR, sha: "aaaaaaa111", repoId: "r" };
556 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" } };
557 const builds = landedDeploys([row("d1", "aaaaaaa"), row("d2", "ccccccc"), row("d3", "ddddddd")], [merge, push]);
558 assert.deepEqual(builds.map((change) => change.key), ["landed:d3"]);
559});
560
561const deploy = (over: Partial<Deployment>): Deployment => ({
562 id: "d1",
563 kind: "production",
564 branch: null,
565 number: null,
566 commit: "abcdef1234",
567 status: "ready",
568 url: "https://site.g1t.page",
569 error: null,
570 warnings: [],
571 buildSeconds: 40,
572 createdBy: "bo",
573 createdAt: iso(NOW - 2 * HOUR),
574 finishedAt: iso(NOW - HOUR),
575 ...over,
576});
577
578test("builds that finished in the span: live or failed, never skipped", () => {
579 const rows = deploysIn(
580 [
581 {
582 slug: "site",
583 deployments: [
584 deploy({}),
585 deploy({ id: "d2", status: "replaced", finishedAt: iso(NOW - DAY) }),
586 deploy({ id: "d3", status: "failed", finishedAt: iso(NOW - 2 * DAY) }),
587 deploy({ id: "d4", status: "skipped", finishedAt: iso(NOW - HOUR) }),
588 deploy({ id: "d5", status: "building", finishedAt: null }),
589 deploy({ id: "d6", finishedAt: iso(NOW - 5 * DAY) }),
590 ],
591 },
592 ],
593 "acme",
594 SPAN,
595 { "user:bo": "bo" },
596 );
597 assert.deepEqual(rows.map((row) => [row.key, row.outcome]), [
598 ["deploy:site:d1", "live"],
599 ["deploy:site:d2", "live"],
600 ["deploy:site:d3", "failed"],
601 ]);
602 assert.equal(rows[0]!.to, "/acme/site/deployments/d1");
603 assert.equal(rows[0]!.commit, "abcdef1");
604 // The maker is named, never shown as an id; an older agent's by its handle; an id identity doesn't know as someone.
605 assert.equal(rows[0]!.by, "bo");
606 const unknown = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "usr_zzz" })] }], "acme", SPAN);
607 assert.equal(unknown[0]!.by, "someone");
608 const robot = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "agt_01x" })] }], "acme", SPAN, { "user:agt_01x": "otto" });
609 assert.equal(robot[0]!.by, "@otto");
610});
611
612test("running now: workflow runs and builds going", () => {
613 const builds = runningDeploys(
614 [
615 { slug: "site", enabled: true, production: null, previews: 0, latest: deploy({ status: "building", finishedAt: null }) },
616 { slug: "docs", enabled: true, production: null, previews: 0, latest: deploy({}) },
617 ],
618 "acme",
619 );
620 assert.deepEqual(builds.map((row) => row.status), ["Building"]);
621 const run = (over: Partial<WorkflowRun>): WorkflowRun => ({
622 id: "run_1",
623 workflowId: "wf_1",
624 path: ".g1t/workflows/ci.yml",
625 name: "CI",
626 title: "Fix the flaky test",
627 number: 12,
628 attempt: 1,
629 event: "push",
630 ref: "refs/heads/main",
631 sha: "abcdef1",
632 pull: null,
633 status: "in_progress",
634 conclusion: null,
635 error: null,
636 actor: "ana",
637 createdAt: iso(NOW - 10 * 60_000),
638 startedAt: iso(NOW - 9 * 60_000),
639 finishedAt: null,
640 ...over,
641 });
642 const workflows = runningWorkflows([
643 { 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" })] },
644 ]);
645 assert.deepEqual(
646 workflows.map((row) => [row.key, row.title, row.detail, row.status, row.to]),
647 [
648 ["workflow:acme/web:run_1", "CI: Fix the flaky test", "acme/web main · by ana", "Running", "/acme/web/actions/runs/run_1"],
649 ["workflow:acme/web:run_2", "CI", "acme/web main · by ana", "Queued", "/acme/web/actions/runs/run_2"],
650 ["workflow:acme/web:run_4", "CI: Fix the flaky test", "acme/web main · by ana", "Needs approval", "/acme/web/actions/runs/run_4"],
651 ],
652 );
653 // Longest-going first within a kind: workflow runs, then builds.
654 assert.deepEqual(running([builds, workflows.slice(0, 1)]).map((row) => row.key), ["workflow:acme/web:run_1", "deploy:site:d1"]);
655});
656
657
658test("the span's months: one, or the ones it crosses", () => {
659 assert.deepEqual(spanMonths(SPAN), ["2026-10"]);
660 assert.deepEqual(spanMonths({ from: Date.parse("2026-09-28T00:00:00Z"), now: NOW }), ["2026-09", "2026-10"]);
661 assert.deepEqual(spanMonths({ from: Date.parse("2025-12-30T00:00:00Z"), now: Date.parse("2026-01-02T00:00:00Z") }), ["2025-12", "2026-01"]);
662});
663
664test("spend in the span: the UTC days it covers across months, at price, without money in", () => {
665 const september = {
666 groups: [
667 { key: "2026-09-29", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 2, chargedMicros: 1_000_000, costMicros: 0 }] },
668 { key: "2026-09-20", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 9, chargedMicros: 9_000_000, costMicros: 0 }] },
669 ],
670 totals: { chargedMicros: 10_000_000, paidMicros: 0, costMicros: 0, entries: 2 },
671 };
672 const october = {
673 groups: [
674 {
675 key: "2026-10-09",
676 label: "2026-10-09",
677 chargedMicros: 0,
678 lines: [
679 { kind: "Workflow minutes", count: 12, chargedMicros: 3_000_000, costMicros: 2_500_000, priceMicros: 4_000_000 },
680 { kind: "Sandbox time", count: 12, chargedMicros: 600_000, costMicros: 500_000 },
681 { kind: "Payments", count: 1, chargedMicros: -50_000_000, costMicros: 0, priceMicros: 0 },
682 ],
683 },
684 ],
685 totals: { chargedMicros: 12_600_000, paidMicros: 50_000_000, costMicros: 0, entries: 4, priceMicros: 13_600_000 },
686 };
687 const spend = spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, 13_609_000);
688 assert.deepEqual([spend.from, spend.to], ["2026-09-29", "2026-10-09"]);
689 assert.equal(spend.totalMicros, 5_600_000);
690 assert.deepEqual(spend.lines.map((l) => [l.kind, l.micros, l.count]), [
691 ["Workflow minutes", 5_000_000, 14],
692 ["Sandbox time", 600_000, 12],
693 ]);
694 // The month so far is billing's usage report, the top bar's figure, not the statement: it carries what is not yet closed.
695 assert.equal(spend.monthMicros, 13_609_000);
696 assert.equal(spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, null).monthMicros, null);
697});
698
699
700// --- What landed: shipped, made in a session, day by day -----------------------
701
702const merge = (over: Partial<LandedChange> = {}): LandedChange => ({
703 key: "landed:acme/web#12",
704 kind: "merge",
705 title: "Add retries",
706 detail: "web#12 · by ana",
707 to: "/acme/web/pull/12",
708 at: NOW - 2 * HOUR,
709 sha: "m12",
710 repoId: "rep_web",
711 ...over,
712});
713
714test("each merge and push says whether it is live, from what deployments recorded", () => {
715 const push = merge({ key: "push:rep_web:evt_1", kind: "push", sha: "p1", at: NOW - HOUR });
716 const release = merge({ key: "release:rep_web:v1", kind: "release", sha: null });
717 assert.deepEqual(shippedAsks([merge(), push, release]), [
718 { repo_id: "rep_web", commits: [{ sha: "m12", landed_at: iso(NOW - 2 * HOUR) }, { sha: "p1", landed_at: iso(NOW - HOUR) }] },
719 ]);
720 const answers = [
721 {
722 repo_id: "rep_web",
723 deploys: true,
724 commits: [
725 { sha: "m12", state: "live" as const, deployed_at: iso(NOW - 2 * HOUR + 9 * 60_000), url: "/acme/web/actions/runs/run_1", environment: "production" },
726 { sha: "p1", state: "waiting" as const, deployed_at: null, url: null, environment: null },
727 ],
728 },
729 ];
730 const [shippedMerge, shippedPush, untouched] = withShipped([merge(), push, release], answers);
731 assert.deepEqual(shippedMerge!.shipped, { state: "live", at: NOW - 2 * HOUR + 9 * 60_000, to: "/acme/web/actions/runs/run_1" });
732 assert.equal(shippedLabel(shippedMerge!.shipped!, shippedMerge!), "Live 9 min after merge");
733 assert.equal(shippedLabel(shippedPush!.shipped!, shippedPush!), "Waiting to deploy");
734 assert.equal(shippedLabel({ state: "failed", at: null, to: "/x" }, push), "Deploy failed");
735 assert.equal(shippedLabel({ state: "deploying", at: null, to: null }, push), "Deploying");
736 assert.equal(shippedLabel({ state: "live", at: push.at + 3 * HOUR, to: null }, push), "Live 3 h after push");
737 assert.equal(untouched!.shipped, undefined);
738 // A project that does not deploy says nothing; deployments not answering leaves rows as they were.
739 assert.equal(withShipped([merge()], [{ repo_id: "rep_web", deploys: false, commits: [] }])[0]!.shipped, undefined);
740 assert.deepEqual(withShipped([merge()], null), [merge()]);
741});
742
743test("a change made in a harness session carries the agent mark, linked to the session", () => {
744 const [marked, plain] = withSessions([merge(), merge({ key: "landed:acme/web#13", sha: "m13" })], [
745 { repo: { namespace: "acme", name: "web" }, pulls: [{ number: 12, sessions: [{ id: "ses_1", harness: "claude_code" }, { id: "ses_2", harness: "codex" }] }] },
746 ]);
747 assert.deepEqual(marked!.agent, { harness: "claude_code", to: "/acme/web/work/sessions/ses_1" });
748 assert.equal(agentLabel(marked!.agent!), "Made in a Claude Code session");
749 assert.equal(plain!.agent, undefined);
750 assert.equal(agentLabel({ harness: null, to: null }), "Made in a session");
751 assert.deepEqual(sessionTrailers("Fix the bell\n\nG1t-Session: ses_01ABC\ng1t-session: ses_01abc\nG1t-Session: ses_02x"), ["ses_01abc", "ses_02x"]);
752 assert.deepEqual(sessionTrailers("Mentions G1t-Session: ses_1 in prose"), []);
753});
754
755test("each person's push to a default branch is a row: its commit, who pushed, and its session", () => {
756 const repo = { namespace: "acme", name: "web", defaultBranch: "main" };
757 const history = {
758 rep_web: [
759 { hash: "c3", parents: ["c2"], message: "Fix the bell's count\n\nG1t-Session: ses_9" },
760 { hash: "c2", parents: ["c1"], message: "Tidy" },
761 { hash: "c1", parents: ["c0"], message: "Start" },
762 { hash: "c0", parents: [], message: "Root" },
763 ],
764 };
765 const push = (id: string, after: string, before: string, at: number, actor = "usr_ana") => ({
766 repoId: "rep_web",
767 repo,
768 push: { id, time: iso(at), actor, data: { after, before } },
769 by: "ana",
770 });
771 const span = { from: NOW - DAY, now: NOW };
772 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);
773 assert.deepEqual(
774 rows.map((row) => [row.title, row.detail, row.sha, row.to, row.agent?.to ?? null]),
775 [
776 ["Fix the bell's count", "web · pushed to main · by ana", "c3", "/acme/web/commit/c3", "/acme/web/work/sessions/ses_9"],
777 ["2 commits pushed to main", "web · Tidy · by ana", "c2", "/acme/web/activity", null],
778 ],
779 );
780 // g1t's own pushes (a pull request landing) are listed as the merge instead.
781 assert.deepEqual(pushRows([push("evt_3", "c3", "c2", NOW - HOUR, "usr_g1t_agent")], history, span), []);
782});
783
784test("what landed over the week reads day by day, newest first", () => {
785 const tz = "America/Los_Angeles";
786 const days = landedByDay([merge({ at: NOW - DAY }), merge({ key: "b", at: NOW - HOUR }), merge({ key: "c", at: NOW - 3 * DAY })], NOW, tz);
787 assert.deepEqual(days.map((day) => [day.label, day.rows.length]), [["Today", 1], ["Yesterday", 1], ["Tue, Oct 6", 1]]);
788});
789
790test("while you were away: what settled by itself, what shipped, and what still needs you", () => {
791 const span = { from: NOW - DAY, now: NOW };
792 const settled = settledIn(
793 [
794 item({ id: "n1", doneAt: iso(NOW - HOUR), settledReason: "CI passed in run 330" }),
795 item({ id: "n2", doneAt: iso(NOW - 2 * HOUR), settledReason: "Merged", settledBy: "ana" }),
796 // Marked done by hand: yours, not g1t's.
797 item({ id: "n3", doneAt: iso(NOW - HOUR) }),
798 // Before the span, or another workspace's.
799 item({ id: "n4", doneAt: iso(NOW - 2 * DAY), settledReason: "Merged" }),
800 item({ id: "n5", doneAt: iso(NOW - HOUR), settledReason: "Merged", workspace: "other", repo: "other/x" }),
801 ],
802 "acme",
803 true,
804 span,
805 );
806 assert.deepEqual(
807 settled.map((row) => [row.key, row.why, row.where]),
808 [
809 ["settled:n1", "CI passed in run 330", "acme/web#12"],
810 ["settled:n2", "Merged by @ana", "acme/web#12"],
811 ],
812 );
813 assert.equal(settledIn([item({ doneAt: iso(NOW - HOUR), settledReason: "Merged" })], "acme", false, span).length, 0, "nothing about repositories without Code");
814 const away = awayOf({
815 settled,
816 landed: [merge({ shipped: { state: "live", at: NOW, to: null } }), merge({ key: "x", shipped: { state: "waiting", at: null, to: null } }), merge({ key: "y" })],
817 needs: 3,
818 });
819 assert.deepEqual({ ...away, settled: away.settled.length }, { settled: 2, shipped: 1, waiting: 1, deploying: 0, failed: 0, needs: 3 });
820});
821
822test("Summarize asks about the span the page shows, as a person would", () => {
823 const now = Date.UTC(2026, 9, 14, 22, 0);
824 assert.equal(
825 summarizeQuestion({ key: "last", note: null, words: "Since this afternoon", from: Date.UTC(2026, 9, 14, 18, 0), now }, "UTC"),
826 "Summarize what changed since I was last here (6:00 PM) in plain English.",
827 );
828 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.");
829 // A first visit shows the last 24 hours, so it is not asked as "since I was last here".
830 assert.doesNotMatch(summarizeQuestion({ key: "last", note: "first", words: "In the last 24 hours", from: now - DAY, now }, "UTC"), /last here/);
831});
832
833test("Explain asks about the one change, by what it is", () => {
834 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?");
835 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?");
836 assert.match(explainQuestion({ kind: "deploy", title: "web", to: "/acme/web/deployments/1" }), /went out in this deployment/);
837 assert.match(explainQuestion({ kind: "release", title: "v1.2.0", to: "/acme/web/releases/v1.2.0" }), /What's new in v1\.2\.0/);
838});