Skip to content
890 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { ActivityDigest, ActorCount, AgentSession, ChatActivity, Deployment, InboxItem, InstallRequest, Memory, RepoDigest, WorkflowRun } from "@g1t/contracts";
5
6import {
7 type AttentionRow,
8 type CodeNeed,
9 type CodePull,
10 acceptance,
11 actorKind,
12 agentRow,
13 attention,
14 chatRow,
15 codeRow,
16 dayIn,
17 decisionsIn,
18 deploysIn,
19 digestOf,
20 duration,
21 installRequestRow,
22 landed,
23 landedDeploys,
24 landedFromActivity,
25 landedIn,
26 limitRow,
27 notificationRow,
28 parseWindow,
29 pullOutcome,
30 rankAttention,
31 running,
32 runningDeploys,
33 runningSessions,
34 runningWorkflows,
35 sessionOutcome,
36 sinceWords,
37 spanFor,
38 spanMonths,
39 spanSentence,
40 spanWork,
41 spendIn,
42 startHere,
43 trendLabel,
44 waitingSentence,
45} from "./home.ts";
46
47const HOUR = 3_600_000;
48const DAY = 24 * HOUR;
49// Friday 2026-10-09, 18:00 UTC; 11:00 in Los Angeles.
50const NOW = Date.parse("2026-10-09T18:00:00Z");
51const LA = "America/Los_Angeles";
52const repo = { namespace: "acme", name: "web" };
53const iso = (ms: number) => new Date(ms).toISOString();
54
55function pull(over: Partial<CodePull> = {}): CodePull {
56 return {
57 repo,
58 number: 1,
59 title: "Fix the flaky test",
60 status: "merged",
61 mergedAt: iso(NOW - HOUR),
62 createdAt: iso(NOW - 3 * HOUR),
63 updatedAt: iso(NOW - HOUR),
64 author: { username: "g1t", kind: "agent" },
65 ...over,
66 };
67}
68
69function session(over: Partial<AgentSession> = {}): AgentSession {
70 return {
71 id: "s1",
72 workspace_id: "w",
73 agent_id: "a",
74 agent_handle: "margo",
75 agent_name: "Margo",
76 agent_avatar_seed: "margo",
77 agent_look: null,
78 subagent: null,
79 kind: "chat",
80 parent_id: null,
81 root_id: "s1",
82 payer_agent_id: "a",
83 title: "Draft the launch notes",
84 goal: "",
85 status: "done",
86 status_note: null,
87 summary: null,
88 channel_id: "c",
89 channel_kind: "channel",
90 channel_name: "launch",
91 card_message_id: null,
92 asked_by: null,
93 asked_by_username: "bo",
94 routine_id: null,
95 steps: 3,
96 tool_calls: 2,
97 input_tokens: 0,
98 output_tokens: 0,
99 charged_micros: 400_000,
100 cap_micros: 2_000_000,
101 model: null,
102 outputs: [],
103 created_at: iso(NOW - 2 * HOUR),
104 updated_at: iso(NOW - HOUR),
105 finished_at: iso(NOW - HOUR),
106 visible: true,
107 ...over,
108 };
109}
110const need = (over: Partial<CodeNeed>): CodeNeed => ({
111 key: "pull:p1",
112 reason: "ready_to_merge",
113 repo,
114 ref: "#12",
115 title: "Add retries",
116 ask: "Waiting for you to merge it.",
117 by: { name: "g1t", agent: true },
118 for: "bo",
119 at: NOW - 2 * HOUR,
120 to: "/acme/web/pull/12",
121 facts: [{ label: "Files changed", value: "4", tone: null }],
122 ...over,
123});
124
125test("Code's needs become rows with what is at stake", () => {
126 const ready = codeRow(need({}));
127 assert.equal(ready.kind, "ready");
128 assert.equal(ready.stake?.text, "4 files to land");
129 assert.equal(ready.action.label, "Merge");
130 const deploy = codeRow(need({ key: "deploy:site", reason: "blocking", facts: [{ label: "Production", value: "Serving the build before", tone: "good" }] }));
131 assert.equal(deploy.kind, "deploy");
132 assert.equal(deploy.stake?.text, "Production on an older build");
133 assert.equal(codeRow(need({ reason: "checks_failing" })).kind, "pull_blocked");
134 assert.equal(codeRow(need({ key: "run:r1", reason: "stalled", facts: [{ label: "Cost so far", value: "$1.20", tone: null }] })).stake?.text, "$1.20 so far");
135});
136
137const row = (over: Partial<AttentionRow>): AttentionRow => ({
138 key: "k",
139 kind: "chat",
140 title: "t",
141 detail: "d",
142 stake: null,
143 action: { label: "Open", to: "/x" },
144 owner: null,
145 at: NOW,
146 from: "chat",
147 ...over,
148});
149
150test("Start here: the most pressing tier first, then what has waited longest", () => {
151 const rows = [
152 row({ key: "chat:old", kind: "chat", at: NOW - 5 * DAY, action: { label: "Reply", to: "/acme/-/chat/general" } }),
153 row({ key: "ready:new", kind: "ready", at: NOW - HOUR, action: { label: "Merge", to: "/acme/web/pull/2" } }),
154 row({ key: "ready:old", kind: "ready", at: NOW - 3 * HOUR, action: { label: "Merge", to: "/acme/web/pull/1" } }),
155 row({ key: "session:s", kind: "session_cap", at: NOW - 10 * 60_000, action: { label: "Approve more", to: "/acme/-/agents/margo/sessions/s" } }),
156 ];
157 const ranked = rankAttention(rows);
158 assert.deepEqual(ranked.map((r) => r.key), ["session:s", "ready:old", "ready:new", "chat:old"]);
159 const start = startHere(ranked);
160 assert.equal(start?.row.key, "session:s");
161 assert.match(start?.why ?? "", /spend cap/);
162 // A failed production deploy beats everything.
163 const withDeploy = rankAttention([...rows, row({ key: "deploy:site", kind: "deploy", at: NOW, action: { label: "See the build", to: "/acme/site/deployments/1" } })]);
164 assert.equal(startHere(withDeploy)?.row.key, "deploy:site");
165 // Ties in tier say so.
166 const two = startHere(rankAttention(rows.filter((r) => r.kind === "ready")));
167 assert.equal(two?.row.key, "ready:old");
168 assert.match(two?.why ?? "", /Of the 2 like it, it has waited longest/);
169 assert.equal(startHere([]), null);
170});
171
172test("the same order whatever order the rows came in", () => {
173 const rows = [row({ key: "b", kind: "review", at: NOW }), row({ key: "a", kind: "review", at: NOW, action: { label: "Review", to: "/y" } })];
174 assert.deepEqual(rankAttention(rows).map((r) => r.key), ["a", "b"]);
175 assert.deepEqual(rankAttention([...rows].reverse()).map((r) => r.key), ["a", "b"]);
176});
177
178const item = (over: Partial<InboxItem>): InboxItem => ({
179 id: "n1",
180 reason: "ci_activity",
181 severity: "error",
182 title: "Checks failed on #12",
183 body: "Add retries",
184 event: "check.failed",
185 repo: "acme/web",
186 workspace: "acme",
187 subject: "pull",
188 number: 12,
189 url: "/acme/web/pull/12",
190 actor: "g1t",
191 count: 1,
192 createdAt: iso(NOW - HOUR),
193 updatedAt: iso(NOW - HOUR),
194 readAt: null,
195 doneAt: null,
196 saved: false,
197 snoozedUntil: null,
198 ...over,
199});
200
201test("notifications: warnings and failures in this workspace; none about repositories without Code", () => {
202 assert.equal(notificationRow(item({}), "acme", true)?.kind, "notification");
203 assert.equal(notificationRow(item({}), "acme", false), null);
204 assert.equal(notificationRow(item({ repo: null, url: "/acme/-/agents/margo" }), "acme", false)?.kind, "notification");
205 assert.equal(notificationRow(item({ severity: "info" }), "acme", true), null);
206 assert.equal(notificationRow(item({ workspace: "other", repo: "other/web" }), "acme", true), null);
207 assert.equal(notificationRow(item({ readAt: iso(NOW) }), "acme", true), null);
208});
209
210test("a notification about something already listed is left out", () => {
211 const out = attention({
212 slug: "acme",
213 code: true,
214 codeNeeds: [need({})],
215 capped: [],
216 agents: [],
217 canManage: false,
218 notifications: [item({})],
219 chat: [],
220 });
221 assert.deepEqual(out.rows.map((r) => r.key), ["pull:p1"]);
222 assert.deepEqual(out.missing, []);
223});
224
225test("without Code access no Code row is asked for or shown, and a silent source is named", () => {
226 const out = attention({
227 slug: "acme",
228 code: false,
229 codeNeeds: [need({})],
230 capped: null,
231 agents: null,
232 canManage: false,
233 notifications: [item({}), item({ id: "n2", repo: null, reason: "agent", severity: "warning", url: "/acme/-/agents/margo" })],
234 chat: null,
235 });
236 assert.deepEqual(out.rows.map((r) => r.key), ["notification:n2"]);
237 assert.equal(out.rows.some((r) => r.from === "code"), false);
238 assert.deepEqual(out.missing, ["Agents", "Chat"]);
239});
240
241test("an agent out of budget is listed only for those who may raise it", () => {
242 const agent = { id: "a1", handle: "margo", display_name: "Margo", status: "out_of_budget", spent_month_micros: 52_000_000, budget: { monthly_micros: 50_000_000 }, updated_at: iso(NOW) };
243 assert.equal(agentRow(agent, "acme")?.stake?.text, "$0.00 left");
244 const base = { slug: "acme", code: true, codeNeeds: [], capped: [], notifications: [], chat: [] };
245 assert.equal(attention({ ...base, agents: [agent], canManage: false }).rows.length, 0);
246 assert.equal(attention({ ...base, agents: [agent], canManage: true }).rows[0]?.kind, "agent_budget");
247});
248
249test("chat: mentions anywhere and unread direct messages, never muted", () => {
250 const channel = { id: "c1", kind: "channel" as const, name: "general", last_message_at: iso(NOW) };
251 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 0 }, "acme"), null);
252 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 2 }, "acme")?.stake?.text, "2 mentions");
253 assert.equal(chatRow({ channel, title: "general", muted: true, unread: 4, mentions: 2 }, "acme"), null);
254 const dm = chatRow({ channel: { ...channel, kind: "dm", name: null }, title: "Bo Reed", muted: false, unread: 1, mentions: 0 }, "acme");
255 assert.equal(dm?.action.to, "/acme/-/chat/dm/c1");
256});
257
258// The span is NOW back to 3 days ago unless a test says otherwise.
259const SPAN = { from: NOW - 3 * DAY, now: NOW };
260
261test("a day is the reader's day", () => {
262 assert.equal(dayIn(NOW, LA), "2026-10-09");
263 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), LA), "2026-10-09");
264 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), null), "2026-10-10");
265 assert.equal(dayIn(NOW, "Not/AZone"), "2026-10-09");
266});
267
268test("since your last visit, in words, in the reader's time zone", () => {
269 // NOW is Friday 11:00 in Los Angeles.
270 assert.equal(sinceWords(NOW - 20_000, NOW, LA), "a moment ago");
271 assert.equal(sinceWords(NOW - 40 * 60_000, NOW, LA), "40 minutes ago");
272 assert.equal(sinceWords(NOW - HOUR - 60_000, NOW, LA), "an hour ago");
273 assert.equal(sinceWords(NOW - 2 * HOUR, NOW, LA), "2 hours ago");
274 // 07:00 Friday.
275 assert.equal(sinceWords(NOW - 4 * HOUR, NOW, LA), "this morning");
276 // 02:00 Friday is Thursday night.
277 assert.equal(sinceWords(NOW - 9 * HOUR, NOW, LA), "last night");
278 // 19:00 Thursday.
279 assert.equal(sinceWords(NOW - 16 * HOUR, NOW, LA), "yesterday evening");
280 // 19:00 Tuesday.
281 assert.equal(sinceWords(NOW - 2 * DAY - 16 * HOUR, NOW, LA), "Tuesday evening");
282 // 02:00 Wednesday is Tuesday night.
283 assert.equal(sinceWords(NOW - 2 * DAY - 9 * HOUR, NOW, LA), "Tuesday night");
284 assert.equal(sinceWords(NOW - 9 * DAY, NOW, LA), "Sep 30");
285});
286
287test("how long a span is", () => {
288 assert.equal(duration(30_000), "a moment");
289 assert.equal(duration(45 * 60_000), "45 minutes");
290 assert.equal(duration(HOUR), "1 hour");
291 assert.equal(duration(3 * DAY + HOUR), "3 days");
292 assert.equal(duration(15 * DAY), "2 weeks");
293});
294
295test("the span: since your last visit, capped, or a fixed window", () => {
296 assert.equal(parseWindow("24h"), "24h");
297 assert.equal(parseWindow("7d"), "7d");
298 assert.equal(parseWindow("nonsense"), "last");
299 assert.equal(parseWindow(null), "last");
300 const away = spanFor("last", NOW, NOW - 2 * DAY - 16 * HOUR, LA);
301 assert.equal(away.from, NOW - 2 * DAY - 16 * HOUR);
302 assert.equal(away.words, "Since Tuesday evening");
303 assert.equal(away.length, "2 days");
304 assert.equal(away.note, null);
305 // Never visited: the last 24 hours, and it says why.
306 const first = spanFor("last", NOW, null, LA);
307 assert.equal(first.from, NOW - DAY);
308 assert.equal(first.note, "first");
309 assert.equal(spanFor("last", NOW, undefined, LA).note, "unknown");
310 // Away a month: 14 days at most.
311 const capped = spanFor("last", NOW, NOW - 30 * DAY, LA);
312 assert.equal(capped.from, NOW - 14 * DAY);
313 assert.equal(capped.note, "capped");
314 assert.equal(capped.words, "In the last 14 days");
315 // The fixed windows ignore the last visit.
316 assert.equal(spanFor("24h", NOW, NOW - 3 * DAY, LA).from, NOW - DAY);
317 assert.deepEqual([spanFor("7d", NOW, NOW - HOUR, LA).from, spanFor("7d", NOW, NOW - HOUR, LA).words], [NOW - 7 * DAY, "In the last 7 days"]);
318});
319
320test("an agent's pull request is accepted first time when it merged in the span with no revision", () => {
321 assert.equal(pullOutcome(pull(), 0, SPAN), "accepted");
322 assert.equal(pullOutcome(pull(), 2, SPAN), "fixed");
323 assert.equal(pullOutcome(pull({ mergedAt: iso(NOW - 4 * DAY) }), 0, SPAN), null);
324 assert.equal(pullOutcome(pull({ status: "closed", mergedAt: null }), 0, SPAN), "dropped");
325 // Still open is not settled: it is under Running now.
326 assert.equal(pullOutcome(pull({ status: "open", mergedAt: null }), 1, SPAN), null);
327});
328
329test("a session counts once, at the root of its tree, when it settled in the span", () => {
330 assert.equal(sessionOutcome(session(), SPAN), "finished");
331 assert.equal(sessionOutcome(session({ parent_id: "s0" }), SPAN), null);
332 assert.equal(sessionOutcome(session({ status: "failed" }), SPAN), "dropped");
333 assert.equal(sessionOutcome(session({ status: "stopped" }), SPAN), "dropped");
334 assert.equal(sessionOutcome(session({ status: "working", finished_at: null }), SPAN), null);
335 assert.equal(sessionOutcome(session({ finished_at: iso(NOW - 4 * DAY) }), SPAN), null);
336});
337
338test("accepted first time is accepted over every agent pull request that settled", () => {
339 assert.deepEqual(acceptance(["accepted", "accepted", "fixed", "dropped", "finished"]), { value: 0.5, of: 4 });
340 assert.equal(acceptance(["finished"]), null);
341});
342
343test("the span's work: pull requests and sessions, sources, and the 7 days before", () => {
344 const work = spanWork({
345 span: SPAN,
346 slug: "acme",
347 code: {
348 pulls: [
349 pull({ number: 1 }),
350 pull({ number: 2 }),
351 pull({ number: 3, status: "open", mergedAt: null }),
352 // A person's pull request is not agent work.
353 pull({ number: 4, author: { username: "bo", kind: "user" } }),
354 // The 7 days before the span: one accepted, one fixed.
355 pull({ number: 5, mergedAt: iso(NOW - 4 * DAY), updatedAt: iso(NOW - 4 * DAY) }),
356 pull({ number: 6, mergedAt: iso(NOW - 5 * DAY), updatedAt: iso(NOW - 5 * DAY) }),
357 // Older than that: not counted.
358 pull({ number: 7, mergedAt: iso(NOW - 11 * DAY), updatedAt: iso(NOW - 11 * DAY) }),
359 ],
360 revisions: { "acme/web#2": 1, "acme/web#6": 2 },
361 complete: true,
362 },
363 sessions: { sessions: [session(), session({ id: "s2", kind: "routine", status: "working", finished_at: null }), session({ id: "s3", parent_id: "s1" })], complete: true },
364 });
365 assert.equal(work.tasks.length, 3);
366 assert.deepEqual(work.counts, { accepted: 1, fixed: 1, finished: 1, dropped: 0 });
367 assert.deepEqual(work.rate, { value: 0.5, of: 2 });
368 assert.deepEqual(work.before, { value: 0.5, of: 2 });
369 assert.deepEqual(work.sources, [
370 { source: "code", count: 2 },
371 { source: "chat", count: 1 },
372 ]);
373 assert.equal(work.code, "read");
374 assert.equal(work.partial, false);
375});
376
377test("no trend when the 7 days before could not all be read, and none without Code", () => {
378 const partial = spanWork({ span: SPAN, slug: "acme", code: { pulls: [pull()], revisions: {}, complete: false }, sessions: null });
379 assert.equal(partial.before, null);
380 assert.equal(partial.partial, true);
381 assert.equal(partial.sessions, "unavailable");
382 const member = spanWork({ span: SPAN, slug: "acme", code: "no_access", sessions: { sessions: [session()], complete: true } });
383 assert.equal(member.code, "no_access");
384 assert.equal(member.rate, null);
385 assert.equal(member.tasks.every((t) => t.source !== "code"), true);
386});
387
388test("the trend is in whole points", () => {
389 assert.equal(trendLabel({ value: 0.89 }, { value: 0.83 }), "+6 pts vs the 7 days before");
390 assert.equal(trendLabel({ value: 0.5 }, { value: 0.75 }), "−25 pts vs the 7 days before");
391 assert.equal(trendLabel({ value: 0.5 }, { value: 0.5 }), "Same as the 7 days before");
392 assert.equal(trendLabel(null, { value: 0.5 }), null);
393});
394
395// --- People and agents ------------------------------------------------------------
396
397const ANA = "user:usr_ana";
398const BO = "user:usr_bo";
399const CY = "user:usr_cy";
400const MARGO = "agent:agt_margo";
401const G1T = "user:usr_g1t_agent";
402const NAMES = { [ANA]: "ana", [BO]: "bo", [CY]: "cy", [MARGO]: "margo" };
403const REPOS = {
404 rep_web: { namespace: "acme", name: "web", defaultBranch: "main" },
405 rep_api: { namespace: "acme", name: "api", defaultBranch: "main" },
406 rep_docs: { namespace: "acme", name: "docs", defaultBranch: "main" },
407 rep_cli: { namespace: "acme", name: "cli", defaultBranch: "trunk" },
408};
409
410const count = (actor: string, n: number): ActorCount => ({ actor, count: n });
411const quietRepo = (repo_id: string): RepoDigest => ({
412 repo_id,
413 pushes: { count: 0, commits: 0, branches: [], by: [], default_branch: { count: 0, commits: 0, by: [], last_at: null } },
414 pulls: { opened: [], merged: [], closed: [] },
415 issues: { opened: [], closed: [] },
416 reviews: [],
417 comments: [],
418 deployments: { succeeded: [], failed: [], production: 0 },
419 releases: [],
420 packages: [],
421});
422
423/**
424 * A day on a busy workspace: 3 people pushed 41 commits to 4 projects,
425 * merged 5 pull requests and opened 2; agents (g1t and Margo) pushed 6,
426 * opened 3 pull requests and reviewed 2; 2 production deploys went out;
427 * 120 messages were sent, 15 of them Margo's; 7 docs were edited.
428 */
429function busyDay(): ActivityDigest {
430 return {
431 from: iso(SPAN.from),
432 until: iso(SPAN.now),
433 complete: true,
434 repos: [
435 {
436 ...quietRepo("rep_web"),
437 pushes: {
438 count: 14,
439 commits: 33,
440 branches: ["fix-login", "main"],
441 by: [
442 { actor: ANA, count: 8, commits: 24 },
443 { actor: BO, count: 4, commits: 5 },
444 { actor: G1T, count: 2, commits: 4 },
445 ],
446 default_branch: { count: 3, commits: 9, by: [{ actor: ANA, count: 3, commits: 9 }], last_at: iso(NOW - 2 * HOUR) },
447 },
448 pulls: { opened: [count(ANA, 1), count(G1T, 2), count(MARGO, 1)], merged: [count(ANA, 3), count(BO, 1)], closed: [] },
449 issues: { opened: [count(BO, 2)], closed: [count(ANA, 1), count(G1T, 1)] },
450 reviews: [count(BO, 2), count(G1T, 2)],
451 comments: [count(ANA, 4), count(MARGO, 3)],
452 deployments: { succeeded: [count(ANA, 2)], failed: [], production: 2 },
453 releases: [{ tag: "v1.4.0", name: "Autumn", actor: ANA, at: iso(NOW - 3 * HOUR) }],
454 packages: [],
455 },
456 {
457 ...quietRepo("rep_api"),
458 pushes: { count: 5, commits: 8, branches: ["main"], by: [{ actor: CY, count: 5, commits: 8 }], default_branch: { count: 5, commits: 8, by: [{ actor: CY, count: 5, commits: 8 }], last_at: iso(NOW - 5 * HOUR) } },
459 pulls: { opened: [count(CY, 1)], merged: [count(CY, 1)], closed: [] },
460 packages: [{ ecosystem: "container", name: "api", version: "1.4.0", actor: G1T, at: iso(NOW - HOUR) }],
461 },
462 {
463 ...quietRepo("rep_docs"),
464 pushes: { count: 2, commits: 2, branches: ["typo"], by: [{ actor: BO, count: 2, commits: 2 }], default_branch: { count: 0, commits: 0, by: [], last_at: null } },
465 },
466 {
467 ...quietRepo("rep_cli"),
468 pushes: { count: 3, commits: 4, branches: ["trunk"], by: [{ actor: ANA, count: 1, commits: 2 }, { actor: MARGO, count: 2, commits: 2 }], default_branch: { count: 3, commits: 4, by: [{ actor: ANA, count: 1, commits: 2 }, { actor: MARGO, count: 2, commits: 2 }], last_at: iso(NOW - 30 * 60_000) } },
469 },
470 ],
471 folios: {
472 created: [count(ANA, 1)],
473 edited_count: 7,
474 edited: [
475 { folio_id: "fol_1", kind: "doc", authors: [ANA, MARGO] },
476 { folio_id: "fol_2", kind: "doc", authors: [ANA] },
477 { folio_id: "fol_3", kind: "doc", authors: [BO] },
478 { folio_id: "fol_4", kind: "doc", authors: [BO] },
479 { folio_id: "fol_5", kind: "doc", authors: [CY] },
480 { folio_id: "fol_6", kind: "doc", authors: [CY] },
481 { folio_id: "fol_7", kind: "doc", authors: [MARGO] },
482 ],
483 },
484 };
485}
486
487const busyChat: ChatActivity = {
488 from: iso(SPAN.from),
489 until: iso(SPAN.now),
490 messages: 120,
491 channels: 5,
492 authors: [
493 { key: ANA, messages: 60 },
494 { key: BO, messages: 30 },
495 { key: CY, messages: 15 },
496 { key: MARGO, messages: 15 },
497 ],
498};
499
500/** Seven agent tasks that settled well, and one that did not. */
501function busyWork() {
502 return spanWork({
503 span: SPAN,
504 slug: "acme",
505 code: {
506 pulls: [1, 2, 3, 4, 5].map((n) => pull({ number: n, mergedAt: iso(NOW - n * HOUR), updatedAt: iso(NOW - n * HOUR) })).concat(pull({ number: 6, status: "closed", mergedAt: null })),
507 revisions: { "acme/web#2": 1 },
508 complete: true,
509 },
510 sessions: { sessions: [session(), session({ id: "s2", kind: "routine" })], complete: true },
511 });
512}
513
514test("an actor is an agent when it is one of the workspace's agents or g1t; everyone else is a person", () => {
515 assert.equal(actorKind(MARGO), "agent");
516 assert.equal(actorKind(G1T), "agent");
517 assert.equal(actorKind(ANA), "person");
518 assert.equal(actorKind(""), "person");
519 assert.equal(actorKind("user:usr_bot", new Set(["user:usr_bot"])), "agent");
520});
521
522test("the digest: what people did, in lines with counts, and who", () => {
523 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, work: busyWork(), code: true });
524 assert.deepEqual(
525 digest.people.lines.map((line) => [line.text, line.to]),
526 [
527 ["3 people pushed 41 commits to 4 projects", "/acme/-/repositories"],
528 ["opened 2 pull requests, merged 5", "/acme/-/repositories"],
529 ["reviewed 2 pull requests", "/acme/web/pulls"],
530 ["opened 2 issues, closed 1", "/acme/web/issues"],
531 ["left 4 comments", "/acme/web/issues"],
532 ["2 deploys went out", "/acme/web/deployments"],
533 ["published 1 release", "/acme/web/releases"],
534 ["created 1 doc, edited 6 docs", "/acme/-/artifacts"],
535 ["sent 105 messages in 5 conversations", "/acme/-/chat"],
536 ],
537 );
538 // Faces: most active first, by name.
539 assert.deepEqual(
540 digest.people.actors.map((actor) => [actor.name, actor.kind]),
541 [
542 ["ana", "person"],
543 ["bo", "person"],
544 ["cy", "person"],
545 ],
546 );
547 assert.deepEqual(digest.missing, []);
548 assert.equal(digest.partial, false);
549});
550
551test("the digest: what agents did, their tasks first, and g1t and the workspace's agents as agents", () => {
552 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, work: busyWork(), code: true });
553 assert.deepEqual(
554 digest.agents.lines.map((line) => line.text),
555 [
556 "finished 7 tasks, 1 didn't finish",
557 "ran 2 sessions",
558 "pushed 6 commits to 2 projects",
559 "opened 3 pull requests",
560 "reviewed 2 pull requests",
561 "closed 1 issue",
562 "left 3 comments",
563 "published 1 package version",
564 "edited 2 docs",
565 "replied 15 times in chat",
566 ],
567 );
568 assert.deepEqual(
569 digest.agents.actors.map((actor) => [actor.name, actor.kind, actor.key]),
570 [
571 ["margo", "agent", MARGO],
572 ["g1t", "agent", G1T],
573 ],
574 );
575 assert.deepEqual(digest.summary, { people: 3, commits: 41, merged: 5, finished: 7, dropped: 1, said: 105, anything: true, peopleRead: true });
576});
577
578test("the digest says which part didn't answer, and counts what could be read", () => {
579 const noChat = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: null, names: NAMES, work: busyWork(), code: true });
580 assert.deepEqual(noChat.missing, ["Chat"]);
581 assert.equal(noChat.people.lines.some((line) => line.key === "chat"), false);
582 const noCode = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: busyChat, names: NAMES, work: null, code: true });
583 assert.deepEqual(noCode.missing, ["Code", "Agents"]);
584 assert.deepEqual(
585 noCode.people.lines.map((line) => line.text),
586 ["sent 105 messages in 5 conversations"],
587 );
588 assert.equal(noCode.summary.peopleRead, true);
589 // Without Code access, events are read for artifacts only: no repositories, nothing missing.
590 const member = digestOf({ slug: "acme", repos: {}, activity: { ...busyDay(), repos: [] }, chat: busyChat, names: NAMES, work: busyWork(), code: false });
591 assert.deepEqual(member.missing, []);
592 assert.deepEqual(
593 member.people.lines.map((line) => line.key),
594 ["docs", "chat"],
595 );
596 // A digest cut short says so.
597 const cut = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), complete: false }, chat: busyChat, names: NAMES, work: busyWork(), code: true });
598 assert.equal(cut.partial, true);
599 // Nobody named: a mirror's pushes are still commits.
600 const mirror = digestOf({
601 slug: "acme",
602 repos: REPOS,
603 activity: { ...busyDay(), folios: null, repos: [{ ...quietRepo("rep_web"), pushes: { count: 2, commits: 7, branches: ["main"], by: [{ actor: "", count: 2, commits: 7 }], default_branch: { count: 2, commits: 7, by: [{ actor: "", count: 2, commits: 7 }], last_at: iso(NOW - HOUR) } } }] },
604 chat: null,
605 names: {},
606 work: null,
607 code: true,
608 });
609 assert.deepEqual(
610 mirror.people.lines.map((line) => [line.text, line.to]),
611 [["7 commits pushed to 1 project", "/acme/web/activity"]],
612 );
613 assert.equal(mirror.summary.people, 0);
614});
615
616test("the sentence: people first, then the agents' tasks, then what landed; honest when quiet or unread", () => {
617 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, work: busyWork(), code: true });
618 assert.equal(
619 spanSentence({ words: "Since Tuesday evening" }, digest, 12),
620 "Since Tuesday evening, 3 people pushed 41 commits and merged 5 changes, agents finished 7 tasks, and 12 changes landed.",
621 );
622 const quiet = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: { created: [], edited: [], edited_count: 0 } }, chat: { ...busyChat, messages: 0, channels: 0, authors: [] }, names: {}, work: busyWork(), code: true });
623 quiet.summary.finished = 0;
624 quiet.summary.dropped = 0;
625 quiet.summary.anything = false;
626 assert.equal(spanSentence({ words: "Since Tuesday evening" }, quiet, 0), "Quiet since Tuesday evening: nothing pushed, merged, deployed or said.");
627 assert.equal(spanSentence({ words: "In the last 24 hours" }, quiet, 0), "Quiet in the last 24 hours: nothing pushed, merged, deployed or said.");
628 // Only agents' tasks and merges.
629 const agentsOnly = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: { ...busyChat, authors: [], messages: 0, channels: 0 }, names: {}, work: busyWork(), code: true });
630 assert.equal(spanSentence({ words: "Since this morning" }, agentsOnly, 5), "Since this morning, agents finished 7 tasks and 5 changes landed.");
631 // People only talked.
632 const talked = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: busyChat, names: NAMES, work: null, code: true });
633 assert.equal(spanSentence({ words: "Since an hour ago" }, talked, 0), "Since an hour ago, 3 people sent 105 messages.");
634 // Neither events nor chat answered.
635 const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, work: busyWork(), code: true });
636 assert.equal(spanSentence({ words: "Since this morning" }, unread, 5), "Since this morning, agents finished 7 tasks and 5 changes landed; what people did couldn't be read.");
637 const nothingRead = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, work: null, code: true });
638 assert.equal(spanSentence({ words: "Since this morning" }, nothingRead, 0), "Since this morning, what people did couldn't be read, and no agent task settled.");
639 assert.equal(spanSentence({ words: "Since this morning" }, null, null), "Since this morning, what happened couldn't be read.");
640 assert.equal(waitingSentence(0), "Nothing needs you right now.");
641 assert.equal(waitingSentence(1), "1 thing needs you.");
642 assert.equal(waitingSentence(8), "8 things need you.");
643});
644
645test("what landed: every merge in the span, a person's or an agent's, newest first", () => {
646 const merged = landedIn(
647 [
648 pull({ number: 1, mergedAt: iso(NOW - 2 * DAY), mergedBy: "bo" }),
649 pull({ number: 2, author: { username: "bo", kind: "user" }, mergedAt: iso(NOW - HOUR) }),
650 pull({ number: 3, mergedAt: iso(NOW - 5 * DAY) }),
651 pull({ number: 4, status: "open", mergedAt: null }),
652 ],
653 SPAN,
654 );
655 assert.deepEqual(merged.map((change) => [change.kind, change.title, change.detail, change.agent]), [
656 ["merge", "Fix the flaky test", "web#2 · by bo", false],
657 ["merge", "Fix the flaky test", "web#1 · by @g1t, merged by bo", true],
658 ]);
659});
660
661test("what landed, besides merges: direct pushes, releases, packages and production builds, newest first", () => {
662 const fromActivity = landedFromActivity(busyDay(), REPOS, NAMES, "acme");
663 assert.deepEqual(
664 fromActivity.map((change) => [change.kind, change.title, change.detail, change.to, change.agent]),
665 [
666 ["push", "9 commits pushed to main", "web · 3 pushes by ana", "/acme/web/activity", false],
667 ["release", "v1.4.0 · Autumn", "web · released by ana", "/acme/web/releases/tag/v1.4.0", false],
668 ["push", "8 commits pushed to main", "api · 5 pushes by cy", "/acme/api/activity", false],
669 ["package", "api 1.4.0", "container package · published by @g1t", "/acme/-/packages", true],
670 ["push", "4 commits pushed to trunk", "cli · 3 pushes by ana and @margo", "/acme/cli/activity", false],
671 ],
672 );
673 const builds = landedDeploys([
674 { key: "deploy:site:d1", project: "site", kind: "production", outcome: "live", commit: "abcdef1", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d1", by: "g1t" },
675 { key: "deploy:site:d2", project: "site", kind: "preview", outcome: "live", commit: "abcdef2", branch: "fix", at: NOW - HOUR, to: "/acme/site/deployments/d2", by: "bo" },
676 { key: "deploy:site:d3", project: "site", kind: "production", outcome: "failed", commit: "abcdef3", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d3", by: "bo" },
677 ]);
678 assert.deepEqual(builds.map((change) => [change.kind, change.title, change.agent]), [["deploy", "Production of site went live", true]]);
679 const all = landed([fromActivity, builds]);
680 assert.deepEqual(
681 all.map((change) => change.key),
682 ["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"],
683 );
684});
685
686const deploy = (over: Partial<Deployment>): Deployment => ({
687 id: "d1",
688 kind: "production",
689 branch: null,
690 number: null,
691 commit: "abcdef1234",
692 status: "ready",
693 url: "https://site.g1t.page",
694 error: null,
695 warnings: [],
696 buildSeconds: 40,
697 createdBy: "bo",
698 createdAt: iso(NOW - 2 * HOUR),
699 finishedAt: iso(NOW - HOUR),
700 ...over,
701});
702
703test("builds that finished in the span: live or failed, never skipped", () => {
704 const rows = deploysIn(
705 [
706 {
707 slug: "site",
708 deployments: [
709 deploy({}),
710 deploy({ id: "d2", status: "replaced", finishedAt: iso(NOW - DAY) }),
711 deploy({ id: "d3", status: "failed", finishedAt: iso(NOW - 2 * DAY) }),
712 deploy({ id: "d4", status: "skipped", finishedAt: iso(NOW - HOUR) }),
713 deploy({ id: "d5", status: "building", finishedAt: null }),
714 deploy({ id: "d6", finishedAt: iso(NOW - 5 * DAY) }),
715 ],
716 },
717 ],
718 "acme",
719 SPAN,
720 );
721 assert.deepEqual(rows.map((row) => [row.key, row.outcome]), [
722 ["deploy:site:d1", "live"],
723 ["deploy:site:d2", "live"],
724 ["deploy:site:d3", "failed"],
725 ]);
726 assert.equal(rows[0]!.to, "/acme/site/deployments/d1");
727 assert.equal(rows[0]!.commit, "abcdef1");
728});
729
730const request = (over: Partial<InstallRequest>): InstallRequest => ({
731 id: "r1",
732 listing: "integration:sentry",
733 kind: "integration",
734 name: "Sentry",
735 note: "For the error alerts",
736 requested_by: "bo",
737 requested_at: iso(NOW - 2 * DAY),
738 status: "open",
739 resolved_by: null,
740 resolved_at: null,
741 ...over,
742});
743
744const memory = (over: Partial<Memory>): Memory => ({
745 id: "m1",
746 scope: "workspace",
747 workspace: "acme",
748 repo: null,
749 text: "Use Postgres for the ledger",
750 kind: "decision",
751 source: { kind: "person" } as Memory["source"],
752 createdBy: "ada",
753 pinned: false,
754 createdAt: iso(NOW - DAY),
755 updatedAt: iso(NOW - DAY),
756 lastUsedAt: null,
757 ...over,
758});
759
760test("decisions: kept decisions in memory and requests answered, in the span", () => {
761 const rows = decisionsIn(
762 {
763 memories: [memory({}), memory({ id: "m2", kind: "fact" }), memory({ id: "m3", status: "candidate" }), memory({ id: "m4", createdAt: iso(NOW - 6 * DAY) })],
764 requests: [request({}), request({ id: "r2", status: "declined", resolved_by: "ada", resolved_at: iso(NOW - HOUR) })],
765 },
766 "acme",
767 SPAN,
768 );
769 assert.deepEqual(rows.map((row) => row.key), ["request:r2", "memory:m1"]);
770 assert.equal(rows[0]!.text, "Turned down Sentry, which bo asked for");
771});
772
773test("an open install request needs an owner; answered ones don't", () => {
774 assert.equal(installRequestRow(request({}), "acme")?.action.to, "/acme/-/marketplace/requests");
775 assert.equal(installRequestRow(request({ status: "done" }), "acme"), null);
776 const base = { slug: "acme", code: true, codeNeeds: [], capped: [], agents: [], canManage: true, notifications: [], chat: [] };
777 assert.equal(attention({ ...base, requests: [request({})] }).rows[0]?.kind, "install_request");
778 assert.equal(attention({ ...base, requests: null }).rows.length, 0);
779});
780
781test("the workspace's agent budget used up comes first, for those who may raise it", () => {
782 assert.equal(limitRow({ alert: 90, spentMicros: 1, since: NOW }, "acme"), null);
783 const limit = { alert: 100, spentMicros: 500_000_000, since: NOW - DAY };
784 assert.equal(limitRow(limit, "acme")?.action.to, "/acme/-/spend");
785 const base = { slug: "acme", code: true, codeNeeds: [need({})], capped: [], agents: [], notifications: [], chat: [], limit };
786 assert.equal(attention({ ...base, canManage: true }).start?.row.kind, "limit");
787 assert.equal(attention({ ...base, canManage: false }).rows.some((r) => r.kind === "limit"), false);
788});
789
790test("running now: live root sessions, agents' changes and builds going; a capped session is under Needs you", () => {
791 const sessions = runningSessions(
792 [
793 session({ id: "a", status: "working", finished_at: null, created_at: iso(NOW - HOUR) }),
794 session({ id: "b", status: "needs_approval", finished_at: null }),
795 session({ id: "c", status: "queued", finished_at: null, parent_id: "a" }),
796 session({ id: "d", status: "done" }),
797 session({ id: "e", status: "queued", finished_at: null, visible: false, created_at: iso(NOW - 2 * HOUR) }),
798 ],
799 "acme",
800 );
801 assert.deepEqual(sessions.map((row) => [row.key, row.status]), [
802 ["session:a", "Working"],
803 ["session:e", "Queued"],
804 ]);
805 assert.equal(sessions[1]!.title, "A session in a conversation you're not in");
806 const builds = runningDeploys(
807 [
808 { slug: "site", enabled: true, production: null, previews: 0, latest: deploy({ status: "building", finishedAt: null }) },
809 { slug: "docs", enabled: true, production: null, previews: 0, latest: deploy({}) },
810 ],
811 "acme",
812 );
813 assert.deepEqual(builds.map((row) => row.status), ["Building"]);
814 const run = (over: Partial<WorkflowRun>): WorkflowRun => ({
815 id: "run_1",
816 workflowId: "wf_1",
817 path: ".g1t/workflows/ci.yml",
818 name: "CI",
819 title: "Fix the flaky test",
820 number: 12,
821 attempt: 1,
822 event: "push",
823 ref: "refs/heads/main",
824 sha: "abcdef1",
825 pull: null,
826 status: "in_progress",
827 conclusion: null,
828 error: null,
829 actor: "ana",
830 createdAt: iso(NOW - 10 * 60_000),
831 startedAt: iso(NOW - 9 * 60_000),
832 finishedAt: null,
833 ...over,
834 });
835 const workflows = runningWorkflows([
836 { repo, runs: [run({}), run({ id: "run_2", status: "queued", startedAt: null, title: "" }), run({ id: "run_3", status: "completed", conclusion: "success" }), run({ id: "run_4", status: "action_required" })] },
837 ]);
838 assert.deepEqual(
839 workflows.map((row) => [row.key, row.title, row.detail, row.status, row.to]),
840 [
841 ["workflow:acme/web:run_1", "CI: Fix the flaky test", "acme/web main · by ana", "Running", "/acme/web/actions/runs/run_1"],
842 ["workflow:acme/web:run_2", "CI", "acme/web main · by ana", "Queued", "/acme/web/actions/runs/run_2"],
843 ["workflow:acme/web:run_4", "CI: Fix the flaky test", "acme/web main · by ana", "Needs approval", "/acme/web/actions/runs/run_4"],
844 ],
845 );
846 // Longest-going first within a kind: sessions, then changes, then workflow runs, then builds.
847 assert.deepEqual(running([builds, workflows.slice(0, 1), sessions]).map((row) => row.key), ["session:e", "session:a", "workflow:acme/web:run_1", "deploy:site:d1"]);
848});
849
850
851test("the span's months: one, or the ones it crosses", () => {
852 assert.deepEqual(spanMonths(SPAN), ["2026-10"]);
853 assert.deepEqual(spanMonths({ from: Date.parse("2026-09-28T00:00:00Z"), now: NOW }), ["2026-09", "2026-10"]);
854 assert.deepEqual(spanMonths({ from: Date.parse("2025-12-30T00:00:00Z"), now: Date.parse("2026-01-02T00:00:00Z") }), ["2025-12", "2026-01"]);
855});
856
857test("spend in the span: the UTC days it covers across months, at price, without money in", () => {
858 const september = {
859 groups: [
860 { key: "2026-09-29", label: "", chargedMicros: 0, lines: [{ kind: "Agent runs", count: 2, chargedMicros: 1_000_000, costMicros: 0 }] },
861 { key: "2026-09-20", label: "", chargedMicros: 0, lines: [{ kind: "Agent runs", count: 9, chargedMicros: 9_000_000, costMicros: 0 }] },
862 ],
863 totals: { chargedMicros: 10_000_000, paidMicros: 0, costMicros: 0, entries: 2 },
864 };
865 const october = {
866 groups: [
867 {
868 key: "2026-10-09",
869 label: "2026-10-09",
870 chargedMicros: 0,
871 lines: [
872 { kind: "Agent runs", count: 12, chargedMicros: 3_000_000, costMicros: 2_500_000, priceMicros: 4_000_000 },
873 { kind: "Sandbox time", count: 12, chargedMicros: 600_000, costMicros: 500_000 },
874 { kind: "Payments", count: 1, chargedMicros: -50_000_000, costMicros: 0, priceMicros: 0 },
875 ],
876 },
877 ],
878 totals: { chargedMicros: 12_600_000, paidMicros: 50_000_000, costMicros: 0, entries: 4, priceMicros: 13_600_000 },
879 };
880 const spend = spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, 13_609_000);
881 assert.deepEqual([spend.from, spend.to], ["2026-09-29", "2026-10-09"]);
882 assert.equal(spend.totalMicros, 5_600_000);
883 assert.deepEqual(spend.lines.map((l) => [l.kind, l.micros, l.count]), [
884 ["Agent runs", 5_000_000, 14],
885 ["Sandbox time", 600_000, 12],
886 ]);
887 // The month so far is billing's usage report, the top bar's figure, not the statement: it carries what is not yet closed.
888 assert.equal(spend.monthMicros, 13_609_000);
889 assert.equal(spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, null).monthMicros, null);
890});