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.

1,206 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { ActivityDigest, ActorCount, ChatActivity, Deployment, InboxItem, RepoDigest, WorkItem, 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 nextRow,
29 nextWhy,
30 notificationRow,
31 nowRows,
32 parseWindow,
33 pushRows,
34 rankAttention,
35 running,
36 runningDeploys,
37 runningWorkflows,
38 sessionTrailers,
39 settledIn,
40 shippedAsks,
41 shippedLabel,
42 sinceWords,
43 spanFor,
44 spanMonths,
45 spanSentence,
46 spendIn,
47 startHere,
48 summarizeQuestion,
49 triageRow,
50 waitingSentence,
51 withSessions,
52 withShipped,
53} from "./home.ts";
54
55const HOUR = 3_600_000;
56const DAY = 24 * HOUR;
57// Friday 2026-10-09, 18:00 UTC; 11:00 in Los Angeles.
58const NOW = Date.parse("2026-10-09T18:00:00Z");
59const LA = "America/Los_Angeles";
60const repo = { namespace: "acme", name: "web" };
61const iso = (ms: number) => new Date(ms).toISOString();
62
63function pull(over: Partial<CodePull> = {}): CodePull {
64 return {
65 repo,
66 number: 1,
67 title: "Fix the flaky test",
68 status: "merged",
69 mergedAt: iso(NOW - HOUR),
70 createdAt: iso(NOW - 3 * HOUR),
71 updatedAt: iso(NOW - HOUR),
72 author: { username: "g1t", kind: "agent" },
73 ...over,
74 };
75}
76
77const need = (over: Partial<CodeNeed>): CodeNeed => ({
78 key: "pull:p1",
79 reason: "checks_failing",
80 repo,
81 ref: "#12",
82 title: "Add retries",
83 ask: "It failed in the merge queue.",
84 by: { name: "bo", agent: false },
85 at: NOW - 2 * HOUR,
86 to: "/acme/web/pull/12",
87 facts: [{ label: "Files changed", value: "4", tone: null }],
88 ...over,
89});
90
91test("Code's needs become rows with what is at stake", () => {
92 const failing = codeRow(need({}));
93 assert.equal(failing.kind, "pull_blocked");
94 assert.equal(failing.stake?.text, "Checks failing");
95 assert.equal(failing.action.label, "Look");
96 const deploy = codeRow(need({ key: "deploy:site", reason: "blocking", facts: [{ label: "Production", value: "Serving the build before", tone: "good" }] }));
97 assert.equal(deploy.kind, "deploy");
98 assert.equal(deploy.stake?.text, "Production on an older build");
99 assert.equal(codeRow(need({ key: "review:p2", reason: "needs_review", facts: [{ label: "Lines", value: "+10 −2", tone: null }] })).stake?.text, "+10 −2");
100 assert.equal(codeRow(need({ key: "runner:j1", reason: "stalled", facts: [{ label: "Waiting", value: "2 h", tone: "warn" }] })).stake?.text, "Waiting 2 h");
101 // Your pull request that meets everything: merge it, beside reviews asked of you.
102 const ready = codeRow(need({ key: "ready:p3", reason: "ready", ask: "Ready to land when you merge it." }));
103 assert.equal(ready.kind, "ready");
104 assert.equal(ready.stake?.text, "Ready to merge");
105 assert.equal(ready.action.label, "Merge");
106 // One that needs you (a conflict, a request for changes) blocks it.
107 const conflict = codeRow(need({ key: "pull:p4", reason: "blocking", ask: "It conflicts with the branch it will land on." }));
108 assert.equal(conflict.kind, "pull_blocked");
109 assert.equal(conflict.stake?.text, "Blocking");
110});
111
112const row = (over: Partial<AttentionRow>): AttentionRow => ({
113 key: "k",
114 kind: "chat",
115 title: "t",
116 detail: "d",
117 stake: null,
118 action: { label: "Open", to: "/x" },
119 owner: null,
120 at: NOW,
121 from: "chat",
122 ...over,
123});
124
125test("Start here: the most pressing tier first, then what has waited longest", () => {
126 const rows = [
127 row({ key: "chat:old", kind: "chat", at: NOW - 5 * DAY, action: { label: "Reply", to: "/acme/-/chat/general" } }),
128 row({ key: "review:new", kind: "review", at: NOW - HOUR, action: { label: "Review", to: "/acme/web/pull/2" } }),
129 row({ key: "review:old", kind: "review", at: NOW - 3 * HOUR, action: { label: "Review", to: "/acme/web/pull/1" } }),
130 row({ key: "pull:p", kind: "pull_blocked", at: NOW - 10 * 60_000, action: { label: "Look", to: "/acme/web/pull/3" } }),
131 ];
132 const ranked = rankAttention(rows);
133 assert.deepEqual(
134 ranked.map((r) => r.key),
135 ["pull:p", "review:old", "review:new", "chat:old"],
136 );
137 const start = startHere(ranked);
138 assert.equal(start?.row.key, "pull:p");
139 assert.match(start?.why ?? "", /can't land/);
140 // A failed production deploy beats everything.
141 const withDeploy = rankAttention([
142 ...rows,
143 row({ key: "deploy:site", kind: "deploy", at: NOW, action: { label: "See the build", to: "/acme/site/deployments/1" } }),
144 ]);
145 assert.equal(startHere(withDeploy)?.row.key, "deploy:site");
146 // Ties in tier say so.
147 const two = startHere(rankAttention(rows.filter((r) => r.kind === "review")));
148 assert.equal(two?.row.key, "review:old");
149 assert.match(two?.why ?? "", /Of the 2 like it, it has waited longest/);
150 assert.equal(startHere([]), null);
151});
152
153function workItem(number: number, over: Partial<WorkItem> = {}): WorkItem {
154 return {
155 id: `wki_${number}`,
156 workspace: "acme",
157 number,
158 title: `Item ${number}`,
159 body: "",
160 labels: [],
161 state: "open",
162 reason: null,
163 resolutions: [],
164 author: { id: "usr_1", username: "ana" } as WorkItem["author"],
165 requestedBy: null,
166 createdAt: iso(NOW - DAY),
167 updatedAt: iso(NOW - HOUR),
168 closedAt: null,
169 pullCount: 0,
170 commentCount: 0,
171 assignees: [],
172 blockedBy: [],
173 agent: null,
174 repos: [{ repoId: "rep_web", path: "acme/web", role: "origin", legacyNumber: null, landed: false }],
175 horizon: "next",
176 appetite: null,
177 budgetUsd: null,
178 urgent: false,
179 plannedAt: null,
180 ...over,
181 };
182}
183
184test("with nothing waiting, Start here is the work item to take next, and says why", () => {
185 const next = workItem(31, { title: "Retry lost starts", appetite: "small" });
186 const out = attention({ slug: "acme", code: true, codeNeeds: [], notifications: [], chat: [], now: [], next: { item: next, viewer: "bo" } });
187 // It waits on nobody, so it is no row of Your move.
188 assert.deepEqual(out.rows, []);
189 assert.equal(out.start?.row.kind, "next");
190 assert.equal(out.start?.row.title, "Retry lost starts");
191 assert.equal(out.start?.row.detail, "#31 · acme/web");
192 assert.deepEqual(out.start?.row.action, { label: "Open work item", to: "/acme/-/work/31" });
193 assert.deepEqual(out.start?.row.stake, { text: "Next · S", tone: "accent" });
194 assert.equal(out.start?.why, "Top of the plan's Next, ready and unclaimed.");
195 // Anything that ranks comes first.
196 const busy = attention({ slug: "acme", code: true, codeNeeds: [need({})], notifications: [], chat: [], now: [], next: { item: next, viewer: "bo" } });
197 assert.equal(busy.start?.row.key, "pull:p1");
198 // No work item to take: nothing to start with.
199 assert.equal(attention({ slug: "acme", code: true, codeNeeds: [], notifications: [], chat: [], next: null }).start, null);
200});
201
202test("why the work item to take next is the one", () => {
203 assert.equal(nextWhy(workItem(1, { horizon: "now" }), "bo"), "First in Now that nobody is on, and nothing blocks it.");
204 assert.equal(nextWhy(workItem(1, { horizon: "now", urgent: true }), "bo"), "Marked urgent in Now, and nobody is on it yet.");
205 assert.equal(nextWhy(workItem(1, { assignees: ["Bo"] }), "bo"), "Top of the plan's Next, ready and unclaimed. It is assigned to you.");
206 assert.deepEqual(nextRow(workItem(2, { horizon: "now", urgent: true })).stake, { text: "Urgent", tone: "danger" });
207});
208
209test("your work in Now is in Your move, in Now's order, after everything else that waits on you", () => {
210 const now = [
211 workItem(7, { horizon: "now", urgent: true, assignees: ["bo"] }),
212 workItem(3, { horizon: "now", assignees: ["ana"] }),
213 workItem(5, { horizon: "now" }),
214 workItem(9, { horizon: "now", assignees: ["BO"] }),
215 ];
216 const claims = [
217 { item: 5, state: "active", actor: { username: "bo" } },
218 { item: 3, state: "released", actor: { username: "bo" } },
219 ];
220 const rows = nowRows(now, claims, "bo");
221 const dm = { channel: { id: "c1", kind: "dm" as const, name: null, last_message_at: iso(NOW - DAY) }, title: "ana", muted: false, unread: 1, mentions: 0 };
222 assert.deepEqual(
223 rows.map((r) => [r.key, r.detail, r.stake?.text]),
224 [
225 ["item:acme#7", "#7 · acme/web · assigned to you", "Urgent"],
226 ["item:acme#5", "#5 · acme/web · claimed by you", "In Now"],
227 ["item:acme#9", "#9 · acme/web · assigned to you", "In Now"],
228 ],
229 );
230 const out = attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [dm], now: rows });
231 assert.deepEqual(
232 out.rows.map((r) => r.key),
233 ["chat:c1", "item:acme#7", "item:acme#5", "item:acme#9"],
234 );
235 // Alone, the first of them is where to start.
236 const alone = attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: rows.slice(1) });
237 assert.equal(alone.start?.row.key, "item:acme#5");
238 assert.match(alone.start?.why ?? "", /yours in Now\. It is first of your 2 there\./);
239 // The plan not answering is said, like any other source.
240 assert.deepEqual(attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: null }).missing, ["Work"]);
241});
242
243test("work waiting in Triage over a week is a row for whoever plans, last, after their work in Now", () => {
244 assert.equal(triageRow("acme", 0), null);
245 const one = triageRow("acme", 1);
246 assert.equal(one?.title, "1 work item has waited in Triage over a week");
247 const row = triageRow("acme", 4);
248 assert.equal(row?.title, "4 work items have waited in Triage over a week");
249 assert.deepEqual(row?.action, { label: "Triage", to: "/acme/-/work/triage" });
250 const mine = nowRows([workItem(7, { horizon: "now", assignees: ["bo"] })], [], "bo");
251 const dm = { channel: { id: "c1", kind: "dm" as const, name: null, last_message_at: iso(NOW - DAY) }, title: "ana", muted: false, unread: 1, mentions: 0 };
252 const out = attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [dm], now: mine, triageWaiting: 4 });
253 assert.deepEqual(
254 out.rows.map((r) => r.key),
255 ["chat:c1", "item:acme#7", "triage:acme"],
256 );
257 // Alone, it is where to start, before the work item to take next.
258 const alone = attention({
259 slug: "acme",
260 code: false,
261 codeNeeds: null,
262 notifications: [],
263 chat: [],
264 now: [],
265 triageWaiting: 2,
266 next: { item: workItem(31), viewer: "bo" },
267 });
268 // The work to take next comes before the nudge; with none, the nudge leads.
269 assert.equal(alone.start?.row.key, nextRow(workItem(31)).key);
270 assert.deepEqual(
271 alone.rows.map((row) => row.key),
272 ["triage:acme"],
273 );
274 const nudge = attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: [], triageWaiting: 2 });
275 assert.equal(nudge.start?.row.key, "triage:acme");
276 assert.match(nudge.start?.why ?? "", /waited over a week to be placed/);
277 // Nothing waiting that long, or a viewer who does not plan: no row.
278 assert.deepEqual(attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: [], triageWaiting: 0 }).rows, []);
279 assert.deepEqual(attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: [] }).rows, []);
280});
281
282test("the same order whatever order the rows came in", () => {
283 const rows = [row({ key: "b", kind: "review", at: NOW }), row({ key: "a", kind: "review", at: NOW, action: { label: "Review", to: "/y" } })];
284 assert.deepEqual(
285 rankAttention(rows).map((r) => r.key),
286 ["a", "b"],
287 );
288 assert.deepEqual(
289 rankAttention([...rows].reverse()).map((r) => r.key),
290 ["a", "b"],
291 );
292});
293
294const item = (over: Partial<InboxItem>): InboxItem => ({
295 id: "n1",
296 reason: "ci_activity",
297 severity: "error",
298 title: "Checks failed on #12",
299 body: "Add retries",
300 event: "check.failed",
301 repo: "acme/web",
302 workspace: "acme",
303 subject: "pull",
304 number: 12,
305 url: "/acme/web/pull/12",
306 actor: "g1t",
307 count: 1,
308 createdAt: iso(NOW - HOUR),
309 updatedAt: iso(NOW - HOUR),
310 readAt: null,
311 doneAt: null,
312 saved: false,
313 snoozedUntil: null,
314 ...over,
315});
316
317test("notifications: warnings and failures in this workspace; none about repositories without Code", () => {
318 assert.equal(notificationRow(item({}), "acme", true)?.kind, "notification");
319 assert.equal(notificationRow(item({}), "acme", false), null);
320 assert.equal(notificationRow(item({ repo: null, url: "/acme/-/chat/general" }), "acme", false)?.kind, "notification");
321 // A reason this page does not know, older ones included, reads as a plain notification.
322 assert.equal(notificationRow(item({ reason: "agent" as string as InboxItem["reason"] }), "acme", true)?.stake?.text, "Notification");
323 assert.equal(notificationRow(item({ severity: "info" }), "acme", true), null);
324 assert.equal(notificationRow(item({ workspace: "other", repo: "other/web" }), "acme", true), null);
325 assert.equal(notificationRow(item({ readAt: iso(NOW) }), "acme", true), null);
326});
327
328test("a notification about something already listed is left out", () => {
329 const out = attention({
330 slug: "acme",
331 code: true,
332 codeNeeds: [need({})],
333 notifications: [item({})],
334 chat: [],
335 });
336 assert.deepEqual(
337 out.rows.map((r) => r.key),
338 ["pull:p1"],
339 );
340 assert.deepEqual(out.missing, []);
341});
342
343test("without Code access no Code row is asked for or shown, and a silent source is named", () => {
344 const out = attention({
345 slug: "acme",
346 code: false,
347 codeNeeds: [need({})],
348 notifications: [item({}), item({ id: "n2", repo: null, reason: "mention", severity: "warning", url: "/acme/-/chat/general" })],
349 chat: null,
350 });
351 assert.deepEqual(
352 out.rows.map((r) => r.key),
353 ["notification:n2"],
354 );
355 assert.equal(
356 out.rows.some((r) => r.from === "code"),
357 false,
358 );
359 assert.deepEqual(out.missing, ["Chat"]);
360});
361
362test("chat: mentions anywhere and unread direct messages, never muted", () => {
363 const channel = { id: "c1", kind: "channel" as const, name: "general", last_message_at: iso(NOW) };
364 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 0 }, "acme"), null);
365 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 2 }, "acme")?.stake?.text, "2 mentions");
366 assert.equal(chatRow({ channel, title: "general", muted: true, unread: 4, mentions: 2 }, "acme"), null);
367 const dm = chatRow({ channel: { ...channel, kind: "dm", name: null }, title: "Bo Reed", muted: false, unread: 1, mentions: 0 }, "acme");
368 assert.equal(dm?.action.to, "/acme/-/chat/dm/c1");
369});
370
371// The span is NOW back to 3 days ago unless a test says otherwise.
372const SPAN = { from: NOW - 3 * DAY, now: NOW };
373
374test("a day is the reader's day", () => {
375 assert.equal(dayIn(NOW, LA), "2026-10-09");
376 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), LA), "2026-10-09");
377 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), null), "2026-10-10");
378 assert.equal(dayIn(NOW, "Not/AZone"), "2026-10-09");
379});
380
381test("since your last visit, in words, in the reader's time zone", () => {
382 // NOW is Friday 11:00 in Los Angeles.
383 assert.equal(sinceWords(NOW - 20_000, NOW, LA), "a moment ago");
384 assert.equal(sinceWords(NOW - 40 * 60_000, NOW, LA), "40 minutes ago");
385 assert.equal(sinceWords(NOW - HOUR - 60_000, NOW, LA), "an hour ago");
386 assert.equal(sinceWords(NOW - 2 * HOUR, NOW, LA), "2 hours ago");
387 // 07:00 Friday.
388 assert.equal(sinceWords(NOW - 4 * HOUR, NOW, LA), "this morning");
389 // 02:00 Friday is Thursday night.
390 assert.equal(sinceWords(NOW - 9 * HOUR, NOW, LA), "last night");
391 // 19:00 Thursday.
392 assert.equal(sinceWords(NOW - 16 * HOUR, NOW, LA), "yesterday evening");
393 // 19:00 Tuesday.
394 assert.equal(sinceWords(NOW - 2 * DAY - 16 * HOUR, NOW, LA), "Tuesday evening");
395 // 02:00 Wednesday is Tuesday night.
396 assert.equal(sinceWords(NOW - 2 * DAY - 9 * HOUR, NOW, LA), "Tuesday night");
397 assert.equal(sinceWords(NOW - 9 * DAY, NOW, LA), "Sep 30");
398});
399
400test("how long a span is", () => {
401 assert.equal(duration(30_000), "a moment");
402 assert.equal(duration(45 * 60_000), "45 minutes");
403 assert.equal(duration(HOUR), "1 hour");
404 assert.equal(duration(3 * DAY + HOUR), "3 days");
405 assert.equal(duration(15 * DAY), "2 weeks");
406});
407
408test("the span: since your last visit, capped, or a fixed window", () => {
409 assert.equal(parseWindow("24h"), "24h");
410 assert.equal(parseWindow("7d"), "7d");
411 assert.equal(parseWindow("nonsense"), "last");
412 assert.equal(parseWindow(null), "last");
413 const away = spanFor("last", NOW, NOW - 2 * DAY - 16 * HOUR, LA);
414 assert.equal(away.from, NOW - 2 * DAY - 16 * HOUR);
415 assert.equal(away.words, "Since Tuesday evening");
416 assert.equal(away.length, "2 days");
417 assert.equal(away.note, null);
418 // Never visited: the last 24 hours, and it says why.
419 const first = spanFor("last", NOW, null, LA);
420 assert.equal(first.from, NOW - DAY);
421 assert.equal(first.note, "first");
422 assert.equal(spanFor("last", NOW, undefined, LA).note, "unknown");
423 // Away a month: 14 days at most.
424 const capped = spanFor("last", NOW, NOW - 30 * DAY, LA);
425 assert.equal(capped.from, NOW - 14 * DAY);
426 assert.equal(capped.note, "capped");
427 assert.equal(capped.words, "In the last 14 days");
428 // The fixed windows ignore the last visit.
429 assert.equal(spanFor("24h", NOW, NOW - 3 * DAY, LA).from, NOW - DAY);
430 assert.deepEqual([spanFor("7d", NOW, NOW - HOUR, LA).from, spanFor("7d", NOW, NOW - HOUR, LA).words], [NOW - 7 * DAY, "In the last 7 days"]);
431});
432
433// --- People ------------------------------------------------------------
434
435const ANA = "user:usr_ana";
436const BO = "user:usr_bo";
437const CY = "user:usr_cy";
438const MARGO = "agent:agt_margo";
439const G1T = "user:usr_g1t_agent";
440const NAMES = { [ANA]: "ana", [BO]: "bo", [CY]: "cy", [MARGO]: "margo" };
441const REPOS = {
442 rep_web: { namespace: "acme", name: "web", defaultBranch: "main" },
443 rep_api: { namespace: "acme", name: "api", defaultBranch: "main" },
444 rep_docs: { namespace: "acme", name: "docs", defaultBranch: "main" },
445 rep_cli: { namespace: "acme", name: "cli", defaultBranch: "trunk" },
446};
447
448const count = (actor: string, n: number): ActorCount => ({ actor, count: n });
449const quietRepo = (repo_id: string): RepoDigest => ({
450 repo_id,
451 pushes: { count: 0, commits: 0, branches: [], by: [], default_branch: { count: 0, commits: 0, by: [], last_at: null } },
452 pulls: { opened: [], merged: [], closed: [] },
453 items: { opened: [], closed: [] },
454 reviews: [],
455 comments: [],
456 deployments: { succeeded: [], failed: [], production: 0 },
457 releases: [],
458 packages: [],
459});
460
461/**
462 * A day on a busy workspace: 3 people pushed 41 commits to 4 projects,
463 * merged 5 pull requests and opened 2; older agent rows (g1t and Margo)
464 * pushed 6, opened 3 pull requests and reviewed 2, and are not people;
465 * 2 production deploys went out; 120 messages were sent, 15 of them
466 * Margo's; 7 docs were edited.
467 */
468function busyDay(): ActivityDigest {
469 return {
470 from: iso(SPAN.from),
471 until: iso(SPAN.now),
472 complete: true,
473 repos: [
474 {
475 ...quietRepo("rep_web"),
476 pushes: {
477 count: 14,
478 commits: 33,
479 branches: ["fix-login", "main"],
480 by: [
481 { actor: ANA, count: 8, commits: 24 },
482 { actor: BO, count: 4, commits: 5 },
483 { actor: G1T, count: 2, commits: 4 },
484 ],
485 default_branch: { count: 3, commits: 9, by: [{ actor: ANA, count: 3, commits: 9 }], last_at: iso(NOW - 2 * HOUR) },
486 },
487 pulls: { opened: [count(ANA, 1), count(G1T, 2), count(MARGO, 1)], merged: [count(ANA, 3), count(BO, 1)], closed: [] },
488 items: { opened: [count(BO, 2)], closed: [count(ANA, 1), count(G1T, 1)] },
489 reviews: [count(BO, 2), count(G1T, 2)],
490 comments: [count(ANA, 4), count(MARGO, 3)],
491 deployments: { succeeded: [count(ANA, 2)], failed: [], production: 2 },
492 releases: [{ tag: "v1.4.0", name: "Autumn", actor: ANA, at: iso(NOW - 3 * HOUR) }],
493 packages: [],
494 },
495 {
496 ...quietRepo("rep_api"),
497 pushes: {
498 count: 5,
499 commits: 8,
500 branches: ["main"],
501 by: [{ actor: CY, count: 5, commits: 8 }],
502 default_branch: { count: 5, commits: 8, by: [{ actor: CY, count: 5, commits: 8 }], last_at: iso(NOW - 5 * HOUR) },
503 },
504 pulls: { opened: [count(CY, 1)], merged: [count(CY, 1)], closed: [] },
505 packages: [{ ecosystem: "container", name: "api", version: "1.4.0", actor: G1T, at: iso(NOW - HOUR) }],
506 },
507 {
508 ...quietRepo("rep_docs"),
509 pushes: {
510 count: 2,
511 commits: 2,
512 branches: ["typo"],
513 by: [{ actor: BO, count: 2, commits: 2 }],
514 default_branch: { count: 0, commits: 0, by: [], last_at: null },
515 },
516 },
517 {
518 ...quietRepo("rep_cli"),
519 pushes: {
520 count: 3,
521 commits: 4,
522 branches: ["trunk"],
523 by: [
524 { actor: ANA, count: 1, commits: 2 },
525 { actor: MARGO, count: 2, commits: 2 },
526 ],
527 default_branch: {
528 count: 3,
529 commits: 4,
530 by: [
531 { actor: ANA, count: 1, commits: 2 },
532 { actor: MARGO, count: 2, commits: 2 },
533 ],
534 last_at: iso(NOW - 30 * 60_000),
535 },
536 },
537 },
538 ],
539 folios: {
540 created: [count(ANA, 1)],
541 edited_count: 7,
542 edited: [
543 { folio_id: "fol_1", kind: "doc", authors: [ANA, MARGO] },
544 { folio_id: "fol_2", kind: "doc", authors: [ANA] },
545 { folio_id: "fol_3", kind: "doc", authors: [BO] },
546 { folio_id: "fol_4", kind: "doc", authors: [BO] },
547 { folio_id: "fol_5", kind: "doc", authors: [CY] },
548 { folio_id: "fol_6", kind: "doc", authors: [CY] },
549 { folio_id: "fol_7", kind: "doc", authors: [MARGO] },
550 ],
551 },
552 };
553}
554
555const busyChat: ChatActivity = {
556 from: iso(SPAN.from),
557 until: iso(SPAN.now),
558 messages: 120,
559 channels: 5,
560 authors: [
561 { key: ANA, messages: 60 },
562 { key: BO, messages: 30 },
563 { key: CY, messages: 15 },
564 { key: MARGO, messages: 15 },
565 ],
566};
567
568test("an actor is not a person when it is g1t or an agent of older rows; everyone else is", () => {
569 assert.equal(actorKind(MARGO), "agent");
570 assert.equal(actorKind(G1T), "agent");
571 assert.equal(actorKind("user:agt_01abc"), "agent", "an agent named by its own id under user: is still a robot");
572 assert.equal(actorKind("user:wsp_01abc"), "agent", "a workspace, by its own token or g1t's upkeep, is never a person");
573 assert.equal(actorKind("user:g1t"), "agent");
574 assert.equal(actorKind(ANA), "person");
575 assert.equal(actorKind(""), "person");
576 assert.equal(actorKind("user:usr_bot", new Set(["user:usr_bot"])), "agent");
577});
578
579test("the digest: what was done, by kind with counts, and who, agents marked", () => {
580 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true });
581 assert.deepEqual(
582 digest.people.lines.map((line) => [line.key, line.text, line.to]),
583 [
584 ["pushes", "47 commits to 4 projects", "/acme/-/repositories"],
585 ["pulls", "5 opened, 5 merged, 4 reviewed", "/acme/-/repositories"],
586 ["items", "2 opened, 2 closed, 7 comments", "/acme/-/work/all?repo=web"],
587 ["shipped", "2 deploys, 1 release, 1 package version", "/acme/web/deployments"],
588 ["docs", "1 doc created, 7 docs edited", "/acme/-/artifacts"],
589 ["chat", "120 messages in 5 conversations", "/acme/-/chat"],
590 ],
591 );
592 // Rows: most active first, people and agents, each marked; nobody is "someone".
593 assert.deepEqual(
594 digest.people.actors.map((actor) => [actor.name, actor.kind]),
595 digest.people.actors.map((actor) => [actor.name, actor.name === "margo" || actor.name === "g1t" ? "agent" : "person"]),
596 );
597 assert.deepEqual(new Set(digest.people.actors.map((actor) => actor.name)), new Set(["ana", "bo", "cy", "margo", "g1t"]));
598 assert.equal(digest.people.actors[0]!.name, "ana");
599 // Each row's parts add up to its count, and the kinds' counts to every row's.
600 for (const actor of digest.people.actors) {
601 assert.equal(
602 Object.values(actor.parts).reduce((a, b) => a + b, 0),
603 actor.count,
604 actor.name,
605 );
606 }
607 // The summary still counts people only.
608 assert.deepEqual(digest.summary, { people: 3, commits: 41, merged: 5, said: 105, anything: true, peopleRead: true });
609 assert.deepEqual(digest.missing, []);
610 assert.equal(digest.partial, false);
611});
612
613test("g1t is one row, named, whichever key it used: its account, a workspace, or chat's system:g1t", () => {
614 const chat: ChatActivity = {
615 ...busyChat,
616 messages: 5,
617 channels: 1,
618 authors: [
619 { key: "system:g1t", messages: 3 },
620 { key: ANA, messages: 2 },
621 ],
622 };
623 const activity = { ...busyDay(), folios: null, repos: [{ ...quietRepo("rep_web"), comments: [count(G1T, 2), count("user:wsp_acme", 1)] }] };
624 const digest = digestOf({ slug: "acme", repos: REPOS, activity, chat, names: { [ANA]: "ana", "user:wsp_acme": "g1t", "system:g1t": "g1t" }, code: true });
625 assert.deepEqual(
626 digest.people.actors.map((actor) => [actor.name, actor.kind, actor.count, actor.parts]),
627 [
628 ["g1t", "agent", 6, { items: 3, chat: 3 }],
629 ["ana", "person", 2, { chat: 2 }],
630 ],
631 );
632 assert.equal(digest.summary.people, 1);
633 // A person whose name could not be read is an unknown account, never "someone".
634 const unread = digestOf({
635 slug: "acme",
636 repos: REPOS,
637 activity: null,
638 chat: { ...chat, authors: [{ key: "user:usr_gone", messages: 1 }] },
639 names: {},
640 code: true,
641 });
642 assert.deepEqual(
643 unread.people.actors.map((actor) => actor.name),
644 ["unknown account"],
645 );
646});
647
648test("the digest says which part didn't answer, and counts what could be read", () => {
649 const noChat = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: null, names: NAMES, code: true });
650 assert.deepEqual(noChat.missing, ["Chat"]);
651 assert.equal(
652 noChat.people.lines.some((line) => line.key === "chat"),
653 false,
654 );
655 const noCode = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: busyChat, names: NAMES, code: true });
656 assert.deepEqual(noCode.missing, ["Code"]);
657 assert.deepEqual(
658 noCode.people.lines.map((line) => line.text),
659 ["120 messages in 5 conversations"],
660 );
661 assert.equal(noCode.summary.peopleRead, true);
662 // Without Code access, events are read for artifacts only: no repositories, nothing missing.
663 const member = digestOf({ slug: "acme", repos: {}, activity: { ...busyDay(), repos: [] }, chat: busyChat, names: NAMES, code: false });
664 assert.deepEqual(member.missing, []);
665 assert.deepEqual(
666 member.people.lines.map((line) => line.key),
667 ["docs", "chat"],
668 );
669 // A digest cut short says so.
670 const cut = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), complete: false }, chat: busyChat, names: NAMES, code: true });
671 assert.equal(cut.partial, true);
672 // Nobody named: an import's pushes are still commits.
673 const imported = digestOf({
674 slug: "acme",
675 repos: REPOS,
676 activity: {
677 ...busyDay(),
678 folios: null,
679 repos: [
680 {
681 ...quietRepo("rep_web"),
682 pushes: {
683 count: 2,
684 commits: 7,
685 branches: ["main"],
686 by: [{ actor: "", count: 2, commits: 7 }],
687 default_branch: { count: 2, commits: 7, by: [{ actor: "", count: 2, commits: 7 }], last_at: iso(NOW - HOUR) },
688 },
689 },
690 ],
691 },
692 chat: null,
693 names: {},
694 code: true,
695 });
696 assert.deepEqual(
697 imported.people.lines.map((line) => [line.text, line.to]),
698 [["7 commits to 1 project", "/acme/web/activity"]],
699 );
700 assert.equal(imported.summary.people, 0);
701});
702
703test("the sentence: people first, then what landed; honest when quiet or unread", () => {
704 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true });
705 assert.equal(
706 spanSentence({ words: "Since Tuesday evening" }, digest, 12),
707 "Since Tuesday evening, 3 people pushed 41 commits and merged 5 changes and 12 changes landed.",
708 );
709 const quiet = digestOf({
710 slug: "acme",
711 repos: REPOS,
712 activity: { ...busyDay(), repos: [], folios: { created: [], edited: [], edited_count: 0 } },
713 chat: { ...busyChat, messages: 0, channels: 0, authors: [] },
714 names: {},
715 code: true,
716 });
717 assert.equal(spanSentence({ words: "Since Tuesday evening" }, quiet, 0), "Quiet since Tuesday evening: nothing pushed, merged, deployed or said.");
718 assert.equal(spanSentence({ words: "In the last 24 hours" }, quiet, 0), "Quiet in the last 24 hours: nothing pushed, merged, deployed or said.");
719 // Only merges.
720 const mergesOnly = digestOf({
721 slug: "acme",
722 repos: REPOS,
723 activity: { ...busyDay(), repos: [], folios: null },
724 chat: { ...busyChat, authors: [], messages: 0, channels: 0 },
725 names: {},
726 code: true,
727 });
728 assert.equal(spanSentence({ words: "Since this morning" }, mergesOnly, 5), "Since this morning, 5 changes landed.");
729 // People only talked.
730 const talked = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: busyChat, names: NAMES, code: true });
731 assert.equal(spanSentence({ words: "Since an hour ago" }, talked, 0), "Since an hour ago, 3 people sent 105 messages.");
732 // Neither events nor chat answered.
733 const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true });
734 assert.equal(spanSentence({ words: "Since this morning" }, unread, 5), "Since this morning, 5 changes landed; what people did couldn't be read.");
735 const nothingRead = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true });
736 assert.equal(spanSentence({ words: "Since this morning" }, nothingRead, 0), "Since this morning, what people did couldn't be read.");
737 assert.equal(spanSentence({ words: "Since this morning" }, null, null), "Since this morning, what happened couldn't be read.");
738 assert.equal(waitingSentence(0), "Nothing needs you right now.");
739 assert.equal(waitingSentence(1), "1 thing needs you.");
740 assert.equal(waitingSentence(8), "8 things need you.");
741});
742
743test("what landed: every merge in the span, newest first, g1t's older ones as @g1t", () => {
744 const merged = landedIn(
745 [
746 pull({ number: 1, mergedAt: iso(NOW - 2 * DAY), mergedBy: "bo" }),
747 pull({ number: 2, author: { username: "bo", kind: "user" }, mergedAt: iso(NOW - HOUR) }),
748 pull({ number: 3, mergedAt: iso(NOW - 5 * DAY) }),
749 pull({ number: 4, status: "open", mergedAt: null }),
750 ],
751 SPAN,
752 );
753 assert.deepEqual(
754 merged.map((change) => [change.kind, change.title, change.detail]),
755 [
756 ["merge", "Fix the flaky test", "web#2 · by bo"],
757 ["merge", "Fix the flaky test", "web#1 · by @g1t, merged by bo"],
758 ],
759 );
760});
761
762test("what landed, besides merges: direct pushes, releases, packages and production builds, newest first", () => {
763 const fromActivity = landedFromActivity(busyDay(), REPOS, NAMES, "acme");
764 assert.deepEqual(
765 fromActivity.map((change) => [change.kind, change.title, change.detail, change.to]),
766 [
767 ["push", "9 commits pushed to main", "web · 3 pushes by ana", "/acme/web/activity"],
768 ["release", "v1.4.0 · Autumn", "web · released by ana", "/acme/web/releases/tag/v1.4.0"],
769 ["push", "8 commits pushed to main", "api · 5 pushes by cy", "/acme/api/activity"],
770 ["package", "api 1.4.0", "container package · published by @g1t", "/acme/-/packages"],
771 ["push", "4 commits pushed to trunk", "cli · 3 pushes by ana and @margo", "/acme/cli/activity"],
772 ],
773 );
774 const builds = landedDeploys([
775 {
776 key: "deploy:site:d1",
777 project: "site",
778 kind: "production",
779 outcome: "live",
780 commit: "abcdef1",
781 branch: null,
782 at: NOW - HOUR,
783 to: "/acme/site/deployments/d1",
784 by: "@g1t",
785 },
786 {
787 key: "deploy:site:d2",
788 project: "site",
789 kind: "preview",
790 outcome: "live",
791 commit: "abcdef2",
792 branch: "fix",
793 at: NOW - HOUR,
794 to: "/acme/site/deployments/d2",
795 by: "bo",
796 },
797 {
798 key: "deploy:site:d3",
799 project: "site",
800 kind: "production",
801 outcome: "failed",
802 commit: "abcdef3",
803 branch: null,
804 at: NOW - HOUR,
805 to: "/acme/site/deployments/d3",
806 by: "bo",
807 },
808 ]);
809 assert.deepEqual(
810 builds.map((change) => [change.kind, change.title, change.failed ?? false]),
811 [
812 ["deploy", "Production of site went live", false],
813 ["deploy", "Production of site failed to deploy", true],
814 ],
815 );
816 const all = landed([fromActivity, builds.slice(0, 1)]);
817 assert.deepEqual(
818 all.map((change) => change.key),
819 ["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"],
820 );
821});
822
823test("a production build is its own row only where no merge or push on the list says it", () => {
824 const row = (key: string, commit: string): DeployRow => ({
825 key,
826 project: "site",
827 kind: "production",
828 outcome: "live",
829 commit,
830 branch: null,
831 at: NOW - HOUR,
832 to: `/acme/site/deployments/${key}`,
833 by: "bo",
834 });
835 const merge: LandedChange = {
836 key: "merge:1",
837 kind: "merge",
838 title: "Fix",
839 detail: "",
840 to: "/acme/site/pull/1",
841 at: NOW - 2 * HOUR,
842 sha: "aaaaaaa111",
843 repoId: "r",
844 };
845 const push: LandedChange = {
846 key: "push:1",
847 kind: "push",
848 title: "Push",
849 detail: "",
850 to: "/acme/site/activity",
851 at: NOW - 2 * HOUR,
852 sha: "bbbbbbb222",
853 repoId: "r",
854 shipped: { state: "live", at: NOW - HOUR, to: "/acme/site/deployments/d2" },
855 };
856 const builds = landedDeploys([row("d1", "aaaaaaa"), row("d2", "ccccccc"), row("d3", "ddddddd")], [merge, push]);
857 assert.deepEqual(
858 builds.map((change) => change.key),
859 ["landed:d3"],
860 );
861});
862
863const deploy = (over: Partial<Deployment>): Deployment => ({
864 id: "d1",
865 kind: "production",
866 branch: null,
867 number: null,
868 commit: "abcdef1234",
869 status: "ready",
870 url: "https://site.g1t.page",
871 error: null,
872 warnings: [],
873 buildSeconds: 40,
874 createdBy: "bo",
875 createdAt: iso(NOW - 2 * HOUR),
876 finishedAt: iso(NOW - HOUR),
877 ...over,
878});
879
880test("builds that finished in the span: live or failed, never skipped", () => {
881 const rows = deploysIn(
882 [
883 {
884 slug: "site",
885 deployments: [
886 deploy({}),
887 deploy({ id: "d2", status: "replaced", finishedAt: iso(NOW - DAY) }),
888 deploy({ id: "d3", status: "failed", finishedAt: iso(NOW - 2 * DAY) }),
889 deploy({ id: "d4", status: "skipped", finishedAt: iso(NOW - HOUR) }),
890 deploy({ id: "d5", status: "building", finishedAt: null }),
891 deploy({ id: "d6", finishedAt: iso(NOW - 5 * DAY) }),
892 ],
893 },
894 ],
895 "acme",
896 SPAN,
897 { "user:bo": "bo" },
898 );
899 assert.deepEqual(
900 rows.map((row) => [row.key, row.outcome]),
901 [
902 ["deploy:site:d1", "live"],
903 ["deploy:site:d2", "live"],
904 ["deploy:site:d3", "failed"],
905 ],
906 );
907 assert.equal(rows[0]!.to, "/acme/site/deployments/d1");
908 assert.equal(rows[0]!.commit, "abcdef1");
909 // The maker is named, never shown as an id; an older agent's by its handle; an id identity doesn't know as someone.
910 assert.equal(rows[0]!.by, "bo");
911 const unknown = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "usr_zzz" })] }], "acme", SPAN);
912 assert.equal(unknown[0]!.by, "someone");
913 const robot = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "agt_01x" })] }], "acme", SPAN, { "user:agt_01x": "otto" });
914 assert.equal(robot[0]!.by, "@otto");
915});
916
917test("running now: workflow runs and builds going", () => {
918 const builds = runningDeploys(
919 [
920 { slug: "site", enabled: true, production: null, previews: 0, latest: deploy({ status: "building", finishedAt: null }) },
921 { slug: "docs", enabled: true, production: null, previews: 0, latest: deploy({}) },
922 ],
923 "acme",
924 );
925 assert.deepEqual(
926 builds.map((row) => row.status),
927 ["Building"],
928 );
929 const run = (over: Partial<WorkflowRun>): WorkflowRun => ({
930 id: "run_1",
931 workflowId: "wf_1",
932 path: ".g1t/workflows/ci.yml",
933 name: "CI",
934 title: "Fix the flaky test",
935 number: 12,
936 attempt: 1,
937 event: "push",
938 ref: "refs/heads/main",
939 sha: "abcdef1",
940 pull: null,
941 status: "in_progress",
942 conclusion: null,
943 error: null,
944 actor: "ana",
945 createdAt: iso(NOW - 10 * 60_000),
946 startedAt: iso(NOW - 9 * 60_000),
947 finishedAt: null,
948 ...over,
949 });
950 const workflows = runningWorkflows([
951 {
952 repo,
953 runs: [
954 run({}),
955 run({ id: "run_2", status: "queued", startedAt: null, title: "" }),
956 run({ id: "run_3", status: "completed", conclusion: "success" }),
957 run({ id: "run_4", status: "action_required" }),
958 ],
959 },
960 ]);
961 assert.deepEqual(
962 workflows.map((row) => [row.key, row.title, row.detail, row.status, row.to]),
963 [
964 ["workflow:acme/web:run_1", "CI: Fix the flaky test", "acme/web main · by ana", "Running", "/acme/web/actions/runs/run_1"],
965 ["workflow:acme/web:run_2", "CI", "acme/web main · by ana", "Queued", "/acme/web/actions/runs/run_2"],
966 ["workflow:acme/web:run_4", "CI: Fix the flaky test", "acme/web main · by ana", "Needs approval", "/acme/web/actions/runs/run_4"],
967 ],
968 );
969 // Longest-going first within a kind: workflow runs, then builds.
970 assert.deepEqual(
971 running([builds, workflows.slice(0, 1)]).map((row) => row.key),
972 ["workflow:acme/web:run_1", "deploy:site:d1"],
973 );
974});
975
976test("the span's months: one, or the ones it crosses", () => {
977 assert.deepEqual(spanMonths(SPAN), ["2026-10"]);
978 assert.deepEqual(spanMonths({ from: Date.parse("2026-09-28T00:00:00Z"), now: NOW }), ["2026-09", "2026-10"]);
979 assert.deepEqual(spanMonths({ from: Date.parse("2025-12-30T00:00:00Z"), now: Date.parse("2026-01-02T00:00:00Z") }), ["2025-12", "2026-01"]);
980});
981
982test("spend in the span: the UTC days it covers across months, at price, without money in", () => {
983 const september = {
984 groups: [
985 { key: "2026-09-29", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 2, chargedMicros: 1_000_000, costMicros: 0 }] },
986 { key: "2026-09-20", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 9, chargedMicros: 9_000_000, costMicros: 0 }] },
987 ],
988 totals: { chargedMicros: 10_000_000, paidMicros: 0, costMicros: 0, entries: 2 },
989 };
990 const october = {
991 groups: [
992 {
993 key: "2026-10-09",
994 label: "2026-10-09",
995 chargedMicros: 0,
996 lines: [
997 { kind: "Workflow minutes", count: 12, chargedMicros: 3_000_000, costMicros: 2_500_000, priceMicros: 4_000_000 },
998 { kind: "Sandbox time", count: 12, chargedMicros: 600_000, costMicros: 500_000 },
999 { kind: "Payments", count: 1, chargedMicros: -50_000_000, costMicros: 0, priceMicros: 0 },
1000 ],
1001 },
1002 ],
1003 totals: { chargedMicros: 12_600_000, paidMicros: 50_000_000, costMicros: 0, entries: 4, priceMicros: 13_600_000 },
1004 };
1005 const spend = spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, 13_609_000);
1006 assert.deepEqual([spend.from, spend.to], ["2026-09-29", "2026-10-09"]);
1007 assert.equal(spend.totalMicros, 5_600_000);
1008 assert.deepEqual(
1009 spend.lines.map((l) => [l.kind, l.micros, l.count]),
1010 [
1011 ["Workflow minutes", 5_000_000, 14],
1012 ["Sandbox time", 600_000, 12],
1013 ],
1014 );
1015 // The month so far is billing's usage report, the top bar's figure, not the statement: it carries what is not yet closed.
1016 assert.equal(spend.monthMicros, 13_609_000);
1017 assert.equal(spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, null).monthMicros, null);
1018});
1019
1020// --- What landed: shipped, made in a session, day by day -----------------------
1021
1022const merge = (over: Partial<LandedChange> = {}): LandedChange => ({
1023 key: "landed:acme/web#12",
1024 kind: "merge",
1025 title: "Add retries",
1026 detail: "web#12 · by ana",
1027 to: "/acme/web/pull/12",
1028 at: NOW - 2 * HOUR,
1029 sha: "m12",
1030 repoId: "rep_web",
1031 ...over,
1032});
1033
1034test("each merge and push says whether it is live, from what deployments recorded", () => {
1035 const push = merge({ key: "push:rep_web:evt_1", kind: "push", sha: "p1", at: NOW - HOUR });
1036 const release = merge({ key: "release:rep_web:v1", kind: "release", sha: null });
1037 assert.deepEqual(shippedAsks([merge(), push, release]), [
1038 {
1039 repo_id: "rep_web",
1040 commits: [
1041 { sha: "m12", landed_at: iso(NOW - 2 * HOUR) },
1042 { sha: "p1", landed_at: iso(NOW - HOUR) },
1043 ],
1044 },
1045 ]);
1046 const answers = [
1047 {
1048 repo_id: "rep_web",
1049 deploys: true,
1050 commits: [
1051 { sha: "m12", state: "live" as const, deployed_at: iso(NOW - 2 * HOUR + 9 * 60_000), url: "/acme/web/actions/runs/run_1", environment: "production" },
1052 { sha: "p1", state: "waiting" as const, deployed_at: null, url: null, environment: null },
1053 ],
1054 },
1055 ];
1056 const [shippedMerge, shippedPush, untouched] = withShipped([merge(), push, release], answers);
1057 assert.deepEqual(shippedMerge!.shipped, { state: "live", at: NOW - 2 * HOUR + 9 * 60_000, to: "/acme/web/actions/runs/run_1" });
1058 assert.equal(shippedLabel(shippedMerge!.shipped!, shippedMerge!), "Live 9 min after merge");
1059 assert.equal(shippedLabel(shippedPush!.shipped!, shippedPush!), "Waiting to deploy");
1060 assert.equal(shippedLabel({ state: "failed", at: null, to: "/x" }, push), "Deploy failed");
1061 assert.equal(shippedLabel({ state: "deploying", at: null, to: null }, push), "Deploying");
1062 assert.equal(shippedLabel({ state: "live", at: push.at + 3 * HOUR, to: null }, push), "Live 3 h after push");
1063 assert.equal(untouched!.shipped, undefined);
1064 // A project that does not deploy says nothing; deployments not answering leaves rows as they were.
1065 assert.equal(withShipped([merge()], [{ repo_id: "rep_web", deploys: false, commits: [] }])[0]!.shipped, undefined);
1066 assert.deepEqual(withShipped([merge()], null), [merge()]);
1067});
1068
1069test("a change made in a harness session carries the agent mark, linked to the session", () => {
1070 const [marked, plain] = withSessions(
1071 [merge(), merge({ key: "landed:acme/web#13", sha: "m13" })],
1072 [
1073 {
1074 repo: { namespace: "acme", name: "web" },
1075 pulls: [
1076 {
1077 number: 12,
1078 sessions: [
1079 { id: "ses_1", harness: "claude_code" },
1080 { id: "ses_2", harness: "codex" },
1081 ],
1082 },
1083 ],
1084 },
1085 ],
1086 );
1087 assert.deepEqual(marked!.agent, { harness: "claude_code", to: "/acme/-/work/sessions/ses_1" });
1088 assert.equal(agentLabel(marked!.agent!), "Made in a Claude Code session");
1089 assert.equal(plain!.agent, undefined);
1090 assert.equal(agentLabel({ harness: null, to: null }), "Made in a session");
1091 assert.deepEqual(sessionTrailers("Fix the bell\n\nG1t-Session: ses_01ABC\ng1t-session: ses_01abc\nG1t-Session: ses_02x"), ["ses_01abc", "ses_02x"]);
1092 assert.deepEqual(sessionTrailers("Mentions G1t-Session: ses_1 in prose"), []);
1093});
1094
1095test("each person's push to a default branch is a row: its commit, who pushed, and its session", () => {
1096 const repo = { namespace: "acme", name: "web", defaultBranch: "main" };
1097 const history = {
1098 rep_web: [
1099 { hash: "c3", parents: ["c2"], message: "Fix the bell's count\n\nG1t-Session: ses_9" },
1100 { hash: "c2", parents: ["c1"], message: "Tidy" },
1101 { hash: "c1", parents: ["c0"], message: "Start" },
1102 { hash: "c0", parents: [], message: "Root" },
1103 ],
1104 };
1105 const push = (id: string, after: string, before: string, at: number, actor = "usr_ana") => ({
1106 repoId: "rep_web",
1107 repo,
1108 push: { id, time: iso(at), actor, data: { after, before } },
1109 by: "ana",
1110 });
1111 const span = { from: NOW - DAY, now: NOW };
1112 const rows = pushRows(
1113 [push("evt_2", "c3", "c2", NOW - HOUR), push("evt_1", "c2", "c0", NOW - 2 * HOUR), push("evt_0", "c1", "c0", NOW - 2 * DAY)],
1114 history,
1115 span,
1116 );
1117 assert.deepEqual(
1118 rows.map((row) => [row.title, row.detail, row.sha, row.to, row.agent?.to ?? null]),
1119 [
1120 ["Fix the bell's count", "web · pushed to main · by ana", "c3", "/acme/web/commit/c3", "/acme/-/work/sessions/ses_9"],
1121 ["2 commits pushed to main", "web · Tidy · by ana", "c2", "/acme/web/activity", null],
1122 ],
1123 );
1124 // g1t's own pushes (a pull request landing) are listed as the merge instead.
1125 assert.deepEqual(pushRows([push("evt_3", "c3", "c2", NOW - HOUR, "usr_g1t_agent")], history, span), []);
1126});
1127
1128test("what landed over the week reads day by day, newest first", () => {
1129 const tz = "America/Los_Angeles";
1130 const days = landedByDay([merge({ at: NOW - DAY }), merge({ key: "b", at: NOW - HOUR }), merge({ key: "c", at: NOW - 3 * DAY })], NOW, tz);
1131 assert.deepEqual(
1132 days.map((day) => [day.label, day.rows.length]),
1133 [
1134 ["Today", 1],
1135 ["Yesterday", 1],
1136 ["Tue, Oct 6", 1],
1137 ],
1138 );
1139});
1140
1141test("while you were away: what settled by itself, what shipped, and what still needs you", () => {
1142 const span = { from: NOW - DAY, now: NOW };
1143 const settled = settledIn(
1144 [
1145 item({ id: "n1", doneAt: iso(NOW - HOUR), settledReason: "CI passed in run 330" }),
1146 item({ id: "n2", doneAt: iso(NOW - 2 * HOUR), settledReason: "Merged", settledBy: "ana" }),
1147 // Marked done by hand: yours, not g1t's.
1148 item({ id: "n3", doneAt: iso(NOW - HOUR) }),
1149 // Before the span, or another workspace's.
1150 item({ id: "n4", doneAt: iso(NOW - 2 * DAY), settledReason: "Merged" }),
1151 item({ id: "n5", doneAt: iso(NOW - HOUR), settledReason: "Merged", workspace: "other", repo: "other/x" }),
1152 ],
1153 "acme",
1154 true,
1155 span,
1156 );
1157 assert.deepEqual(
1158 settled.map((row) => [row.key, row.why, row.where]),
1159 [
1160 ["settled:n1", "CI passed in run 330", "acme/web#12"],
1161 ["settled:n2", "Merged by @ana", "acme/web#12"],
1162 ],
1163 );
1164 assert.equal(
1165 settledIn([item({ doneAt: iso(NOW - HOUR), settledReason: "Merged" })], "acme", false, span).length,
1166 0,
1167 "nothing about repositories without Code",
1168 );
1169 const away = awayOf({
1170 settled,
1171 landed: [
1172 merge({ shipped: { state: "live", at: NOW, to: null } }),
1173 merge({ key: "x", shipped: { state: "waiting", at: null, to: null } }),
1174 merge({ key: "y" }),
1175 ],
1176 needs: 3,
1177 });
1178 assert.deepEqual({ ...away, settled: away.settled.length }, { settled: 2, shipped: 1, waiting: 1, deploying: 0, failed: 0, needs: 3 });
1179});
1180
1181test("Summarize asks about the span the page shows, as a person would", () => {
1182 const now = Date.UTC(2026, 9, 14, 22, 0);
1183 assert.equal(
1184 summarizeQuestion({ key: "last", note: null, words: "Since this afternoon", from: Date.UTC(2026, 9, 14, 18, 0), now }, "UTC"),
1185 "Summarize what changed since I was last here (6:00 PM) in plain English.",
1186 );
1187 assert.equal(
1188 summarizeQuestion({ key: "24h", note: null, words: "In the last 24 hours", from: now - DAY, now }, "UTC"),
1189 "Summarize what changed in the last 24 hours in plain English.",
1190 );
1191 // A first visit shows the last 24 hours, so it is not asked as "since I was last here".
1192 assert.doesNotMatch(summarizeQuestion({ key: "last", note: "first", words: "In the last 24 hours", from: now - DAY, now }, "UTC"), /last here/);
1193});
1194
1195test("Explain asks about the one change, by what it is", () => {
1196 assert.equal(
1197 explainQuestion({ kind: "merge", title: "Faster pushes", to: "/acme/web/pull/12" }),
1198 "Explain pull request #12 in acme/web in plain English: what changed, why, and is it live?",
1199 );
1200 assert.equal(
1201 explainQuestion({ kind: "push", title: "Fix login", to: "/acme/web/commit/abc1234def" }),
1202 "Explain commit abc1234 in acme/web in plain English: what changed, why, and is it live?",
1203 );
1204 assert.match(explainQuestion({ kind: "deploy", title: "web", to: "/acme/web/deployments/1" }), /went out in this deployment/);
1205 assert.match(explainQuestion({ kind: "release", title: "v1.2.0", to: "/acme/web/releases/v1.2.0" }), /What's new in v1\.2\.0/);
1206});