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