| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { ActivityDigest, ActorCount, AgentSession, ChatActivity, Deployment, InboxItem, InstallRequest, Memory, RepoDigest, WorkflowRun } from "@g1t/contracts"; |
| 5 | |
| 6 | import { |
| 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 | |
| 47 | const HOUR = 3_600_000; |
| 48 | const DAY = 24 * HOUR; |
| 49 | // Friday 2026-10-09, 18:00 UTC; 11:00 in Los Angeles. |
| 50 | const NOW = Date.parse("2026-10-09T18:00:00Z"); |
| 51 | const LA = "America/Los_Angeles"; |
| 52 | const repo = { namespace: "acme", name: "web" }; |
| 53 | const iso = (ms: number) => new Date(ms).toISOString(); |
| 54 | |
| 55 | function 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 | |
| 69 | function 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 | } |
| 110 | const 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 | |
| 125 | test("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 | |
| 137 | const 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 | |
| 150 | test("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 | |
| 172 | test("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 | |
| 178 | const 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 | |
| 201 | test("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 | |
| 210 | test("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 | |
| 225 | test("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 | |
| 241 | test("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 | |
| 249 | test("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. |
| 259 | const SPAN = { from: NOW - 3 * DAY, now: NOW }; |
| 260 | |
| 261 | test("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 | |
| 268 | test("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 | |
| 287 | test("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 | |
| 295 | test("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 | |
| 320 | test("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 | |
| 329 | test("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 | |
| 338 | test("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 | |
| 343 | test("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 | |
| 377 | test("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 | |
| 388 | test("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 | |
| 397 | const ANA = "user:usr_ana"; |
| 398 | const BO = "user:usr_bo"; |
| 399 | const CY = "user:usr_cy"; |
| 400 | const MARGO = "agent:agt_margo"; |
| 401 | const G1T = "user:usr_g1t_agent"; |
| 402 | const NAMES = { [ANA]: "ana", [BO]: "bo", [CY]: "cy", [MARGO]: "margo" }; |
| 403 | const 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 | |
| 410 | const count = (actor: string, n: number): ActorCount => ({ actor, count: n }); |
| 411 | const 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 | */ |
| 429 | function 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 | |
| 487 | const 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. */ |
| 501 | function 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 | |
| 514 | test("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 | |
| 522 | test("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 | |
| 551 | test("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 | |
| 578 | test("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 | |
| 616 | test("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 | |
| 645 | test("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 | |
| 661 | test("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 | |
| 686 | const 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 | |
| 703 | test("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 | |
| 730 | const 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 | |
| 744 | const 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 | |
| 760 | test("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 | |
| 773 | test("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 | |
| 781 | test("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 | |
| 790 | test("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 | |
| 851 | test("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 | |
| 857 | test("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 | }); |