| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { AgentSession, InboxItem } from "@g1t/contracts"; |
| 5 | |
| 6 | import { |
| 7 | type AttentionRow, |
| 8 | type CodeNeed, |
| 9 | type CodePull, |
| 10 | acceptance, |
| 11 | agentRow, |
| 12 | attention, |
| 13 | chatRow, |
| 14 | codeRow, |
| 15 | dayIn, |
| 16 | daySentence, |
| 17 | dayWork, |
| 18 | daysBefore, |
| 19 | notificationRow, |
| 20 | pullOutcome, |
| 21 | rankAttention, |
| 22 | sessionOutcome, |
| 23 | spendToday, |
| 24 | startHere, |
| 25 | trendLabel, |
| 26 | waitingSentence, |
| 27 | } from "./today.ts"; |
| 28 | |
| 29 | const HOUR = 3_600_000; |
| 30 | const DAY = 24 * HOUR; |
| 31 | // Friday 2026-10-09, 18:00 UTC; 11:00 in Los Angeles. |
| 32 | const NOW = Date.parse("2026-10-09T18:00:00Z"); |
| 33 | const LA = "America/Los_Angeles"; |
| 34 | const repo = { namespace: "acme", name: "web" }; |
| 35 | const iso = (ms: number) => new Date(ms).toISOString(); |
| 36 | |
| 37 | function pull(over: Partial<CodePull> = {}): CodePull { |
| 38 | return { |
| 39 | repo, |
| 40 | number: 1, |
| 41 | title: "Fix the flaky test", |
| 42 | status: "merged", |
| 43 | mergedAt: iso(NOW - HOUR), |
| 44 | createdAt: iso(NOW - 3 * HOUR), |
| 45 | updatedAt: iso(NOW - HOUR), |
| 46 | author: { username: "g1t", kind: "agent" }, |
| 47 | ...over, |
| 48 | }; |
| 49 | } |
| 50 | |
| 51 | function session(over: Partial<AgentSession> = {}): AgentSession { |
| 52 | return { |
| 53 | id: "s1", |
| 54 | workspace_id: "w", |
| 55 | agent_id: "a", |
| 56 | agent_handle: "margo", |
| 57 | agent_name: "Margo", |
| 58 | agent_avatar_seed: "margo", |
| 59 | subagent: null, |
| 60 | kind: "chat", |
| 61 | parent_id: null, |
| 62 | root_id: "s1", |
| 63 | payer_agent_id: "a", |
| 64 | title: "Draft the launch notes", |
| 65 | goal: "", |
| 66 | status: "done", |
| 67 | status_note: null, |
| 68 | summary: null, |
| 69 | channel_id: "c", |
| 70 | channel_kind: "channel", |
| 71 | channel_name: "launch", |
| 72 | card_message_id: null, |
| 73 | asked_by: null, |
| 74 | asked_by_username: "bo", |
| 75 | routine_id: null, |
| 76 | steps: 3, |
| 77 | tool_calls: 2, |
| 78 | input_tokens: 0, |
| 79 | output_tokens: 0, |
| 80 | charged_micros: 400_000, |
| 81 | cap_micros: 2_000_000, |
| 82 | model: null, |
| 83 | outputs: [], |
| 84 | created_at: iso(NOW - 2 * HOUR), |
| 85 | updated_at: iso(NOW - HOUR), |
| 86 | finished_at: iso(NOW - HOUR), |
| 87 | visible: true, |
| 88 | ...over, |
| 89 | }; |
| 90 | } |
| 91 | |
| 92 | test("today is the reader's day, and the seven days before it", () => { |
| 93 | assert.equal(dayIn(NOW, LA), "2026-10-09"); |
| 94 | assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), LA), "2026-10-09"); |
| 95 | assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), null), "2026-10-10"); |
| 96 | assert.equal(dayIn(NOW, "Not/AZone"), "2026-10-09"); |
| 97 | assert.deepEqual(daysBefore(NOW, LA, 3), ["2026-10-08", "2026-10-07", "2026-10-06"]); |
| 98 | }); |
| 99 | |
| 100 | test("an agent's pull request is accepted first time when it merged today with no revision", () => { |
| 101 | assert.equal(pullOutcome(pull(), 0, "2026-10-09", null), "accepted"); |
| 102 | assert.equal(pullOutcome(pull(), 2, "2026-10-09", null), "fixed"); |
| 103 | assert.equal(pullOutcome(pull({ mergedAt: iso(NOW - DAY) }), 0, "2026-10-09", null), null); |
| 104 | assert.equal(pullOutcome(pull({ status: "closed", mergedAt: null }), 0, "2026-10-09", null), "dropped"); |
| 105 | assert.equal(pullOutcome(pull({ status: "open", mergedAt: null }), 1, "2026-10-09", null), "open"); |
| 106 | assert.equal(pullOutcome(pull({ status: "draft", mergedAt: null, updatedAt: iso(NOW - 2 * DAY) }), 0, "2026-10-09", null), null); |
| 107 | }); |
| 108 | |
| 109 | test("a session counts once, at the root of its tree, and is never 'accepted'", () => { |
| 110 | assert.equal(sessionOutcome(session(), "2026-10-09", null), "finished"); |
| 111 | assert.equal(sessionOutcome(session({ parent_id: "s0" }), "2026-10-09", null), null); |
| 112 | assert.equal(sessionOutcome(session({ status: "failed" }), "2026-10-09", null), "dropped"); |
| 113 | assert.equal(sessionOutcome(session({ status: "stopped" }), "2026-10-09", null), "dropped"); |
| 114 | assert.equal(sessionOutcome(session({ status: "working", finished_at: null }), "2026-10-09", null), "open"); |
| 115 | assert.equal(sessionOutcome(session({ status: "needs_approval", finished_at: null, updated_at: iso(NOW - 3 * DAY) }), "2026-10-09", null), null); |
| 116 | assert.equal(sessionOutcome(session({ finished_at: iso(NOW - DAY) }), "2026-10-09", null), null); |
| 117 | }); |
| 118 | |
| 119 | test("accepted first time is accepted over every agent pull request that settled", () => { |
| 120 | assert.deepEqual(acceptance(["accepted", "accepted", "fixed", "dropped", "open", "finished"]), { value: 0.5, of: 4 }); |
| 121 | assert.equal(acceptance(["open", "finished"]), null); |
| 122 | }); |
| 123 | |
| 124 | test("the day's work: pull requests and sessions, sources, and the week before", () => { |
| 125 | const work = dayWork({ |
| 126 | now: NOW, |
| 127 | timeZone: null, |
| 128 | slug: "acme", |
| 129 | code: { |
| 130 | pulls: [ |
| 131 | pull({ number: 1 }), |
| 132 | pull({ number: 2 }), |
| 133 | pull({ number: 3, status: "open", mergedAt: null }), |
| 134 | // A person's pull request is not agent work. |
| 135 | pull({ number: 4, author: { username: "bo", kind: "user" } }), |
| 136 | // The week before: one accepted, one fixed. |
| 137 | pull({ number: 5, mergedAt: iso(NOW - 2 * DAY), updatedAt: iso(NOW - 2 * DAY) }), |
| 138 | pull({ number: 6, mergedAt: iso(NOW - 3 * DAY), updatedAt: iso(NOW - 3 * DAY) }), |
| 139 | // Older than a week: not counted. |
| 140 | pull({ number: 7, mergedAt: iso(NOW - 9 * DAY), updatedAt: iso(NOW - 9 * DAY) }), |
| 141 | ], |
| 142 | revisions: { "acme/web#2": 1, "acme/web#6": 2 }, |
| 143 | complete: true, |
| 144 | }, |
| 145 | sessions: { sessions: [session(), session({ id: "s2", kind: "routine", status: "working", finished_at: null }), session({ id: "s3", parent_id: "s1" })], complete: true }, |
| 146 | }); |
| 147 | assert.equal(work.tasks.length, 5); |
| 148 | assert.deepEqual(work.counts, { accepted: 1, fixed: 1, finished: 1, open: 2, dropped: 0 }); |
| 149 | assert.deepEqual(work.rate, { value: 0.5, of: 2 }); |
| 150 | assert.deepEqual(work.lastWeek, { value: 0.5, of: 2 }); |
| 151 | assert.deepEqual(work.sources, [ |
| 152 | { source: "code", count: 3 }, |
| 153 | { source: "chat", count: 1 }, |
| 154 | { source: "schedule", count: 1 }, |
| 155 | ]); |
| 156 | // Open work goes last on the strip. |
| 157 | assert.deepEqual(work.tasks.slice(-2).map((t) => t.outcome), ["open", "open"]); |
| 158 | assert.equal(work.code, "read"); |
| 159 | assert.equal(work.partial, false); |
| 160 | assert.equal(daySentence(work), "Agents finished 3 tasks. 1 needed a fix after review, and 2 are still open."); |
| 161 | }); |
| 162 | |
| 163 | test("no trend when the week could not all be read, and none without Code", () => { |
| 164 | const partial = dayWork({ now: NOW, timeZone: null, slug: "acme", code: { pulls: [pull()], revisions: {}, complete: false }, sessions: null }); |
| 165 | assert.equal(partial.lastWeek, null); |
| 166 | assert.equal(partial.partial, true); |
| 167 | assert.equal(partial.sessions, "unavailable"); |
| 168 | const member = dayWork({ now: NOW, timeZone: null, slug: "acme", code: "no_access", sessions: { sessions: [session()], complete: true } }); |
| 169 | assert.equal(member.code, "no_access"); |
| 170 | assert.equal(member.rate, null); |
| 171 | assert.equal(member.tasks.every((t) => t.source !== "code"), true); |
| 172 | }); |
| 173 | |
| 174 | test("the trend is in whole points", () => { |
| 175 | assert.equal(trendLabel({ value: 0.89 }, { value: 0.83 }), "+6 pts vs the last 7 days"); |
| 176 | assert.equal(trendLabel({ value: 0.5 }, { value: 0.75 }), "−25 pts vs the last 7 days"); |
| 177 | assert.equal(trendLabel({ value: 0.5 }, { value: 0.5 }), "Same as the last 7 days"); |
| 178 | assert.equal(trendLabel(null, { value: 0.5 }), null); |
| 179 | }); |
| 180 | |
| 181 | test("the sentence for a quiet day, and for what waits", () => { |
| 182 | const quiet = dayWork({ now: NOW, timeZone: null, slug: "acme", code: { pulls: [], revisions: {}, complete: true }, sessions: { sessions: [], complete: true } }); |
| 183 | assert.equal(daySentence(quiet), "No agent work today yet."); |
| 184 | assert.equal(waitingSentence(0), "Nothing is waiting on you."); |
| 185 | assert.equal(waitingSentence(1), "1 thing is waiting on you."); |
| 186 | assert.equal(waitingSentence(8), "8 things are waiting on you."); |
| 187 | }); |
| 188 | |
| 189 | const need = (over: Partial<CodeNeed>): CodeNeed => ({ |
| 190 | key: "pull:p1", |
| 191 | reason: "ready_to_merge", |
| 192 | repo, |
| 193 | ref: "#12", |
| 194 | title: "Add retries", |
| 195 | ask: "Waiting for you to merge it.", |
| 196 | by: { name: "g1t", agent: true }, |
| 197 | for: "bo", |
| 198 | at: NOW - 2 * HOUR, |
| 199 | to: "/acme/web/pull/12", |
| 200 | facts: [{ label: "Files changed", value: "4", tone: null }], |
| 201 | ...over, |
| 202 | }); |
| 203 | |
| 204 | test("Code's needs become rows with what is at stake", () => { |
| 205 | const ready = codeRow(need({})); |
| 206 | assert.equal(ready.kind, "ready"); |
| 207 | assert.equal(ready.stake?.text, "4 files to land"); |
| 208 | assert.equal(ready.action.label, "Merge"); |
| 209 | const deploy = codeRow(need({ key: "deploy:site", reason: "blocking", facts: [{ label: "Production", value: "Serving the build before", tone: "good" }] })); |
| 210 | assert.equal(deploy.kind, "deploy"); |
| 211 | assert.equal(deploy.stake?.text, "Production on an older build"); |
| 212 | assert.equal(codeRow(need({ reason: "checks_failing" })).kind, "pull_blocked"); |
| 213 | 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"); |
| 214 | }); |
| 215 | |
| 216 | const row = (over: Partial<AttentionRow>): AttentionRow => ({ |
| 217 | key: "k", |
| 218 | kind: "chat", |
| 219 | title: "t", |
| 220 | detail: "d", |
| 221 | stake: null, |
| 222 | action: { label: "Open", to: "/x" }, |
| 223 | owner: null, |
| 224 | at: NOW, |
| 225 | from: "chat", |
| 226 | ...over, |
| 227 | }); |
| 228 | |
| 229 | test("Start here: the most pressing tier first, then what has waited longest", () => { |
| 230 | const rows = [ |
| 231 | row({ key: "chat:old", kind: "chat", at: NOW - 5 * DAY, action: { label: "Reply", to: "/acme/-/chat/general" } }), |
| 232 | row({ key: "ready:new", kind: "ready", at: NOW - HOUR, action: { label: "Merge", to: "/acme/web/pull/2" } }), |
| 233 | row({ key: "ready:old", kind: "ready", at: NOW - 3 * HOUR, action: { label: "Merge", to: "/acme/web/pull/1" } }), |
| 234 | row({ key: "session:s", kind: "session_cap", at: NOW - 10 * 60_000, action: { label: "Approve more", to: "/acme/-/agents/margo/sessions/s" } }), |
| 235 | ]; |
| 236 | const ranked = rankAttention(rows); |
| 237 | assert.deepEqual(ranked.map((r) => r.key), ["session:s", "ready:old", "ready:new", "chat:old"]); |
| 238 | const start = startHere(ranked); |
| 239 | assert.equal(start?.row.key, "session:s"); |
| 240 | assert.match(start?.why ?? "", /spend cap/); |
| 241 | // A failed production deploy beats everything. |
| 242 | const withDeploy = rankAttention([...rows, row({ key: "deploy:site", kind: "deploy", at: NOW, action: { label: "See the build", to: "/acme/site/deployments/1" } })]); |
| 243 | assert.equal(startHere(withDeploy)?.row.key, "deploy:site"); |
| 244 | // Ties in tier say so. |
| 245 | const two = startHere(rankAttention(rows.filter((r) => r.kind === "ready"))); |
| 246 | assert.equal(two?.row.key, "ready:old"); |
| 247 | assert.match(two?.why ?? "", /Of the 2 like it, it has waited longest/); |
| 248 | assert.equal(startHere([]), null); |
| 249 | }); |
| 250 | |
| 251 | test("the same order whatever order the rows came in", () => { |
| 252 | const rows = [row({ key: "b", kind: "review", at: NOW }), row({ key: "a", kind: "review", at: NOW, action: { label: "Review", to: "/y" } })]; |
| 253 | assert.deepEqual(rankAttention(rows).map((r) => r.key), ["a", "b"]); |
| 254 | assert.deepEqual(rankAttention([...rows].reverse()).map((r) => r.key), ["a", "b"]); |
| 255 | }); |
| 256 | |
| 257 | const item = (over: Partial<InboxItem>): InboxItem => ({ |
| 258 | id: "n1", |
| 259 | reason: "ci_activity", |
| 260 | severity: "error", |
| 261 | title: "Checks failed on #12", |
| 262 | body: "Add retries", |
| 263 | event: "check.failed", |
| 264 | repo: "acme/web", |
| 265 | workspace: "acme", |
| 266 | subject: "pull", |
| 267 | number: 12, |
| 268 | url: "/acme/web/pull/12", |
| 269 | actor: "g1t", |
| 270 | count: 1, |
| 271 | createdAt: iso(NOW - HOUR), |
| 272 | updatedAt: iso(NOW - HOUR), |
| 273 | readAt: null, |
| 274 | doneAt: null, |
| 275 | saved: false, |
| 276 | snoozedUntil: null, |
| 277 | ...over, |
| 278 | }); |
| 279 | |
| 280 | test("notifications: warnings and failures in this workspace; none about repositories without Code", () => { |
| 281 | assert.equal(notificationRow(item({}), "acme", true)?.kind, "notification"); |
| 282 | assert.equal(notificationRow(item({}), "acme", false), null); |
| 283 | assert.equal(notificationRow(item({ repo: null, url: "/acme/-/agents/margo" }), "acme", false)?.kind, "notification"); |
| 284 | assert.equal(notificationRow(item({ severity: "info" }), "acme", true), null); |
| 285 | assert.equal(notificationRow(item({ workspace: "other", repo: "other/web" }), "acme", true), null); |
| 286 | assert.equal(notificationRow(item({ readAt: iso(NOW) }), "acme", true), null); |
| 287 | }); |
| 288 | |
| 289 | test("a notification about something already listed is left out", () => { |
| 290 | const out = attention({ |
| 291 | slug: "acme", |
| 292 | code: true, |
| 293 | codeNeeds: [need({})], |
| 294 | capped: [], |
| 295 | agents: [], |
| 296 | canManage: false, |
| 297 | notifications: [item({})], |
| 298 | chat: [], |
| 299 | }); |
| 300 | assert.deepEqual(out.rows.map((r) => r.key), ["pull:p1"]); |
| 301 | assert.deepEqual(out.missing, []); |
| 302 | }); |
| 303 | |
| 304 | test("without Code access no Code row is asked for or shown, and a silent source is named", () => { |
| 305 | const out = attention({ |
| 306 | slug: "acme", |
| 307 | code: false, |
| 308 | codeNeeds: [need({})], |
| 309 | capped: null, |
| 310 | agents: null, |
| 311 | canManage: false, |
| 312 | notifications: [item({}), item({ id: "n2", repo: null, reason: "agent", severity: "warning", url: "/acme/-/agents/margo" })], |
| 313 | chat: null, |
| 314 | }); |
| 315 | assert.deepEqual(out.rows.map((r) => r.key), ["notification:n2"]); |
| 316 | assert.equal(out.rows.some((r) => r.from === "code"), false); |
| 317 | assert.deepEqual(out.missing, ["Agents", "Chat"]); |
| 318 | }); |
| 319 | |
| 320 | test("an agent out of budget is listed only for those who may raise it", () => { |
| 321 | 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) }; |
| 322 | assert.equal(agentRow(agent, "acme")?.stake?.text, "$0.00 left"); |
| 323 | const base = { slug: "acme", code: true, codeNeeds: [], capped: [], notifications: [], chat: [] }; |
| 324 | assert.equal(attention({ ...base, agents: [agent], canManage: false }).rows.length, 0); |
| 325 | assert.equal(attention({ ...base, agents: [agent], canManage: true }).rows[0]?.kind, "agent_budget"); |
| 326 | }); |
| 327 | |
| 328 | test("chat: mentions anywhere and unread direct messages, never muted", () => { |
| 329 | const channel = { id: "c1", kind: "channel" as const, name: "general", last_message_at: iso(NOW) }; |
| 330 | assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 0 }, "acme"), null); |
| 331 | assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 2 }, "acme")?.stake?.text, "2 mentions"); |
| 332 | assert.equal(chatRow({ channel, title: "general", muted: true, unread: 4, mentions: 2 }, "acme"), null); |
| 333 | const dm = chatRow({ channel: { ...channel, kind: "dm", name: null }, title: "Bo Reed", muted: false, unread: 1, mentions: 0 }, "acme"); |
| 334 | assert.equal(dm?.action.to, "/acme/-/chat/dm/c1"); |
| 335 | }); |
| 336 | |
| 337 | test("spent today: today's UTC group of the statement, at price, without money in", () => { |
| 338 | const spend = spendToday( |
| 339 | { |
| 340 | groups: [ |
| 341 | { |
| 342 | key: "2026-10-09", |
| 343 | label: "2026-10-09", |
| 344 | chargedMicros: 0, |
| 345 | lines: [ |
| 346 | { kind: "Agent runs", count: 12, chargedMicros: 3_000_000, costMicros: 2_500_000, priceMicros: 4_000_000 }, |
| 347 | { kind: "Sandbox time", count: 12, chargedMicros: 600_000, costMicros: 500_000 }, |
| 348 | { kind: "Payments", count: 1, chargedMicros: -50_000_000, costMicros: 0, priceMicros: 0 }, |
| 349 | ], |
| 350 | }, |
| 351 | { key: "2026-10-08", label: "2026-10-08", chargedMicros: 0, lines: [{ kind: "Agent runs", count: 3, chargedMicros: 9_000_000, costMicros: 0 }] }, |
| 352 | ], |
| 353 | totals: { chargedMicros: 12_600_000, paidMicros: 50_000_000, costMicros: 0, entries: 4, priceMicros: 13_600_000 }, |
| 354 | }, |
| 355 | NOW, |
| 356 | ); |
| 357 | assert.equal(spend.day, "2026-10-09"); |
| 358 | assert.equal(spend.totalMicros, 4_600_000); |
| 359 | assert.deepEqual(spend.lines.map((l) => l.kind), ["Agent runs", "Sandbox time"]); |
| 360 | assert.equal(spend.monthMicros, 13_600_000); |
| 361 | }); |