| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { ActivityDigest, ActorCount, ChatActivity, Deployment, InboxItem, RepoDigest, WorkItem, WorkflowRun } from "@g1t/contracts"; |
| 5 | |
| 6 | import { |
| 7 | type AttentionRow, |
| 8 | type CodeNeed, |
| 9 | type CodePull, |
| 10 | type DeployRow, |
| 11 | type LandedChange, |
| 12 | actorKind, |
| 13 | agentLabel, |
| 14 | attention, |
| 15 | awayOf, |
| 16 | chatRow, |
| 17 | codeRow, |
| 18 | dayIn, |
| 19 | deploysIn, |
| 20 | digestOf, |
| 21 | duration, |
| 22 | explainQuestion, |
| 23 | landed, |
| 24 | landedByDay, |
| 25 | landedDeploys, |
| 26 | landedFromActivity, |
| 27 | landedIn, |
| 28 | nextRow, |
| 29 | nextWhy, |
| 30 | notificationRow, |
| 31 | nowRows, |
| 32 | parseWindow, |
| 33 | pushRows, |
| 34 | rankAttention, |
| 35 | running, |
| 36 | runningDeploys, |
| 37 | runningWorkflows, |
| 38 | sessionTrailers, |
| 39 | settledIn, |
| 40 | shippedAsks, |
| 41 | shippedLabel, |
| 42 | sinceWords, |
| 43 | spanFor, |
| 44 | spanMonths, |
| 45 | spanSentence, |
| 46 | spendIn, |
| 47 | startHere, |
| 48 | summarizeQuestion, |
| 49 | triageRow, |
| 50 | waitingSentence, |
| 51 | withSessions, |
| 52 | withShipped, |
| 53 | } from "./home.ts"; |
| 54 | |
| 55 | const HOUR = 3_600_000; |
| 56 | const DAY = 24 * HOUR; |
| 57 | // Friday 2026-10-09, 18:00 UTC; 11:00 in Los Angeles. |
| 58 | const NOW = Date.parse("2026-10-09T18:00:00Z"); |
| 59 | const LA = "America/Los_Angeles"; |
| 60 | const repo = { namespace: "acme", name: "web" }; |
| 61 | const iso = (ms: number) => new Date(ms).toISOString(); |
| 62 | |
| 63 | function pull(over: Partial<CodePull> = {}): CodePull { |
| 64 | return { |
| 65 | repo, |
| 66 | number: 1, |
| 67 | title: "Fix the flaky test", |
| 68 | status: "merged", |
| 69 | mergedAt: iso(NOW - HOUR), |
| 70 | createdAt: iso(NOW - 3 * HOUR), |
| 71 | updatedAt: iso(NOW - HOUR), |
| 72 | author: { username: "g1t", kind: "agent" }, |
| 73 | ...over, |
| 74 | }; |
| 75 | } |
| 76 | |
| 77 | const need = (over: Partial<CodeNeed>): CodeNeed => ({ |
| 78 | key: "pull:p1", |
| 79 | reason: "checks_failing", |
| 80 | repo, |
| 81 | ref: "#12", |
| 82 | title: "Add retries", |
| 83 | ask: "It failed in the merge queue.", |
| 84 | by: { name: "bo", agent: false }, |
| 85 | at: NOW - 2 * HOUR, |
| 86 | to: "/acme/web/pull/12", |
| 87 | facts: [{ label: "Files changed", value: "4", tone: null }], |
| 88 | ...over, |
| 89 | }); |
| 90 | |
| 91 | test("Code's needs become rows with what is at stake", () => { |
| 92 | const failing = codeRow(need({})); |
| 93 | assert.equal(failing.kind, "pull_blocked"); |
| 94 | assert.equal(failing.stake?.text, "Checks failing"); |
| 95 | assert.equal(failing.action.label, "Look"); |
| 96 | const deploy = codeRow(need({ key: "deploy:site", reason: "blocking", facts: [{ label: "Production", value: "Serving the build before", tone: "good" }] })); |
| 97 | assert.equal(deploy.kind, "deploy"); |
| 98 | assert.equal(deploy.stake?.text, "Production on an older build"); |
| 99 | assert.equal(codeRow(need({ key: "review:p2", reason: "needs_review", facts: [{ label: "Lines", value: "+10 −2", tone: null }] })).stake?.text, "+10 −2"); |
| 100 | assert.equal(codeRow(need({ key: "runner:j1", reason: "stalled", facts: [{ label: "Waiting", value: "2 h", tone: "warn" }] })).stake?.text, "Waiting 2 h"); |
| 101 | // Your pull request that meets everything: merge it, beside reviews asked of you. |
| 102 | const ready = codeRow(need({ key: "ready:p3", reason: "ready", ask: "Ready to land when you merge it." })); |
| 103 | assert.equal(ready.kind, "ready"); |
| 104 | assert.equal(ready.stake?.text, "Ready to merge"); |
| 105 | assert.equal(ready.action.label, "Merge"); |
| 106 | // One that needs you (a conflict, a request for changes) blocks it. |
| 107 | const conflict = codeRow(need({ key: "pull:p4", reason: "blocking", ask: "It conflicts with the branch it will land on." })); |
| 108 | assert.equal(conflict.kind, "pull_blocked"); |
| 109 | assert.equal(conflict.stake?.text, "Blocking"); |
| 110 | }); |
| 111 | |
| 112 | const row = (over: Partial<AttentionRow>): AttentionRow => ({ |
| 113 | key: "k", |
| 114 | kind: "chat", |
| 115 | title: "t", |
| 116 | detail: "d", |
| 117 | stake: null, |
| 118 | action: { label: "Open", to: "/x" }, |
| 119 | owner: null, |
| 120 | at: NOW, |
| 121 | from: "chat", |
| 122 | ...over, |
| 123 | }); |
| 124 | |
| 125 | test("Start here: the most pressing tier first, then what has waited longest", () => { |
| 126 | const rows = [ |
| 127 | row({ key: "chat:old", kind: "chat", at: NOW - 5 * DAY, action: { label: "Reply", to: "/acme/-/chat/general" } }), |
| 128 | row({ key: "review:new", kind: "review", at: NOW - HOUR, action: { label: "Review", to: "/acme/web/pull/2" } }), |
| 129 | row({ key: "review:old", kind: "review", at: NOW - 3 * HOUR, action: { label: "Review", to: "/acme/web/pull/1" } }), |
| 130 | row({ key: "pull:p", kind: "pull_blocked", at: NOW - 10 * 60_000, action: { label: "Look", to: "/acme/web/pull/3" } }), |
| 131 | ]; |
| 132 | const ranked = rankAttention(rows); |
| 133 | assert.deepEqual( |
| 134 | ranked.map((r) => r.key), |
| 135 | ["pull:p", "review:old", "review:new", "chat:old"], |
| 136 | ); |
| 137 | const start = startHere(ranked); |
| 138 | assert.equal(start?.row.key, "pull:p"); |
| 139 | assert.match(start?.why ?? "", /can't land/); |
| 140 | // A failed production deploy beats everything. |
| 141 | const withDeploy = rankAttention([ |
| 142 | ...rows, |
| 143 | row({ key: "deploy:site", kind: "deploy", at: NOW, action: { label: "See the build", to: "/acme/site/deployments/1" } }), |
| 144 | ]); |
| 145 | assert.equal(startHere(withDeploy)?.row.key, "deploy:site"); |
| 146 | // Ties in tier say so. |
| 147 | const two = startHere(rankAttention(rows.filter((r) => r.kind === "review"))); |
| 148 | assert.equal(two?.row.key, "review:old"); |
| 149 | assert.match(two?.why ?? "", /Of the 2 like it, it has waited longest/); |
| 150 | assert.equal(startHere([]), null); |
| 151 | }); |
| 152 | |
| 153 | function workItem(number: number, over: Partial<WorkItem> = {}): WorkItem { |
| 154 | return { |
| 155 | id: `wki_${number}`, |
| 156 | workspace: "acme", |
| 157 | number, |
| 158 | title: `Item ${number}`, |
| 159 | body: "", |
| 160 | labels: [], |
| 161 | state: "open", |
| 162 | reason: null, |
| 163 | resolutions: [], |
| 164 | author: { id: "usr_1", username: "ana" } as WorkItem["author"], |
| 165 | requestedBy: null, |
| 166 | createdAt: iso(NOW - DAY), |
| 167 | updatedAt: iso(NOW - HOUR), |
| 168 | closedAt: null, |
| 169 | pullCount: 0, |
| 170 | commentCount: 0, |
| 171 | assignees: [], |
| 172 | blockedBy: [], |
| 173 | agent: null, |
| 174 | repos: [{ repoId: "rep_web", path: "acme/web", role: "origin", legacyNumber: null, landed: false }], |
| 175 | horizon: "next", |
| 176 | appetite: null, |
| 177 | budgetUsd: null, |
| 178 | urgent: false, |
| 179 | plannedAt: null, |
| 180 | ...over, |
| 181 | }; |
| 182 | } |
| 183 | |
| 184 | test("with nothing waiting, Start here is the work item to take next, and says why", () => { |
| 185 | const next = workItem(31, { title: "Retry lost starts", appetite: "small" }); |
| 186 | const out = attention({ slug: "acme", code: true, codeNeeds: [], notifications: [], chat: [], now: [], next: { item: next, viewer: "bo" } }); |
| 187 | // It waits on nobody, so it is no row of Your move. |
| 188 | assert.deepEqual(out.rows, []); |
| 189 | assert.equal(out.start?.row.kind, "next"); |
| 190 | assert.equal(out.start?.row.title, "Retry lost starts"); |
| 191 | assert.equal(out.start?.row.detail, "#31 · acme/web"); |
| 192 | assert.deepEqual(out.start?.row.action, { label: "Open work item", to: "/acme/-/work/31" }); |
| 193 | assert.deepEqual(out.start?.row.stake, { text: "Next · S", tone: "accent" }); |
| 194 | assert.equal(out.start?.why, "Top of the plan's Next, ready and unclaimed."); |
| 195 | // Anything that ranks comes first. |
| 196 | const busy = attention({ slug: "acme", code: true, codeNeeds: [need({})], notifications: [], chat: [], now: [], next: { item: next, viewer: "bo" } }); |
| 197 | assert.equal(busy.start?.row.key, "pull:p1"); |
| 198 | // No work item to take: nothing to start with. |
| 199 | assert.equal(attention({ slug: "acme", code: true, codeNeeds: [], notifications: [], chat: [], next: null }).start, null); |
| 200 | }); |
| 201 | |
| 202 | test("why the work item to take next is the one", () => { |
| 203 | assert.equal(nextWhy(workItem(1, { horizon: "now" }), "bo"), "First in Now that nobody is on, and nothing blocks it."); |
| 204 | assert.equal(nextWhy(workItem(1, { horizon: "now", urgent: true }), "bo"), "Marked urgent in Now, and nobody is on it yet."); |
| 205 | assert.equal(nextWhy(workItem(1, { assignees: ["Bo"] }), "bo"), "Top of the plan's Next, ready and unclaimed. It is assigned to you."); |
| 206 | assert.deepEqual(nextRow(workItem(2, { horizon: "now", urgent: true })).stake, { text: "Urgent", tone: "danger" }); |
| 207 | }); |
| 208 | |
| 209 | test("your work in Now is in Your move, in Now's order, after everything else that waits on you", () => { |
| 210 | const now = [ |
| 211 | workItem(7, { horizon: "now", urgent: true, assignees: ["bo"] }), |
| 212 | workItem(3, { horizon: "now", assignees: ["ana"] }), |
| 213 | workItem(5, { horizon: "now" }), |
| 214 | workItem(9, { horizon: "now", assignees: ["BO"] }), |
| 215 | ]; |
| 216 | const claims = [ |
| 217 | { item: 5, state: "active", actor: { username: "bo" } }, |
| 218 | { item: 3, state: "released", actor: { username: "bo" } }, |
| 219 | ]; |
| 220 | const rows = nowRows(now, claims, "bo"); |
| 221 | const dm = { channel: { id: "c1", kind: "dm" as const, name: null, last_message_at: iso(NOW - DAY) }, title: "ana", muted: false, unread: 1, mentions: 0 }; |
| 222 | assert.deepEqual( |
| 223 | rows.map((r) => [r.key, r.detail, r.stake?.text]), |
| 224 | [ |
| 225 | ["item:acme#7", "#7 · acme/web · assigned to you", "Urgent"], |
| 226 | ["item:acme#5", "#5 · acme/web · claimed by you", "In Now"], |
| 227 | ["item:acme#9", "#9 · acme/web · assigned to you", "In Now"], |
| 228 | ], |
| 229 | ); |
| 230 | const out = attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [dm], now: rows }); |
| 231 | assert.deepEqual( |
| 232 | out.rows.map((r) => r.key), |
| 233 | ["chat:c1", "item:acme#7", "item:acme#5", "item:acme#9"], |
| 234 | ); |
| 235 | // Alone, the first of them is where to start. |
| 236 | const alone = attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: rows.slice(1) }); |
| 237 | assert.equal(alone.start?.row.key, "item:acme#5"); |
| 238 | assert.match(alone.start?.why ?? "", /yours in Now\. It is first of your 2 there\./); |
| 239 | // The plan not answering is said, like any other source. |
| 240 | assert.deepEqual(attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: null }).missing, ["Work"]); |
| 241 | }); |
| 242 | |
| 243 | test("work waiting in Triage over a week is a row for whoever plans, last, after their work in Now", () => { |
| 244 | assert.equal(triageRow("acme", 0), null); |
| 245 | const one = triageRow("acme", 1); |
| 246 | assert.equal(one?.title, "1 work item has waited in Triage over a week"); |
| 247 | const row = triageRow("acme", 4); |
| 248 | assert.equal(row?.title, "4 work items have waited in Triage over a week"); |
| 249 | assert.deepEqual(row?.action, { label: "Triage", to: "/acme/-/work/triage" }); |
| 250 | const mine = nowRows([workItem(7, { horizon: "now", assignees: ["bo"] })], [], "bo"); |
| 251 | const dm = { channel: { id: "c1", kind: "dm" as const, name: null, last_message_at: iso(NOW - DAY) }, title: "ana", muted: false, unread: 1, mentions: 0 }; |
| 252 | const out = attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [dm], now: mine, triageWaiting: 4 }); |
| 253 | assert.deepEqual( |
| 254 | out.rows.map((r) => r.key), |
| 255 | ["chat:c1", "item:acme#7", "triage:acme"], |
| 256 | ); |
| 257 | // Alone, it is where to start, before the work item to take next. |
| 258 | const alone = attention({ |
| 259 | slug: "acme", |
| 260 | code: false, |
| 261 | codeNeeds: null, |
| 262 | notifications: [], |
| 263 | chat: [], |
| 264 | now: [], |
| 265 | triageWaiting: 2, |
| 266 | next: { item: workItem(31), viewer: "bo" }, |
| 267 | }); |
| 268 | // The work to take next comes before the nudge; with none, the nudge leads. |
| 269 | assert.equal(alone.start?.row.key, nextRow(workItem(31)).key); |
| 270 | assert.deepEqual( |
| 271 | alone.rows.map((row) => row.key), |
| 272 | ["triage:acme"], |
| 273 | ); |
| 274 | const nudge = attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: [], triageWaiting: 2 }); |
| 275 | assert.equal(nudge.start?.row.key, "triage:acme"); |
| 276 | assert.match(nudge.start?.why ?? "", /waited over a week to be placed/); |
| 277 | // Nothing waiting that long, or a viewer who does not plan: no row. |
| 278 | assert.deepEqual(attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: [], triageWaiting: 0 }).rows, []); |
| 279 | assert.deepEqual(attention({ slug: "acme", code: false, codeNeeds: null, notifications: [], chat: [], now: [] }).rows, []); |
| 280 | }); |
| 281 | |
| 282 | test("the same order whatever order the rows came in", () => { |
| 283 | const rows = [row({ key: "b", kind: "review", at: NOW }), row({ key: "a", kind: "review", at: NOW, action: { label: "Review", to: "/y" } })]; |
| 284 | assert.deepEqual( |
| 285 | rankAttention(rows).map((r) => r.key), |
| 286 | ["a", "b"], |
| 287 | ); |
| 288 | assert.deepEqual( |
| 289 | rankAttention([...rows].reverse()).map((r) => r.key), |
| 290 | ["a", "b"], |
| 291 | ); |
| 292 | }); |
| 293 | |
| 294 | const item = (over: Partial<InboxItem>): InboxItem => ({ |
| 295 | id: "n1", |
| 296 | reason: "ci_activity", |
| 297 | severity: "error", |
| 298 | title: "Checks failed on #12", |
| 299 | body: "Add retries", |
| 300 | event: "check.failed", |
| 301 | repo: "acme/web", |
| 302 | workspace: "acme", |
| 303 | subject: "pull", |
| 304 | number: 12, |
| 305 | url: "/acme/web/pull/12", |
| 306 | actor: "g1t", |
| 307 | count: 1, |
| 308 | createdAt: iso(NOW - HOUR), |
| 309 | updatedAt: iso(NOW - HOUR), |
| 310 | readAt: null, |
| 311 | doneAt: null, |
| 312 | saved: false, |
| 313 | snoozedUntil: null, |
| 314 | ...over, |
| 315 | }); |
| 316 | |
| 317 | test("notifications: warnings and failures in this workspace; none about repositories without Code", () => { |
| 318 | assert.equal(notificationRow(item({}), "acme", true)?.kind, "notification"); |
| 319 | assert.equal(notificationRow(item({}), "acme", false), null); |
| 320 | assert.equal(notificationRow(item({ repo: null, url: "/acme/-/chat/general" }), "acme", false)?.kind, "notification"); |
| 321 | // A reason this page does not know, older ones included, reads as a plain notification. |
| 322 | assert.equal(notificationRow(item({ reason: "agent" as string as InboxItem["reason"] }), "acme", true)?.stake?.text, "Notification"); |
| 323 | assert.equal(notificationRow(item({ severity: "info" }), "acme", true), null); |
| 324 | assert.equal(notificationRow(item({ workspace: "other", repo: "other/web" }), "acme", true), null); |
| 325 | assert.equal(notificationRow(item({ readAt: iso(NOW) }), "acme", true), null); |
| 326 | }); |
| 327 | |
| 328 | test("a notification about something already listed is left out", () => { |
| 329 | const out = attention({ |
| 330 | slug: "acme", |
| 331 | code: true, |
| 332 | codeNeeds: [need({})], |
| 333 | notifications: [item({})], |
| 334 | chat: [], |
| 335 | }); |
| 336 | assert.deepEqual( |
| 337 | out.rows.map((r) => r.key), |
| 338 | ["pull:p1"], |
| 339 | ); |
| 340 | assert.deepEqual(out.missing, []); |
| 341 | }); |
| 342 | |
| 343 | test("without Code access no Code row is asked for or shown, and a silent source is named", () => { |
| 344 | const out = attention({ |
| 345 | slug: "acme", |
| 346 | code: false, |
| 347 | codeNeeds: [need({})], |
| 348 | notifications: [item({}), item({ id: "n2", repo: null, reason: "mention", severity: "warning", url: "/acme/-/chat/general" })], |
| 349 | chat: null, |
| 350 | }); |
| 351 | assert.deepEqual( |
| 352 | out.rows.map((r) => r.key), |
| 353 | ["notification:n2"], |
| 354 | ); |
| 355 | assert.equal( |
| 356 | out.rows.some((r) => r.from === "code"), |
| 357 | false, |
| 358 | ); |
| 359 | assert.deepEqual(out.missing, ["Chat"]); |
| 360 | }); |
| 361 | |
| 362 | test("chat: mentions anywhere and unread direct messages, never muted", () => { |
| 363 | const channel = { id: "c1", kind: "channel" as const, name: "general", last_message_at: iso(NOW) }; |
| 364 | assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 0 }, "acme"), null); |
| 365 | assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 2 }, "acme")?.stake?.text, "2 mentions"); |
| 366 | assert.equal(chatRow({ channel, title: "general", muted: true, unread: 4, mentions: 2 }, "acme"), null); |
| 367 | const dm = chatRow({ channel: { ...channel, kind: "dm", name: null }, title: "Bo Reed", muted: false, unread: 1, mentions: 0 }, "acme"); |
| 368 | assert.equal(dm?.action.to, "/acme/-/chat/dm/c1"); |
| 369 | }); |
| 370 | |
| 371 | // The span is NOW back to 3 days ago unless a test says otherwise. |
| 372 | const SPAN = { from: NOW - 3 * DAY, now: NOW }; |
| 373 | |
| 374 | test("a day is the reader's day", () => { |
| 375 | assert.equal(dayIn(NOW, LA), "2026-10-09"); |
| 376 | assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), LA), "2026-10-09"); |
| 377 | assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), null), "2026-10-10"); |
| 378 | assert.equal(dayIn(NOW, "Not/AZone"), "2026-10-09"); |
| 379 | }); |
| 380 | |
| 381 | test("since your last visit, in words, in the reader's time zone", () => { |
| 382 | // NOW is Friday 11:00 in Los Angeles. |
| 383 | assert.equal(sinceWords(NOW - 20_000, NOW, LA), "a moment ago"); |
| 384 | assert.equal(sinceWords(NOW - 40 * 60_000, NOW, LA), "40 minutes ago"); |
| 385 | assert.equal(sinceWords(NOW - HOUR - 60_000, NOW, LA), "an hour ago"); |
| 386 | assert.equal(sinceWords(NOW - 2 * HOUR, NOW, LA), "2 hours ago"); |
| 387 | // 07:00 Friday. |
| 388 | assert.equal(sinceWords(NOW - 4 * HOUR, NOW, LA), "this morning"); |
| 389 | // 02:00 Friday is Thursday night. |
| 390 | assert.equal(sinceWords(NOW - 9 * HOUR, NOW, LA), "last night"); |
| 391 | // 19:00 Thursday. |
| 392 | assert.equal(sinceWords(NOW - 16 * HOUR, NOW, LA), "yesterday evening"); |
| 393 | // 19:00 Tuesday. |
| 394 | assert.equal(sinceWords(NOW - 2 * DAY - 16 * HOUR, NOW, LA), "Tuesday evening"); |
| 395 | // 02:00 Wednesday is Tuesday night. |
| 396 | assert.equal(sinceWords(NOW - 2 * DAY - 9 * HOUR, NOW, LA), "Tuesday night"); |
| 397 | assert.equal(sinceWords(NOW - 9 * DAY, NOW, LA), "Sep 30"); |
| 398 | }); |
| 399 | |
| 400 | test("how long a span is", () => { |
| 401 | assert.equal(duration(30_000), "a moment"); |
| 402 | assert.equal(duration(45 * 60_000), "45 minutes"); |
| 403 | assert.equal(duration(HOUR), "1 hour"); |
| 404 | assert.equal(duration(3 * DAY + HOUR), "3 days"); |
| 405 | assert.equal(duration(15 * DAY), "2 weeks"); |
| 406 | }); |
| 407 | |
| 408 | test("the span: since your last visit, capped, or a fixed window", () => { |
| 409 | assert.equal(parseWindow("24h"), "24h"); |
| 410 | assert.equal(parseWindow("7d"), "7d"); |
| 411 | assert.equal(parseWindow("nonsense"), "last"); |
| 412 | assert.equal(parseWindow(null), "last"); |
| 413 | const away = spanFor("last", NOW, NOW - 2 * DAY - 16 * HOUR, LA); |
| 414 | assert.equal(away.from, NOW - 2 * DAY - 16 * HOUR); |
| 415 | assert.equal(away.words, "Since Tuesday evening"); |
| 416 | assert.equal(away.length, "2 days"); |
| 417 | assert.equal(away.note, null); |
| 418 | // Never visited: the last 24 hours, and it says why. |
| 419 | const first = spanFor("last", NOW, null, LA); |
| 420 | assert.equal(first.from, NOW - DAY); |
| 421 | assert.equal(first.note, "first"); |
| 422 | assert.equal(spanFor("last", NOW, undefined, LA).note, "unknown"); |
| 423 | // Away a month: 14 days at most. |
| 424 | const capped = spanFor("last", NOW, NOW - 30 * DAY, LA); |
| 425 | assert.equal(capped.from, NOW - 14 * DAY); |
| 426 | assert.equal(capped.note, "capped"); |
| 427 | assert.equal(capped.words, "In the last 14 days"); |
| 428 | // The fixed windows ignore the last visit. |
| 429 | assert.equal(spanFor("24h", NOW, NOW - 3 * DAY, LA).from, NOW - DAY); |
| 430 | assert.deepEqual([spanFor("7d", NOW, NOW - HOUR, LA).from, spanFor("7d", NOW, NOW - HOUR, LA).words], [NOW - 7 * DAY, "In the last 7 days"]); |
| 431 | }); |
| 432 | |
| 433 | // --- People ------------------------------------------------------------ |
| 434 | |
| 435 | const ANA = "user:usr_ana"; |
| 436 | const BO = "user:usr_bo"; |
| 437 | const CY = "user:usr_cy"; |
| 438 | const MARGO = "agent:agt_margo"; |
| 439 | const G1T = "user:usr_g1t_agent"; |
| 440 | const NAMES = { [ANA]: "ana", [BO]: "bo", [CY]: "cy", [MARGO]: "margo" }; |
| 441 | const REPOS = { |
| 442 | rep_web: { namespace: "acme", name: "web", defaultBranch: "main" }, |
| 443 | rep_api: { namespace: "acme", name: "api", defaultBranch: "main" }, |
| 444 | rep_docs: { namespace: "acme", name: "docs", defaultBranch: "main" }, |
| 445 | rep_cli: { namespace: "acme", name: "cli", defaultBranch: "trunk" }, |
| 446 | }; |
| 447 | |
| 448 | const count = (actor: string, n: number): ActorCount => ({ actor, count: n }); |
| 449 | const quietRepo = (repo_id: string): RepoDigest => ({ |
| 450 | repo_id, |
| 451 | pushes: { count: 0, commits: 0, branches: [], by: [], default_branch: { count: 0, commits: 0, by: [], last_at: null } }, |
| 452 | pulls: { opened: [], merged: [], closed: [] }, |
| 453 | items: { opened: [], closed: [] }, |
| 454 | reviews: [], |
| 455 | comments: [], |
| 456 | deployments: { succeeded: [], failed: [], production: 0 }, |
| 457 | releases: [], |
| 458 | packages: [], |
| 459 | }); |
| 460 | |
| 461 | /** |
| 462 | * A day on a busy workspace: 3 people pushed 41 commits to 4 projects, |
| 463 | * merged 5 pull requests and opened 2; older agent rows (g1t and Margo) |
| 464 | * pushed 6, opened 3 pull requests and reviewed 2, and are not people; |
| 465 | * 2 production deploys went out; 120 messages were sent, 15 of them |
| 466 | * Margo's; 7 docs were edited. |
| 467 | */ |
| 468 | function busyDay(): ActivityDigest { |
| 469 | return { |
| 470 | from: iso(SPAN.from), |
| 471 | until: iso(SPAN.now), |
| 472 | complete: true, |
| 473 | repos: [ |
| 474 | { |
| 475 | ...quietRepo("rep_web"), |
| 476 | pushes: { |
| 477 | count: 14, |
| 478 | commits: 33, |
| 479 | branches: ["fix-login", "main"], |
| 480 | by: [ |
| 481 | { actor: ANA, count: 8, commits: 24 }, |
| 482 | { actor: BO, count: 4, commits: 5 }, |
| 483 | { actor: G1T, count: 2, commits: 4 }, |
| 484 | ], |
| 485 | default_branch: { count: 3, commits: 9, by: [{ actor: ANA, count: 3, commits: 9 }], last_at: iso(NOW - 2 * HOUR) }, |
| 486 | }, |
| 487 | pulls: { opened: [count(ANA, 1), count(G1T, 2), count(MARGO, 1)], merged: [count(ANA, 3), count(BO, 1)], closed: [] }, |
| 488 | items: { opened: [count(BO, 2)], closed: [count(ANA, 1), count(G1T, 1)] }, |
| 489 | reviews: [count(BO, 2), count(G1T, 2)], |
| 490 | comments: [count(ANA, 4), count(MARGO, 3)], |
| 491 | deployments: { succeeded: [count(ANA, 2)], failed: [], production: 2 }, |
| 492 | releases: [{ tag: "v1.4.0", name: "Autumn", actor: ANA, at: iso(NOW - 3 * HOUR) }], |
| 493 | packages: [], |
| 494 | }, |
| 495 | { |
| 496 | ...quietRepo("rep_api"), |
| 497 | pushes: { |
| 498 | count: 5, |
| 499 | commits: 8, |
| 500 | branches: ["main"], |
| 501 | by: [{ actor: CY, count: 5, commits: 8 }], |
| 502 | default_branch: { count: 5, commits: 8, by: [{ actor: CY, count: 5, commits: 8 }], last_at: iso(NOW - 5 * HOUR) }, |
| 503 | }, |
| 504 | pulls: { opened: [count(CY, 1)], merged: [count(CY, 1)], closed: [] }, |
| 505 | packages: [{ ecosystem: "container", name: "api", version: "1.4.0", actor: G1T, at: iso(NOW - HOUR) }], |
| 506 | }, |
| 507 | { |
| 508 | ...quietRepo("rep_docs"), |
| 509 | pushes: { |
| 510 | count: 2, |
| 511 | commits: 2, |
| 512 | branches: ["typo"], |
| 513 | by: [{ actor: BO, count: 2, commits: 2 }], |
| 514 | default_branch: { count: 0, commits: 0, by: [], last_at: null }, |
| 515 | }, |
| 516 | }, |
| 517 | { |
| 518 | ...quietRepo("rep_cli"), |
| 519 | pushes: { |
| 520 | count: 3, |
| 521 | commits: 4, |
| 522 | branches: ["trunk"], |
| 523 | by: [ |
| 524 | { actor: ANA, count: 1, commits: 2 }, |
| 525 | { actor: MARGO, count: 2, commits: 2 }, |
| 526 | ], |
| 527 | default_branch: { |
| 528 | count: 3, |
| 529 | commits: 4, |
| 530 | by: [ |
| 531 | { actor: ANA, count: 1, commits: 2 }, |
| 532 | { actor: MARGO, count: 2, commits: 2 }, |
| 533 | ], |
| 534 | last_at: iso(NOW - 30 * 60_000), |
| 535 | }, |
| 536 | }, |
| 537 | }, |
| 538 | ], |
| 539 | folios: { |
| 540 | created: [count(ANA, 1)], |
| 541 | edited_count: 7, |
| 542 | edited: [ |
| 543 | { folio_id: "fol_1", kind: "doc", authors: [ANA, MARGO] }, |
| 544 | { folio_id: "fol_2", kind: "doc", authors: [ANA] }, |
| 545 | { folio_id: "fol_3", kind: "doc", authors: [BO] }, |
| 546 | { folio_id: "fol_4", kind: "doc", authors: [BO] }, |
| 547 | { folio_id: "fol_5", kind: "doc", authors: [CY] }, |
| 548 | { folio_id: "fol_6", kind: "doc", authors: [CY] }, |
| 549 | { folio_id: "fol_7", kind: "doc", authors: [MARGO] }, |
| 550 | ], |
| 551 | }, |
| 552 | }; |
| 553 | } |
| 554 | |
| 555 | const busyChat: ChatActivity = { |
| 556 | from: iso(SPAN.from), |
| 557 | until: iso(SPAN.now), |
| 558 | messages: 120, |
| 559 | channels: 5, |
| 560 | authors: [ |
| 561 | { key: ANA, messages: 60 }, |
| 562 | { key: BO, messages: 30 }, |
| 563 | { key: CY, messages: 15 }, |
| 564 | { key: MARGO, messages: 15 }, |
| 565 | ], |
| 566 | }; |
| 567 | |
| 568 | test("an actor is not a person when it is g1t or an agent of older rows; everyone else is", () => { |
| 569 | assert.equal(actorKind(MARGO), "agent"); |
| 570 | assert.equal(actorKind(G1T), "agent"); |
| 571 | assert.equal(actorKind("user:agt_01abc"), "agent", "an agent named by its own id under user: is still a robot"); |
| 572 | assert.equal(actorKind("user:wsp_01abc"), "agent", "a workspace, by its own token or g1t's upkeep, is never a person"); |
| 573 | assert.equal(actorKind("user:g1t"), "agent"); |
| 574 | assert.equal(actorKind(ANA), "person"); |
| 575 | assert.equal(actorKind(""), "person"); |
| 576 | assert.equal(actorKind("user:usr_bot", new Set(["user:usr_bot"])), "agent"); |
| 577 | }); |
| 578 | |
| 579 | test("the digest: what was done, by kind with counts, and who, agents marked", () => { |
| 580 | const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true }); |
| 581 | assert.deepEqual( |
| 582 | digest.people.lines.map((line) => [line.key, line.text, line.to]), |
| 583 | [ |
| 584 | ["pushes", "47 commits to 4 projects", "/acme/-/repositories"], |
| 585 | ["pulls", "5 opened, 5 merged, 4 reviewed", "/acme/-/repositories"], |
| 586 | ["items", "2 opened, 2 closed, 7 comments", "/acme/-/work/all?repo=web"], |
| 587 | ["shipped", "2 deploys, 1 release, 1 package version", "/acme/web/deployments"], |
| 588 | ["docs", "1 doc created, 7 docs edited", "/acme/-/artifacts"], |
| 589 | ["chat", "120 messages in 5 conversations", "/acme/-/chat"], |
| 590 | ], |
| 591 | ); |
| 592 | // Rows: most active first, people and agents, each marked; nobody is "someone". |
| 593 | assert.deepEqual( |
| 594 | digest.people.actors.map((actor) => [actor.name, actor.kind]), |
| 595 | digest.people.actors.map((actor) => [actor.name, actor.name === "margo" || actor.name === "g1t" ? "agent" : "person"]), |
| 596 | ); |
| 597 | assert.deepEqual(new Set(digest.people.actors.map((actor) => actor.name)), new Set(["ana", "bo", "cy", "margo", "g1t"])); |
| 598 | assert.equal(digest.people.actors[0]!.name, "ana"); |
| 599 | // Each row's parts add up to its count, and the kinds' counts to every row's. |
| 600 | for (const actor of digest.people.actors) { |
| 601 | assert.equal( |
| 602 | Object.values(actor.parts).reduce((a, b) => a + b, 0), |
| 603 | actor.count, |
| 604 | actor.name, |
| 605 | ); |
| 606 | } |
| 607 | // The summary still counts people only. |
| 608 | assert.deepEqual(digest.summary, { people: 3, commits: 41, merged: 5, said: 105, anything: true, peopleRead: true }); |
| 609 | assert.deepEqual(digest.missing, []); |
| 610 | assert.equal(digest.partial, false); |
| 611 | }); |
| 612 | |
| 613 | test("g1t is one row, named, whichever key it used: its account, a workspace, or chat's system:g1t", () => { |
| 614 | const chat: ChatActivity = { |
| 615 | ...busyChat, |
| 616 | messages: 5, |
| 617 | channels: 1, |
| 618 | authors: [ |
| 619 | { key: "system:g1t", messages: 3 }, |
| 620 | { key: ANA, messages: 2 }, |
| 621 | ], |
| 622 | }; |
| 623 | const activity = { ...busyDay(), folios: null, repos: [{ ...quietRepo("rep_web"), comments: [count(G1T, 2), count("user:wsp_acme", 1)] }] }; |
| 624 | const digest = digestOf({ slug: "acme", repos: REPOS, activity, chat, names: { [ANA]: "ana", "user:wsp_acme": "g1t", "system:g1t": "g1t" }, code: true }); |
| 625 | assert.deepEqual( |
| 626 | digest.people.actors.map((actor) => [actor.name, actor.kind, actor.count, actor.parts]), |
| 627 | [ |
| 628 | ["g1t", "agent", 6, { items: 3, chat: 3 }], |
| 629 | ["ana", "person", 2, { chat: 2 }], |
| 630 | ], |
| 631 | ); |
| 632 | assert.equal(digest.summary.people, 1); |
| 633 | // A person whose name could not be read is an unknown account, never "someone". |
| 634 | const unread = digestOf({ |
| 635 | slug: "acme", |
| 636 | repos: REPOS, |
| 637 | activity: null, |
| 638 | chat: { ...chat, authors: [{ key: "user:usr_gone", messages: 1 }] }, |
| 639 | names: {}, |
| 640 | code: true, |
| 641 | }); |
| 642 | assert.deepEqual( |
| 643 | unread.people.actors.map((actor) => actor.name), |
| 644 | ["unknown account"], |
| 645 | ); |
| 646 | }); |
| 647 | |
| 648 | test("the digest says which part didn't answer, and counts what could be read", () => { |
| 649 | const noChat = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: null, names: NAMES, code: true }); |
| 650 | assert.deepEqual(noChat.missing, ["Chat"]); |
| 651 | assert.equal( |
| 652 | noChat.people.lines.some((line) => line.key === "chat"), |
| 653 | false, |
| 654 | ); |
| 655 | const noCode = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: busyChat, names: NAMES, code: true }); |
| 656 | assert.deepEqual(noCode.missing, ["Code"]); |
| 657 | assert.deepEqual( |
| 658 | noCode.people.lines.map((line) => line.text), |
| 659 | ["120 messages in 5 conversations"], |
| 660 | ); |
| 661 | assert.equal(noCode.summary.peopleRead, true); |
| 662 | // Without Code access, events are read for artifacts only: no repositories, nothing missing. |
| 663 | const member = digestOf({ slug: "acme", repos: {}, activity: { ...busyDay(), repos: [] }, chat: busyChat, names: NAMES, code: false }); |
| 664 | assert.deepEqual(member.missing, []); |
| 665 | assert.deepEqual( |
| 666 | member.people.lines.map((line) => line.key), |
| 667 | ["docs", "chat"], |
| 668 | ); |
| 669 | // A digest cut short says so. |
| 670 | const cut = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), complete: false }, chat: busyChat, names: NAMES, code: true }); |
| 671 | assert.equal(cut.partial, true); |
| 672 | // Nobody named: an import's pushes are still commits. |
| 673 | const imported = digestOf({ |
| 674 | slug: "acme", |
| 675 | repos: REPOS, |
| 676 | activity: { |
| 677 | ...busyDay(), |
| 678 | folios: null, |
| 679 | repos: [ |
| 680 | { |
| 681 | ...quietRepo("rep_web"), |
| 682 | pushes: { |
| 683 | count: 2, |
| 684 | commits: 7, |
| 685 | branches: ["main"], |
| 686 | by: [{ actor: "", count: 2, commits: 7 }], |
| 687 | default_branch: { count: 2, commits: 7, by: [{ actor: "", count: 2, commits: 7 }], last_at: iso(NOW - HOUR) }, |
| 688 | }, |
| 689 | }, |
| 690 | ], |
| 691 | }, |
| 692 | chat: null, |
| 693 | names: {}, |
| 694 | code: true, |
| 695 | }); |
| 696 | assert.deepEqual( |
| 697 | imported.people.lines.map((line) => [line.text, line.to]), |
| 698 | [["7 commits to 1 project", "/acme/web/activity"]], |
| 699 | ); |
| 700 | assert.equal(imported.summary.people, 0); |
| 701 | }); |
| 702 | |
| 703 | test("the sentence: people first, then what landed; honest when quiet or unread", () => { |
| 704 | const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true }); |
| 705 | assert.equal( |
| 706 | spanSentence({ words: "Since Tuesday evening" }, digest, 12), |
| 707 | "Since Tuesday evening, 3 people pushed 41 commits and merged 5 changes and 12 changes landed.", |
| 708 | ); |
| 709 | const quiet = digestOf({ |
| 710 | slug: "acme", |
| 711 | repos: REPOS, |
| 712 | activity: { ...busyDay(), repos: [], folios: { created: [], edited: [], edited_count: 0 } }, |
| 713 | chat: { ...busyChat, messages: 0, channels: 0, authors: [] }, |
| 714 | names: {}, |
| 715 | code: true, |
| 716 | }); |
| 717 | assert.equal(spanSentence({ words: "Since Tuesday evening" }, quiet, 0), "Quiet since Tuesday evening: nothing pushed, merged, deployed or said."); |
| 718 | assert.equal(spanSentence({ words: "In the last 24 hours" }, quiet, 0), "Quiet in the last 24 hours: nothing pushed, merged, deployed or said."); |
| 719 | // Only merges. |
| 720 | const mergesOnly = digestOf({ |
| 721 | slug: "acme", |
| 722 | repos: REPOS, |
| 723 | activity: { ...busyDay(), repos: [], folios: null }, |
| 724 | chat: { ...busyChat, authors: [], messages: 0, channels: 0 }, |
| 725 | names: {}, |
| 726 | code: true, |
| 727 | }); |
| 728 | assert.equal(spanSentence({ words: "Since this morning" }, mergesOnly, 5), "Since this morning, 5 changes landed."); |
| 729 | // People only talked. |
| 730 | const talked = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: busyChat, names: NAMES, code: true }); |
| 731 | assert.equal(spanSentence({ words: "Since an hour ago" }, talked, 0), "Since an hour ago, 3 people sent 105 messages."); |
| 732 | // Neither events nor chat answered. |
| 733 | const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true }); |
| 734 | assert.equal(spanSentence({ words: "Since this morning" }, unread, 5), "Since this morning, 5 changes landed; what people did couldn't be read."); |
| 735 | const nothingRead = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, code: true }); |
| 736 | assert.equal(spanSentence({ words: "Since this morning" }, nothingRead, 0), "Since this morning, what people did couldn't be read."); |
| 737 | assert.equal(spanSentence({ words: "Since this morning" }, null, null), "Since this morning, what happened couldn't be read."); |
| 738 | assert.equal(waitingSentence(0), "Nothing needs you right now."); |
| 739 | assert.equal(waitingSentence(1), "1 thing needs you."); |
| 740 | assert.equal(waitingSentence(8), "8 things need you."); |
| 741 | }); |
| 742 | |
| 743 | test("what landed: every merge in the span, newest first, g1t's older ones as @g1t", () => { |
| 744 | const merged = landedIn( |
| 745 | [ |
| 746 | pull({ number: 1, mergedAt: iso(NOW - 2 * DAY), mergedBy: "bo" }), |
| 747 | pull({ number: 2, author: { username: "bo", kind: "user" }, mergedAt: iso(NOW - HOUR) }), |
| 748 | pull({ number: 3, mergedAt: iso(NOW - 5 * DAY) }), |
| 749 | pull({ number: 4, status: "open", mergedAt: null }), |
| 750 | ], |
| 751 | SPAN, |
| 752 | ); |
| 753 | assert.deepEqual( |
| 754 | merged.map((change) => [change.kind, change.title, change.detail]), |
| 755 | [ |
| 756 | ["merge", "Fix the flaky test", "web#2 · by bo"], |
| 757 | ["merge", "Fix the flaky test", "web#1 · by @g1t, merged by bo"], |
| 758 | ], |
| 759 | ); |
| 760 | }); |
| 761 | |
| 762 | test("what landed, besides merges: direct pushes, releases, packages and production builds, newest first", () => { |
| 763 | const fromActivity = landedFromActivity(busyDay(), REPOS, NAMES, "acme"); |
| 764 | assert.deepEqual( |
| 765 | fromActivity.map((change) => [change.kind, change.title, change.detail, change.to]), |
| 766 | [ |
| 767 | ["push", "9 commits pushed to main", "web · 3 pushes by ana", "/acme/web/activity"], |
| 768 | ["release", "v1.4.0 · Autumn", "web · released by ana", "/acme/web/releases/tag/v1.4.0"], |
| 769 | ["push", "8 commits pushed to main", "api · 5 pushes by cy", "/acme/api/activity"], |
| 770 | ["package", "api 1.4.0", "container package · published by @g1t", "/acme/-/packages"], |
| 771 | ["push", "4 commits pushed to trunk", "cli · 3 pushes by ana and @margo", "/acme/cli/activity"], |
| 772 | ], |
| 773 | ); |
| 774 | const builds = landedDeploys([ |
| 775 | { |
| 776 | key: "deploy:site:d1", |
| 777 | project: "site", |
| 778 | kind: "production", |
| 779 | outcome: "live", |
| 780 | commit: "abcdef1", |
| 781 | branch: null, |
| 782 | at: NOW - HOUR, |
| 783 | to: "/acme/site/deployments/d1", |
| 784 | by: "@g1t", |
| 785 | }, |
| 786 | { |
| 787 | key: "deploy:site:d2", |
| 788 | project: "site", |
| 789 | kind: "preview", |
| 790 | outcome: "live", |
| 791 | commit: "abcdef2", |
| 792 | branch: "fix", |
| 793 | at: NOW - HOUR, |
| 794 | to: "/acme/site/deployments/d2", |
| 795 | by: "bo", |
| 796 | }, |
| 797 | { |
| 798 | key: "deploy:site:d3", |
| 799 | project: "site", |
| 800 | kind: "production", |
| 801 | outcome: "failed", |
| 802 | commit: "abcdef3", |
| 803 | branch: null, |
| 804 | at: NOW - HOUR, |
| 805 | to: "/acme/site/deployments/d3", |
| 806 | by: "bo", |
| 807 | }, |
| 808 | ]); |
| 809 | assert.deepEqual( |
| 810 | builds.map((change) => [change.kind, change.title, change.failed ?? false]), |
| 811 | [ |
| 812 | ["deploy", "Production of site went live", false], |
| 813 | ["deploy", "Production of site failed to deploy", true], |
| 814 | ], |
| 815 | ); |
| 816 | const all = landed([fromActivity, builds.slice(0, 1)]); |
| 817 | assert.deepEqual( |
| 818 | all.map((change) => change.key), |
| 819 | ["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"], |
| 820 | ); |
| 821 | }); |
| 822 | |
| 823 | test("a production build is its own row only where no merge or push on the list says it", () => { |
| 824 | const row = (key: string, commit: string): DeployRow => ({ |
| 825 | key, |
| 826 | project: "site", |
| 827 | kind: "production", |
| 828 | outcome: "live", |
| 829 | commit, |
| 830 | branch: null, |
| 831 | at: NOW - HOUR, |
| 832 | to: `/acme/site/deployments/${key}`, |
| 833 | by: "bo", |
| 834 | }); |
| 835 | const merge: LandedChange = { |
| 836 | key: "merge:1", |
| 837 | kind: "merge", |
| 838 | title: "Fix", |
| 839 | detail: "", |
| 840 | to: "/acme/site/pull/1", |
| 841 | at: NOW - 2 * HOUR, |
| 842 | sha: "aaaaaaa111", |
| 843 | repoId: "r", |
| 844 | }; |
| 845 | const push: LandedChange = { |
| 846 | key: "push:1", |
| 847 | kind: "push", |
| 848 | title: "Push", |
| 849 | detail: "", |
| 850 | to: "/acme/site/activity", |
| 851 | at: NOW - 2 * HOUR, |
| 852 | sha: "bbbbbbb222", |
| 853 | repoId: "r", |
| 854 | shipped: { state: "live", at: NOW - HOUR, to: "/acme/site/deployments/d2" }, |
| 855 | }; |
| 856 | const builds = landedDeploys([row("d1", "aaaaaaa"), row("d2", "ccccccc"), row("d3", "ddddddd")], [merge, push]); |
| 857 | assert.deepEqual( |
| 858 | builds.map((change) => change.key), |
| 859 | ["landed:d3"], |
| 860 | ); |
| 861 | }); |
| 862 | |
| 863 | const deploy = (over: Partial<Deployment>): Deployment => ({ |
| 864 | id: "d1", |
| 865 | kind: "production", |
| 866 | branch: null, |
| 867 | number: null, |
| 868 | commit: "abcdef1234", |
| 869 | status: "ready", |
| 870 | url: "https://site.g1t.page", |
| 871 | error: null, |
| 872 | warnings: [], |
| 873 | buildSeconds: 40, |
| 874 | createdBy: "bo", |
| 875 | createdAt: iso(NOW - 2 * HOUR), |
| 876 | finishedAt: iso(NOW - HOUR), |
| 877 | ...over, |
| 878 | }); |
| 879 | |
| 880 | test("builds that finished in the span: live or failed, never skipped", () => { |
| 881 | const rows = deploysIn( |
| 882 | [ |
| 883 | { |
| 884 | slug: "site", |
| 885 | deployments: [ |
| 886 | deploy({}), |
| 887 | deploy({ id: "d2", status: "replaced", finishedAt: iso(NOW - DAY) }), |
| 888 | deploy({ id: "d3", status: "failed", finishedAt: iso(NOW - 2 * DAY) }), |
| 889 | deploy({ id: "d4", status: "skipped", finishedAt: iso(NOW - HOUR) }), |
| 890 | deploy({ id: "d5", status: "building", finishedAt: null }), |
| 891 | deploy({ id: "d6", finishedAt: iso(NOW - 5 * DAY) }), |
| 892 | ], |
| 893 | }, |
| 894 | ], |
| 895 | "acme", |
| 896 | SPAN, |
| 897 | { "user:bo": "bo" }, |
| 898 | ); |
| 899 | assert.deepEqual( |
| 900 | rows.map((row) => [row.key, row.outcome]), |
| 901 | [ |
| 902 | ["deploy:site:d1", "live"], |
| 903 | ["deploy:site:d2", "live"], |
| 904 | ["deploy:site:d3", "failed"], |
| 905 | ], |
| 906 | ); |
| 907 | assert.equal(rows[0]!.to, "/acme/site/deployments/d1"); |
| 908 | assert.equal(rows[0]!.commit, "abcdef1"); |
| 909 | // The maker is named, never shown as an id; an older agent's by its handle; an id identity doesn't know as someone. |
| 910 | assert.equal(rows[0]!.by, "bo"); |
| 911 | const unknown = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "usr_zzz" })] }], "acme", SPAN); |
| 912 | assert.equal(unknown[0]!.by, "someone"); |
| 913 | const robot = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "agt_01x" })] }], "acme", SPAN, { "user:agt_01x": "otto" }); |
| 914 | assert.equal(robot[0]!.by, "@otto"); |
| 915 | }); |
| 916 | |
| 917 | test("running now: workflow runs and builds going", () => { |
| 918 | const builds = runningDeploys( |
| 919 | [ |
| 920 | { slug: "site", enabled: true, production: null, previews: 0, latest: deploy({ status: "building", finishedAt: null }) }, |
| 921 | { slug: "docs", enabled: true, production: null, previews: 0, latest: deploy({}) }, |
| 922 | ], |
| 923 | "acme", |
| 924 | ); |
| 925 | assert.deepEqual( |
| 926 | builds.map((row) => row.status), |
| 927 | ["Building"], |
| 928 | ); |
| 929 | const run = (over: Partial<WorkflowRun>): WorkflowRun => ({ |
| 930 | id: "run_1", |
| 931 | workflowId: "wf_1", |
| 932 | path: ".g1t/workflows/ci.yml", |
| 933 | name: "CI", |
| 934 | title: "Fix the flaky test", |
| 935 | number: 12, |
| 936 | attempt: 1, |
| 937 | event: "push", |
| 938 | ref: "refs/heads/main", |
| 939 | sha: "abcdef1", |
| 940 | pull: null, |
| 941 | status: "in_progress", |
| 942 | conclusion: null, |
| 943 | error: null, |
| 944 | actor: "ana", |
| 945 | createdAt: iso(NOW - 10 * 60_000), |
| 946 | startedAt: iso(NOW - 9 * 60_000), |
| 947 | finishedAt: null, |
| 948 | ...over, |
| 949 | }); |
| 950 | const workflows = runningWorkflows([ |
| 951 | { |
| 952 | repo, |
| 953 | runs: [ |
| 954 | run({}), |
| 955 | run({ id: "run_2", status: "queued", startedAt: null, title: "" }), |
| 956 | run({ id: "run_3", status: "completed", conclusion: "success" }), |
| 957 | run({ id: "run_4", status: "action_required" }), |
| 958 | ], |
| 959 | }, |
| 960 | ]); |
| 961 | assert.deepEqual( |
| 962 | workflows.map((row) => [row.key, row.title, row.detail, row.status, row.to]), |
| 963 | [ |
| 964 | ["workflow:acme/web:run_1", "CI: Fix the flaky test", "acme/web main · by ana", "Running", "/acme/web/actions/runs/run_1"], |
| 965 | ["workflow:acme/web:run_2", "CI", "acme/web main · by ana", "Queued", "/acme/web/actions/runs/run_2"], |
| 966 | ["workflow:acme/web:run_4", "CI: Fix the flaky test", "acme/web main · by ana", "Needs approval", "/acme/web/actions/runs/run_4"], |
| 967 | ], |
| 968 | ); |
| 969 | // Longest-going first within a kind: workflow runs, then builds. |
| 970 | assert.deepEqual( |
| 971 | running([builds, workflows.slice(0, 1)]).map((row) => row.key), |
| 972 | ["workflow:acme/web:run_1", "deploy:site:d1"], |
| 973 | ); |
| 974 | }); |
| 975 | |
| 976 | test("the span's months: one, or the ones it crosses", () => { |
| 977 | assert.deepEqual(spanMonths(SPAN), ["2026-10"]); |
| 978 | assert.deepEqual(spanMonths({ from: Date.parse("2026-09-28T00:00:00Z"), now: NOW }), ["2026-09", "2026-10"]); |
| 979 | assert.deepEqual(spanMonths({ from: Date.parse("2025-12-30T00:00:00Z"), now: Date.parse("2026-01-02T00:00:00Z") }), ["2025-12", "2026-01"]); |
| 980 | }); |
| 981 | |
| 982 | test("spend in the span: the UTC days it covers across months, at price, without money in", () => { |
| 983 | const september = { |
| 984 | groups: [ |
| 985 | { key: "2026-09-29", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 2, chargedMicros: 1_000_000, costMicros: 0 }] }, |
| 986 | { key: "2026-09-20", label: "", chargedMicros: 0, lines: [{ kind: "Workflow minutes", count: 9, chargedMicros: 9_000_000, costMicros: 0 }] }, |
| 987 | ], |
| 988 | totals: { chargedMicros: 10_000_000, paidMicros: 0, costMicros: 0, entries: 2 }, |
| 989 | }; |
| 990 | const october = { |
| 991 | groups: [ |
| 992 | { |
| 993 | key: "2026-10-09", |
| 994 | label: "2026-10-09", |
| 995 | chargedMicros: 0, |
| 996 | lines: [ |
| 997 | { kind: "Workflow minutes", count: 12, chargedMicros: 3_000_000, costMicros: 2_500_000, priceMicros: 4_000_000 }, |
| 998 | { kind: "Sandbox time", count: 12, chargedMicros: 600_000, costMicros: 500_000 }, |
| 999 | { kind: "Payments", count: 1, chargedMicros: -50_000_000, costMicros: 0, priceMicros: 0 }, |
| 1000 | ], |
| 1001 | }, |
| 1002 | ], |
| 1003 | totals: { chargedMicros: 12_600_000, paidMicros: 50_000_000, costMicros: 0, entries: 4, priceMicros: 13_600_000 }, |
| 1004 | }; |
| 1005 | const spend = spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, 13_609_000); |
| 1006 | assert.deepEqual([spend.from, spend.to], ["2026-09-29", "2026-10-09"]); |
| 1007 | assert.equal(spend.totalMicros, 5_600_000); |
| 1008 | assert.deepEqual( |
| 1009 | spend.lines.map((l) => [l.kind, l.micros, l.count]), |
| 1010 | [ |
| 1011 | ["Workflow minutes", 5_000_000, 14], |
| 1012 | ["Sandbox time", 600_000, 12], |
| 1013 | ], |
| 1014 | ); |
| 1015 | // The month so far is billing's usage report, the top bar's figure, not the statement: it carries what is not yet closed. |
| 1016 | assert.equal(spend.monthMicros, 13_609_000); |
| 1017 | assert.equal(spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, null).monthMicros, null); |
| 1018 | }); |
| 1019 | |
| 1020 | // --- What landed: shipped, made in a session, day by day ----------------------- |
| 1021 | |
| 1022 | const merge = (over: Partial<LandedChange> = {}): LandedChange => ({ |
| 1023 | key: "landed:acme/web#12", |
| 1024 | kind: "merge", |
| 1025 | title: "Add retries", |
| 1026 | detail: "web#12 · by ana", |
| 1027 | to: "/acme/web/pull/12", |
| 1028 | at: NOW - 2 * HOUR, |
| 1029 | sha: "m12", |
| 1030 | repoId: "rep_web", |
| 1031 | ...over, |
| 1032 | }); |
| 1033 | |
| 1034 | test("each merge and push says whether it is live, from what deployments recorded", () => { |
| 1035 | const push = merge({ key: "push:rep_web:evt_1", kind: "push", sha: "p1", at: NOW - HOUR }); |
| 1036 | const release = merge({ key: "release:rep_web:v1", kind: "release", sha: null }); |
| 1037 | assert.deepEqual(shippedAsks([merge(), push, release]), [ |
| 1038 | { |
| 1039 | repo_id: "rep_web", |
| 1040 | commits: [ |
| 1041 | { sha: "m12", landed_at: iso(NOW - 2 * HOUR) }, |
| 1042 | { sha: "p1", landed_at: iso(NOW - HOUR) }, |
| 1043 | ], |
| 1044 | }, |
| 1045 | ]); |
| 1046 | const answers = [ |
| 1047 | { |
| 1048 | repo_id: "rep_web", |
| 1049 | deploys: true, |
| 1050 | commits: [ |
| 1051 | { sha: "m12", state: "live" as const, deployed_at: iso(NOW - 2 * HOUR + 9 * 60_000), url: "/acme/web/actions/runs/run_1", environment: "production" }, |
| 1052 | { sha: "p1", state: "waiting" as const, deployed_at: null, url: null, environment: null }, |
| 1053 | ], |
| 1054 | }, |
| 1055 | ]; |
| 1056 | const [shippedMerge, shippedPush, untouched] = withShipped([merge(), push, release], answers); |
| 1057 | assert.deepEqual(shippedMerge!.shipped, { state: "live", at: NOW - 2 * HOUR + 9 * 60_000, to: "/acme/web/actions/runs/run_1" }); |
| 1058 | assert.equal(shippedLabel(shippedMerge!.shipped!, shippedMerge!), "Live 9 min after merge"); |
| 1059 | assert.equal(shippedLabel(shippedPush!.shipped!, shippedPush!), "Waiting to deploy"); |
| 1060 | assert.equal(shippedLabel({ state: "failed", at: null, to: "/x" }, push), "Deploy failed"); |
| 1061 | assert.equal(shippedLabel({ state: "deploying", at: null, to: null }, push), "Deploying"); |
| 1062 | assert.equal(shippedLabel({ state: "live", at: push.at + 3 * HOUR, to: null }, push), "Live 3 h after push"); |
| 1063 | assert.equal(untouched!.shipped, undefined); |
| 1064 | // A project that does not deploy says nothing; deployments not answering leaves rows as they were. |
| 1065 | assert.equal(withShipped([merge()], [{ repo_id: "rep_web", deploys: false, commits: [] }])[0]!.shipped, undefined); |
| 1066 | assert.deepEqual(withShipped([merge()], null), [merge()]); |
| 1067 | }); |
| 1068 | |
| 1069 | test("a change made in a harness session carries the agent mark, linked to the session", () => { |
| 1070 | const [marked, plain] = withSessions( |
| 1071 | [merge(), merge({ key: "landed:acme/web#13", sha: "m13" })], |
| 1072 | [ |
| 1073 | { |
| 1074 | repo: { namespace: "acme", name: "web" }, |
| 1075 | pulls: [ |
| 1076 | { |
| 1077 | number: 12, |
| 1078 | sessions: [ |
| 1079 | { id: "ses_1", harness: "claude_code" }, |
| 1080 | { id: "ses_2", harness: "codex" }, |
| 1081 | ], |
| 1082 | }, |
| 1083 | ], |
| 1084 | }, |
| 1085 | ], |
| 1086 | ); |
| 1087 | assert.deepEqual(marked!.agent, { harness: "claude_code", to: "/acme/-/work/sessions/ses_1" }); |
| 1088 | assert.equal(agentLabel(marked!.agent!), "Made in a Claude Code session"); |
| 1089 | assert.equal(plain!.agent, undefined); |
| 1090 | assert.equal(agentLabel({ harness: null, to: null }), "Made in a session"); |
| 1091 | assert.deepEqual(sessionTrailers("Fix the bell\n\nG1t-Session: ses_01ABC\ng1t-session: ses_01abc\nG1t-Session: ses_02x"), ["ses_01abc", "ses_02x"]); |
| 1092 | assert.deepEqual(sessionTrailers("Mentions G1t-Session: ses_1 in prose"), []); |
| 1093 | }); |
| 1094 | |
| 1095 | test("each person's push to a default branch is a row: its commit, who pushed, and its session", () => { |
| 1096 | const repo = { namespace: "acme", name: "web", defaultBranch: "main" }; |
| 1097 | const history = { |
| 1098 | rep_web: [ |
| 1099 | { hash: "c3", parents: ["c2"], message: "Fix the bell's count\n\nG1t-Session: ses_9" }, |
| 1100 | { hash: "c2", parents: ["c1"], message: "Tidy" }, |
| 1101 | { hash: "c1", parents: ["c0"], message: "Start" }, |
| 1102 | { hash: "c0", parents: [], message: "Root" }, |
| 1103 | ], |
| 1104 | }; |
| 1105 | const push = (id: string, after: string, before: string, at: number, actor = "usr_ana") => ({ |
| 1106 | repoId: "rep_web", |
| 1107 | repo, |
| 1108 | push: { id, time: iso(at), actor, data: { after, before } }, |
| 1109 | by: "ana", |
| 1110 | }); |
| 1111 | const span = { from: NOW - DAY, now: NOW }; |
| 1112 | const rows = pushRows( |
| 1113 | [push("evt_2", "c3", "c2", NOW - HOUR), push("evt_1", "c2", "c0", NOW - 2 * HOUR), push("evt_0", "c1", "c0", NOW - 2 * DAY)], |
| 1114 | history, |
| 1115 | span, |
| 1116 | ); |
| 1117 | assert.deepEqual( |
| 1118 | rows.map((row) => [row.title, row.detail, row.sha, row.to, row.agent?.to ?? null]), |
| 1119 | [ |
| 1120 | ["Fix the bell's count", "web · pushed to main · by ana", "c3", "/acme/web/commit/c3", "/acme/-/work/sessions/ses_9"], |
| 1121 | ["2 commits pushed to main", "web · Tidy · by ana", "c2", "/acme/web/activity", null], |
| 1122 | ], |
| 1123 | ); |
| 1124 | // g1t's own pushes (a pull request landing) are listed as the merge instead. |
| 1125 | assert.deepEqual(pushRows([push("evt_3", "c3", "c2", NOW - HOUR, "usr_g1t_agent")], history, span), []); |
| 1126 | }); |
| 1127 | |
| 1128 | test("what landed over the week reads day by day, newest first", () => { |
| 1129 | const tz = "America/Los_Angeles"; |
| 1130 | const days = landedByDay([merge({ at: NOW - DAY }), merge({ key: "b", at: NOW - HOUR }), merge({ key: "c", at: NOW - 3 * DAY })], NOW, tz); |
| 1131 | assert.deepEqual( |
| 1132 | days.map((day) => [day.label, day.rows.length]), |
| 1133 | [ |
| 1134 | ["Today", 1], |
| 1135 | ["Yesterday", 1], |
| 1136 | ["Tue, Oct 6", 1], |
| 1137 | ], |
| 1138 | ); |
| 1139 | }); |
| 1140 | |
| 1141 | test("while you were away: what settled by itself, what shipped, and what still needs you", () => { |
| 1142 | const span = { from: NOW - DAY, now: NOW }; |
| 1143 | const settled = settledIn( |
| 1144 | [ |
| 1145 | item({ id: "n1", doneAt: iso(NOW - HOUR), settledReason: "CI passed in run 330" }), |
| 1146 | item({ id: "n2", doneAt: iso(NOW - 2 * HOUR), settledReason: "Merged", settledBy: "ana" }), |
| 1147 | // Marked done by hand: yours, not g1t's. |
| 1148 | item({ id: "n3", doneAt: iso(NOW - HOUR) }), |
| 1149 | // Before the span, or another workspace's. |
| 1150 | item({ id: "n4", doneAt: iso(NOW - 2 * DAY), settledReason: "Merged" }), |
| 1151 | item({ id: "n5", doneAt: iso(NOW - HOUR), settledReason: "Merged", workspace: "other", repo: "other/x" }), |
| 1152 | ], |
| 1153 | "acme", |
| 1154 | true, |
| 1155 | span, |
| 1156 | ); |
| 1157 | assert.deepEqual( |
| 1158 | settled.map((row) => [row.key, row.why, row.where]), |
| 1159 | [ |
| 1160 | ["settled:n1", "CI passed in run 330", "acme/web#12"], |
| 1161 | ["settled:n2", "Merged by @ana", "acme/web#12"], |
| 1162 | ], |
| 1163 | ); |
| 1164 | assert.equal( |
| 1165 | settledIn([item({ doneAt: iso(NOW - HOUR), settledReason: "Merged" })], "acme", false, span).length, |
| 1166 | 0, |
| 1167 | "nothing about repositories without Code", |
| 1168 | ); |
| 1169 | const away = awayOf({ |
| 1170 | settled, |
| 1171 | landed: [ |
| 1172 | merge({ shipped: { state: "live", at: NOW, to: null } }), |
| 1173 | merge({ key: "x", shipped: { state: "waiting", at: null, to: null } }), |
| 1174 | merge({ key: "y" }), |
| 1175 | ], |
| 1176 | needs: 3, |
| 1177 | }); |
| 1178 | assert.deepEqual({ ...away, settled: away.settled.length }, { settled: 2, shipped: 1, waiting: 1, deploying: 0, failed: 0, needs: 3 }); |
| 1179 | }); |
| 1180 | |
| 1181 | test("Summarize asks about the span the page shows, as a person would", () => { |
| 1182 | const now = Date.UTC(2026, 9, 14, 22, 0); |
| 1183 | assert.equal( |
| 1184 | summarizeQuestion({ key: "last", note: null, words: "Since this afternoon", from: Date.UTC(2026, 9, 14, 18, 0), now }, "UTC"), |
| 1185 | "Summarize what changed since I was last here (6:00 PM) in plain English.", |
| 1186 | ); |
| 1187 | assert.equal( |
| 1188 | summarizeQuestion({ key: "24h", note: null, words: "In the last 24 hours", from: now - DAY, now }, "UTC"), |
| 1189 | "Summarize what changed in the last 24 hours in plain English.", |
| 1190 | ); |
| 1191 | // A first visit shows the last 24 hours, so it is not asked as "since I was last here". |
| 1192 | assert.doesNotMatch(summarizeQuestion({ key: "last", note: "first", words: "In the last 24 hours", from: now - DAY, now }, "UTC"), /last here/); |
| 1193 | }); |
| 1194 | |
| 1195 | test("Explain asks about the one change, by what it is", () => { |
| 1196 | assert.equal( |
| 1197 | explainQuestion({ kind: "merge", title: "Faster pushes", to: "/acme/web/pull/12" }), |
| 1198 | "Explain pull request #12 in acme/web in plain English: what changed, why, and is it live?", |
| 1199 | ); |
| 1200 | assert.equal( |
| 1201 | explainQuestion({ kind: "push", title: "Fix login", to: "/acme/web/commit/abc1234def" }), |
| 1202 | "Explain commit abc1234 in acme/web in plain English: what changed, why, and is it live?", |
| 1203 | ); |
| 1204 | assert.match(explainQuestion({ kind: "deploy", title: "web", to: "/acme/web/deployments/1" }), /went out in this deployment/); |
| 1205 | assert.match(explainQuestion({ kind: "release", title: "v1.2.0", to: "/acme/web/releases/v1.2.0" }), /What's new in v1\.2\.0/); |
| 1206 | }); |