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