Skip to content
893 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("user:agt_01abc"), "agent", "an agent named by its own id under user: is still a robot");
518 assert.equal(actorKind("user:wsp_01abc"), "agent", "the workspace's own upkeep is g1t's");
519 assert.equal(actorKind("user:g1t"), "agent");
520 assert.equal(actorKind(ANA), "person");
521 assert.equal(actorKind(""), "person");
522 assert.equal(actorKind("user:usr_bot", new Set(["user:usr_bot"])), "agent");
523});
524
525test("the digest: what people did, in lines with counts, and who", () => {
526 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, work: busyWork(), code: true });
527 assert.deepEqual(
528 digest.people.lines.map((line) => [line.text, line.to]),
529 [
530 ["3 people pushed 41 commits to 4 projects", "/acme/-/repositories"],
531 ["opened 2 pull requests, merged 5", "/acme/-/repositories"],
532 ["reviewed 2 pull requests", "/acme/web/pulls"],
533 ["opened 2 issues, closed 1", "/acme/web/issues"],
534 ["left 4 comments", "/acme/web/issues"],
535 ["2 deploys went out", "/acme/web/deployments"],
536 ["published 1 release", "/acme/web/releases"],
537 ["created 1 doc, edited 6 docs", "/acme/-/artifacts"],
538 ["sent 105 messages in 5 conversations", "/acme/-/chat"],
539 ],
540 );
541 // Faces: most active first, by name.
542 assert.deepEqual(
543 digest.people.actors.map((actor) => [actor.name, actor.kind]),
544 [
545 ["ana", "person"],
546 ["bo", "person"],
547 ["cy", "person"],
548 ],
549 );
550 assert.deepEqual(digest.missing, []);
551 assert.equal(digest.partial, false);
552});
553
554test("the digest: what agents did, their tasks first, and g1t and the workspace's agents as agents", () => {
555 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, work: busyWork(), code: true });
556 assert.deepEqual(
557 digest.agents.lines.map((line) => line.text),
558 [
559 "finished 7 tasks, 1 didn't finish",
560 "ran 2 sessions",
561 "pushed 6 commits to 2 projects",
562 "opened 3 pull requests",
563 "reviewed 2 pull requests",
564 "closed 1 issue",
565 "left 3 comments",
566 "published 1 package version",
567 "edited 2 docs",
568 "replied 15 times in chat",
569 ],
570 );
571 assert.deepEqual(
572 digest.agents.actors.map((actor) => [actor.name, actor.kind, actor.key]),
573 [
574 ["margo", "agent", MARGO],
575 ["g1t", "agent", G1T],
576 ],
577 );
578 assert.deepEqual(digest.summary, { people: 3, commits: 41, merged: 5, finished: 7, dropped: 1, said: 105, anything: true, peopleRead: true });
579});
580
581test("the digest says which part didn't answer, and counts what could be read", () => {
582 const noChat = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: null, names: NAMES, work: busyWork(), code: true });
583 assert.deepEqual(noChat.missing, ["Chat"]);
584 assert.equal(noChat.people.lines.some((line) => line.key === "chat"), false);
585 const noCode = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: busyChat, names: NAMES, work: null, code: true });
586 assert.deepEqual(noCode.missing, ["Code", "Agents"]);
587 assert.deepEqual(
588 noCode.people.lines.map((line) => line.text),
589 ["sent 105 messages in 5 conversations"],
590 );
591 assert.equal(noCode.summary.peopleRead, true);
592 // Without Code access, events are read for artifacts only: no repositories, nothing missing.
593 const member = digestOf({ slug: "acme", repos: {}, activity: { ...busyDay(), repos: [] }, chat: busyChat, names: NAMES, work: busyWork(), code: false });
594 assert.deepEqual(member.missing, []);
595 assert.deepEqual(
596 member.people.lines.map((line) => line.key),
597 ["docs", "chat"],
598 );
599 // A digest cut short says so.
600 const cut = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), complete: false }, chat: busyChat, names: NAMES, work: busyWork(), code: true });
601 assert.equal(cut.partial, true);
602 // Nobody named: a mirror's pushes are still commits.
603 const mirror = digestOf({
604 slug: "acme",
605 repos: REPOS,
606 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) } } }] },
607 chat: null,
608 names: {},
609 work: null,
610 code: true,
611 });
612 assert.deepEqual(
613 mirror.people.lines.map((line) => [line.text, line.to]),
614 [["7 commits pushed to 1 project", "/acme/web/activity"]],
615 );
616 assert.equal(mirror.summary.people, 0);
617});
618
619test("the sentence: people first, then the agents' tasks, then what landed; honest when quiet or unread", () => {
620 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, work: busyWork(), code: true });
621 assert.equal(
622 spanSentence({ words: "Since Tuesday evening" }, digest, 12),
623 "Since Tuesday evening, 3 people pushed 41 commits and merged 5 changes, agents finished 7 tasks, and 12 changes landed.",
624 );
625 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 });
626 quiet.summary.finished = 0;
627 quiet.summary.dropped = 0;
628 quiet.summary.anything = false;
629 assert.equal(spanSentence({ words: "Since Tuesday evening" }, quiet, 0), "Quiet since Tuesday evening: nothing pushed, merged, deployed or said.");
630 assert.equal(spanSentence({ words: "In the last 24 hours" }, quiet, 0), "Quiet in the last 24 hours: nothing pushed, merged, deployed or said.");
631 // Only agents' tasks and merges.
632 const agentsOnly = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: { ...busyChat, authors: [], messages: 0, channels: 0 }, names: {}, work: busyWork(), code: true });
633 assert.equal(spanSentence({ words: "Since this morning" }, agentsOnly, 5), "Since this morning, agents finished 7 tasks and 5 changes landed.");
634 // People only talked.
635 const talked = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: busyChat, names: NAMES, work: null, code: true });
636 assert.equal(spanSentence({ words: "Since an hour ago" }, talked, 0), "Since an hour ago, 3 people sent 105 messages.");
637 // Neither events nor chat answered.
638 const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, work: busyWork(), code: true });
639 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.");
640 const nothingRead = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, work: null, code: true });
641 assert.equal(spanSentence({ words: "Since this morning" }, nothingRead, 0), "Since this morning, what people did couldn't be read, and no agent task settled.");
642 assert.equal(spanSentence({ words: "Since this morning" }, null, null), "Since this morning, what happened couldn't be read.");
643 assert.equal(waitingSentence(0), "Nothing needs you right now.");
644 assert.equal(waitingSentence(1), "1 thing needs you.");
645 assert.equal(waitingSentence(8), "8 things need you.");
646});
647
648test("what landed: every merge in the span, a person's or an agent's, newest first", () => {
649 const merged = landedIn(
650 [
651 pull({ number: 1, mergedAt: iso(NOW - 2 * DAY), mergedBy: "bo" }),
652 pull({ number: 2, author: { username: "bo", kind: "user" }, mergedAt: iso(NOW - HOUR) }),
653 pull({ number: 3, mergedAt: iso(NOW - 5 * DAY) }),
654 pull({ number: 4, status: "open", mergedAt: null }),
655 ],
656 SPAN,
657 );
658 assert.deepEqual(merged.map((change) => [change.kind, change.title, change.detail, change.agent]), [
659 ["merge", "Fix the flaky test", "web#2 · by bo", false],
660 ["merge", "Fix the flaky test", "web#1 · by @g1t, merged by bo", true],
661 ]);
662});
663
664test("what landed, besides merges: direct pushes, releases, packages and production builds, newest first", () => {
665 const fromActivity = landedFromActivity(busyDay(), REPOS, NAMES, "acme");
666 assert.deepEqual(
667 fromActivity.map((change) => [change.kind, change.title, change.detail, change.to, change.agent]),
668 [
669 ["push", "9 commits pushed to main", "web · 3 pushes by ana", "/acme/web/activity", false],
670 ["release", "v1.4.0 · Autumn", "web · released by ana", "/acme/web/releases/tag/v1.4.0", false],
671 ["push", "8 commits pushed to main", "api · 5 pushes by cy", "/acme/api/activity", false],
672 ["package", "api 1.4.0", "container package · published by @g1t", "/acme/-/packages", true],
673 ["push", "4 commits pushed to trunk", "cli · 3 pushes by ana and @margo", "/acme/cli/activity", false],
674 ],
675 );
676 const builds = landedDeploys([
677 { key: "deploy:site:d1", project: "site", kind: "production", outcome: "live", commit: "abcdef1", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d1", by: "g1t" },
678 { key: "deploy:site:d2", project: "site", kind: "preview", outcome: "live", commit: "abcdef2", branch: "fix", at: NOW - HOUR, to: "/acme/site/deployments/d2", by: "bo" },
679 { key: "deploy:site:d3", project: "site", kind: "production", outcome: "failed", commit: "abcdef3", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d3", by: "bo" },
680 ]);
681 assert.deepEqual(builds.map((change) => [change.kind, change.title, change.agent]), [["deploy", "Production of site went live", true]]);
682 const all = landed([fromActivity, builds]);
683 assert.deepEqual(
684 all.map((change) => change.key),
685 ["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"],
686 );
687});
688
689const deploy = (over: Partial<Deployment>): Deployment => ({
690 id: "d1",
691 kind: "production",
692 branch: null,
693 number: null,
694 commit: "abcdef1234",
695 status: "ready",
696 url: "https://site.g1t.page",
697 error: null,
698 warnings: [],
699 buildSeconds: 40,
700 createdBy: "bo",
701 createdAt: iso(NOW - 2 * HOUR),
702 finishedAt: iso(NOW - HOUR),
703 ...over,
704});
705
706test("builds that finished in the span: live or failed, never skipped", () => {
707 const rows = deploysIn(
708 [
709 {
710 slug: "site",
711 deployments: [
712 deploy({}),
713 deploy({ id: "d2", status: "replaced", finishedAt: iso(NOW - DAY) }),
714 deploy({ id: "d3", status: "failed", finishedAt: iso(NOW - 2 * DAY) }),
715 deploy({ id: "d4", status: "skipped", finishedAt: iso(NOW - HOUR) }),
716 deploy({ id: "d5", status: "building", finishedAt: null }),
717 deploy({ id: "d6", finishedAt: iso(NOW - 5 * DAY) }),
718 ],
719 },
720 ],
721 "acme",
722 SPAN,
723 );
724 assert.deepEqual(rows.map((row) => [row.key, row.outcome]), [
725 ["deploy:site:d1", "live"],
726 ["deploy:site:d2", "live"],
727 ["deploy:site:d3", "failed"],
728 ]);
729 assert.equal(rows[0]!.to, "/acme/site/deployments/d1");
730 assert.equal(rows[0]!.commit, "abcdef1");
731});
732
733const request = (over: Partial<InstallRequest>): InstallRequest => ({
734 id: "r1",
735 listing: "integration:sentry",
736 kind: "integration",
737 name: "Sentry",
738 note: "For the error alerts",
739 requested_by: "bo",
740 requested_at: iso(NOW - 2 * DAY),
741 status: "open",
742 resolved_by: null,
743 resolved_at: null,
744 ...over,
745});
746
747const memory = (over: Partial<Memory>): Memory => ({
748 id: "m1",
749 scope: "workspace",
750 workspace: "acme",
751 repo: null,
752 text: "Use Postgres for the ledger",
753 kind: "decision",
754 source: { kind: "person" } as Memory["source"],
755 createdBy: "ada",
756 pinned: false,
757 createdAt: iso(NOW - DAY),
758 updatedAt: iso(NOW - DAY),
759 lastUsedAt: null,
760 ...over,
761});
762
763test("decisions: kept decisions in memory and requests answered, in the span", () => {
764 const rows = decisionsIn(
765 {
766 memories: [memory({}), memory({ id: "m2", kind: "fact" }), memory({ id: "m3", status: "candidate" }), memory({ id: "m4", createdAt: iso(NOW - 6 * DAY) })],
767 requests: [request({}), request({ id: "r2", status: "declined", resolved_by: "ada", resolved_at: iso(NOW - HOUR) })],
768 },
769 "acme",
770 SPAN,
771 );
772 assert.deepEqual(rows.map((row) => row.key), ["request:r2", "memory:m1"]);
773 assert.equal(rows[0]!.text, "Turned down Sentry, which bo asked for");
774});
775
776test("an open install request needs an owner; answered ones don't", () => {
777 assert.equal(installRequestRow(request({}), "acme")?.action.to, "/acme/-/marketplace/requests");
778 assert.equal(installRequestRow(request({ status: "done" }), "acme"), null);
779 const base = { slug: "acme", code: true, codeNeeds: [], capped: [], agents: [], canManage: true, notifications: [], chat: [] };
780 assert.equal(attention({ ...base, requests: [request({})] }).rows[0]?.kind, "install_request");
781 assert.equal(attention({ ...base, requests: null }).rows.length, 0);
782});
783
784test("the workspace's agent budget used up comes first, for those who may raise it", () => {
785 assert.equal(limitRow({ alert: 90, spentMicros: 1, since: NOW }, "acme"), null);
786 const limit = { alert: 100, spentMicros: 500_000_000, since: NOW - DAY };
787 assert.equal(limitRow(limit, "acme")?.action.to, "/acme/-/spend");
788 const base = { slug: "acme", code: true, codeNeeds: [need({})], capped: [], agents: [], notifications: [], chat: [], limit };
789 assert.equal(attention({ ...base, canManage: true }).start?.row.kind, "limit");
790 assert.equal(attention({ ...base, canManage: false }).rows.some((r) => r.kind === "limit"), false);
791});
792
793test("running now: live root sessions, agents' changes and builds going; a capped session is under Needs you", () => {
794 const sessions = runningSessions(
795 [
796 session({ id: "a", status: "working", finished_at: null, created_at: iso(NOW - HOUR) }),
797 session({ id: "b", status: "needs_approval", finished_at: null }),
798 session({ id: "c", status: "queued", finished_at: null, parent_id: "a" }),
799 session({ id: "d", status: "done" }),
800 session({ id: "e", status: "queued", finished_at: null, visible: false, created_at: iso(NOW - 2 * HOUR) }),
801 ],
802 "acme",
803 );
804 assert.deepEqual(sessions.map((row) => [row.key, row.status]), [
805 ["session:a", "Working"],
806 ["session:e", "Queued"],
807 ]);
808 assert.equal(sessions[1]!.title, "A session in a conversation you're not in");
809 const builds = runningDeploys(
810 [
811 { slug: "site", enabled: true, production: null, previews: 0, latest: deploy({ status: "building", finishedAt: null }) },
812 { slug: "docs", enabled: true, production: null, previews: 0, latest: deploy({}) },
813 ],
814 "acme",
815 );
816 assert.deepEqual(builds.map((row) => row.status), ["Building"]);
817 const run = (over: Partial<WorkflowRun>): WorkflowRun => ({
818 id: "run_1",
819 workflowId: "wf_1",
820 path: ".g1t/workflows/ci.yml",
821 name: "CI",
822 title: "Fix the flaky test",
823 number: 12,
824 attempt: 1,
825 event: "push",
826 ref: "refs/heads/main",
827 sha: "abcdef1",
828 pull: null,
829 status: "in_progress",
830 conclusion: null,
831 error: null,
832 actor: "ana",
833 createdAt: iso(NOW - 10 * 60_000),
834 startedAt: iso(NOW - 9 * 60_000),
835 finishedAt: null,
836 ...over,
837 });
838 const workflows = runningWorkflows([
839 { 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" })] },
840 ]);
841 assert.deepEqual(
842 workflows.map((row) => [row.key, row.title, row.detail, row.status, row.to]),
843 [
844 ["workflow:acme/web:run_1", "CI: Fix the flaky test", "acme/web main · by ana", "Running", "/acme/web/actions/runs/run_1"],
845 ["workflow:acme/web:run_2", "CI", "acme/web main · by ana", "Queued", "/acme/web/actions/runs/run_2"],
846 ["workflow:acme/web:run_4", "CI: Fix the flaky test", "acme/web main · by ana", "Needs approval", "/acme/web/actions/runs/run_4"],
847 ],
848 );
849 // Longest-going first within a kind: sessions, then changes, then workflow runs, then builds.
850 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"]);
851});
852
853
854test("the span's months: one, or the ones it crosses", () => {
855 assert.deepEqual(spanMonths(SPAN), ["2026-10"]);
856 assert.deepEqual(spanMonths({ from: Date.parse("2026-09-28T00:00:00Z"), now: NOW }), ["2026-09", "2026-10"]);
857 assert.deepEqual(spanMonths({ from: Date.parse("2025-12-30T00:00:00Z"), now: Date.parse("2026-01-02T00:00:00Z") }), ["2025-12", "2026-01"]);
858});
859
860test("spend in the span: the UTC days it covers across months, at price, without money in", () => {
861 const september = {
862 groups: [
863 { key: "2026-09-29", label: "", chargedMicros: 0, lines: [{ kind: "Agent runs", count: 2, chargedMicros: 1_000_000, costMicros: 0 }] },
864 { key: "2026-09-20", label: "", chargedMicros: 0, lines: [{ kind: "Agent runs", count: 9, chargedMicros: 9_000_000, costMicros: 0 }] },
865 ],
866 totals: { chargedMicros: 10_000_000, paidMicros: 0, costMicros: 0, entries: 2 },
867 };
868 const october = {
869 groups: [
870 {
871 key: "2026-10-09",
872 label: "2026-10-09",
873 chargedMicros: 0,
874 lines: [
875 { kind: "Agent runs", count: 12, chargedMicros: 3_000_000, costMicros: 2_500_000, priceMicros: 4_000_000 },
876 { kind: "Sandbox time", count: 12, chargedMicros: 600_000, costMicros: 500_000 },
877 { kind: "Payments", count: 1, chargedMicros: -50_000_000, costMicros: 0, priceMicros: 0 },
878 ],
879 },
880 ],
881 totals: { chargedMicros: 12_600_000, paidMicros: 50_000_000, costMicros: 0, entries: 4, priceMicros: 13_600_000 },
882 };
883 const spend = spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, 13_609_000);
884 assert.deepEqual([spend.from, spend.to], ["2026-09-29", "2026-10-09"]);
885 assert.equal(spend.totalMicros, 5_600_000);
886 assert.deepEqual(spend.lines.map((l) => [l.kind, l.micros, l.count]), [
887 ["Agent runs", 5_000_000, 14],
888 ["Sandbox time", 600_000, 12],
889 ]);
890 // The month so far is billing's usage report, the top bar's figure, not the statement: it carries what is not yet closed.
891 assert.equal(spend.monthMicros, 13_609_000);
892 assert.equal(spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, null).monthMicros, null);
893});