g1t/apps/web/app/lib/mission-control.test.ts
| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { |
| 5 | type Merged, |
| 6 | change, |
| 7 | dayKey, |
| 8 | isTestFile, |
| 9 | landedByAgents, |
| 10 | landedToday, |
| 11 | parseSort, |
| 12 | parseTab, |
| 13 | pullFacts, |
| 14 | reachesBack, |
| 15 | reasonFor, |
| 16 | signedPercent, |
| 17 | sortRows, |
| 18 | stallReason, |
| 19 | summaryLine, |
| 20 | waitingRows, |
| 21 | weekOf, |
| 22 | whyFor, |
| 23 | } from "./mission-control.ts"; |
| 24 | |
| 25 | const HOUR = 3_600_000; |
| 26 | const DAY = 24 * HOUR; |
| 27 | // Monday 2026-10-05, 18:00 UTC. |
| 28 | const NOW = Date.parse("2026-10-05T18:00:00Z"); |
| 29 | const repo = { namespace: "acme", name: "web" }; |
| 30 | |
| 31 | test("each kind of need gets its reason chip", () => { |
| 32 | assert.equal(reasonFor({ kind: "deploy", detail: "" }), "blocking"); |
| 33 | assert.equal(reasonFor({ kind: "limit", detail: "" }), "blocking"); |
| 34 | assert.equal(reasonFor({ kind: "review", detail: "" }), "asked_for_you"); |
| 35 | assert.equal(reasonFor({ kind: "invitation", detail: "" }), "asked_for_you"); |
| 36 | assert.equal(reasonFor({ kind: "ready", detail: "" }), "ready_to_merge"); |
| 37 | assert.equal(reasonFor({ kind: "checks", detail: "" }), "checks_failing"); |
| 38 | assert.equal(reasonFor({ kind: "stuck", detail: "" }), "stalled"); |
| 39 | }); |
| 40 | |
| 41 | test("a stopped pull request's reason comes from the sentence g1t stopped with", () => { |
| 42 | assert.equal(stallReason("The acceptance checks still fail after the agent revised 3 times."), "checks_failing"); |
| 43 | assert.equal(stallReason("CI / push still fails after the agent revised 2 times."), "checks_failing"); |
| 44 | assert.equal(stallReason("The acceptance checks could not be run."), "checks_failing"); |
| 45 | assert.equal( |
| 46 | stallReason("g1t stopped the agent's implement run when it reached its cost cap. Raise the cap under Settings, Guardrails."), |
| 47 | "outside_guardrails", |
| 48 | ); |
| 49 | assert.equal(stallReason("g1t stopped the agent's review run when it reached its time cap."), "outside_guardrails"); |
| 50 | assert.equal(stallReason("g1t could not merge this: the branch moved"), "blocking"); |
| 51 | assert.equal(stallReason("Needs 1 approving review from someone with write access."), "needs_review"); |
| 52 | assert.equal(stallReason("The review still asks for changes after the agent revised 3 times."), "needs_review"); |
| 53 | assert.equal(stallReason("alex asked for changes, and the agent has already revised 3 times."), "needs_review"); |
| 54 | assert.equal(stallReason("syntaqx stopped the agent's implement run."), "stalled"); |
| 55 | assert.equal(stallReason("g1t could not start the next step: no slot"), "stalled"); |
| 56 | }); |
| 57 | |
| 58 | test("why a need needs a person is said for every reason", () => { |
| 59 | for (const kind of ["deploy", "limit", "invitation", "review", "stuck", "checks", "ready", "conflict"] as const) { |
| 60 | const need = { kind, detail: "" }; |
| 61 | assert.ok(whyFor(reasonFor(need), need).length > 20, kind); |
| 62 | } |
| 63 | const stalled = { kind: "stalled" as const, detail: "It reached its cost cap." }; |
| 64 | assert.match(whyFor(reasonFor(stalled), stalled), /Guardrails/); |
| 65 | }); |
| 66 | |
| 67 | test("what the agent knows lists only what is known", () => { |
| 68 | const facts = pullFacts({ |
| 69 | checkStatus: "failed", |
| 70 | files: [ |
| 71 | { path: "src/retry.ts", additions: 40, deletions: 2 }, |
| 72 | { path: "src/retry.test.ts", additions: 60, deletions: 0 }, |
| 73 | ], |
| 74 | lifecycle: { revisions: 2 }, |
| 75 | runs: [ |
| 76 | { kind: "implement", costUsd: 0.5 }, |
| 77 | { kind: "revise", costUsd: 0.25 }, |
| 78 | { kind: "checks", costUsd: null }, |
| 79 | ], |
| 80 | }); |
| 81 | const by = Object.fromEntries(facts.map((f) => [f.label, f])); |
| 82 | assert.equal(by.Checks.value, "Failing"); |
| 83 | assert.equal(by.Checks.tone, "bad"); |
| 84 | assert.equal(by["Files changed"].value, "2"); |
| 85 | assert.equal(by.Lines.value, "+100 −2"); |
| 86 | assert.equal(by.Tests.value, "1 file"); |
| 87 | assert.equal(by["Sent back"].value, "2 times"); |
| 88 | assert.equal(by["Agent runs"].value, "2 · $0.75"); |
| 89 | |
| 90 | const bare = pullFacts({ checkStatus: null, files: [] }); |
| 91 | assert.deepEqual(bare, [{ label: "Checks", value: "Not run", tone: null }]); |
| 92 | }); |
| 93 | |
| 94 | test("test files are recognised by the names test runners use", () => { |
| 95 | assert.ok(isTestFile("src/a.test.ts")); |
| 96 | assert.ok(isTestFile("src/__tests__/a.ts")); |
| 97 | assert.ok(isTestFile("tests/api.rs")); |
| 98 | assert.ok(isTestFile("pkg/store_test.go")); |
| 99 | assert.ok(!isTestFile("src/testing-utils.ts")); |
| 100 | assert.ok(!isTestFile("src/contest.ts")); |
| 101 | }); |
| 102 | |
| 103 | const merged = (daysAgo: number, mergedBy: string | null, number = 1): Merged => ({ |
| 104 | repo, |
| 105 | number, |
| 106 | title: `Change ${number}`, |
| 107 | agent: "g1t-agent", |
| 108 | mergedBy, |
| 109 | mergedAt: new Date(NOW - daysAgo * DAY).toISOString(), |
| 110 | files: [], |
| 111 | }); |
| 112 | |
| 113 | test("a change landed without a person when g1t merged it", () => { |
| 114 | assert.ok(landedByAgents({ mergedBy: "g1t" })); |
| 115 | assert.ok(landedByAgents({ mergedBy: "g1t-agent" })); |
| 116 | assert.ok(landedByAgents({ mergedBy: null })); |
| 117 | assert.ok(!landedByAgents({ mergedBy: "syntaqx" })); |
| 118 | }); |
| 119 | |
| 120 | test("the week is seven days, each split by who landed it, with the week before", () => { |
| 121 | const week = weekOf( |
| 122 | [merged(0, "g1t"), merged(0, "syntaqx"), merged(1, "g1t"), merged(6, "g1t"), merged(8, "g1t"), merged(10, "alex"), merged(20, "g1t")], |
| 123 | NOW, |
| 124 | "UTC", |
| 125 | ); |
| 126 | assert.equal(week.days.length, 7); |
| 127 | assert.equal(week.days[6].key, "2026-10-05"); |
| 128 | assert.equal(week.days[6].label, "Mon"); |
| 129 | assert.deepEqual( |
| 130 | week.days.map((d) => [d.agents, d.people]), |
| 131 | [[1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 1]], |
| 132 | ); |
| 133 | assert.equal(week.total, 4); |
| 134 | assert.equal(week.byAgents, 3); |
| 135 | assert.equal(week.previous, 2); |
| 136 | // Not knowing the week before is not the same as nothing in it. |
| 137 | assert.equal(weekOf([merged(0, "g1t")], NOW, "UTC", false).previous, null); |
| 138 | }); |
| 139 | |
| 140 | test("days follow the viewer's time zone", () => { |
| 141 | // 02:00 UTC on the 5th is still the 4th in Los Angeles. |
| 142 | const late = Date.parse("2026-10-05T02:00:00Z"); |
| 143 | assert.equal(dayKey(late, "America/Los_Angeles"), "2026-10-04"); |
| 144 | assert.equal(dayKey(late, "UTC"), "2026-10-05"); |
| 145 | assert.equal(dayKey(late, "Not/AZone"), "2026-10-05"); |
| 146 | const week = weekOf([{ mergedAt: new Date(late).toISOString(), mergedBy: "g1t" }], NOW, "America/Los_Angeles"); |
| 147 | assert.equal(week.days.find((d) => d.key === "2026-10-04")?.agents, 1); |
| 148 | }); |
| 149 | |
| 150 | test("the change from last week, and how it reads", () => { |
| 151 | assert.equal(change(12, 10), 0.2); |
| 152 | assert.equal(change(5, 0), null); |
| 153 | assert.equal(change(5, null), null); |
| 154 | assert.equal(signedPercent(0.2), "+20%"); |
| 155 | assert.equal(signedPercent(-0.054), "−5%"); |
| 156 | assert.equal(signedPercent(0.001), "0%"); |
| 157 | }); |
| 158 | |
| 159 | test("a repository's merged list reaches back when it is not full or its oldest is old enough", () => { |
| 160 | const since = NOW - 14 * DAY; |
| 161 | const at = (days: number) => ({ mergedAt: new Date(NOW - days * DAY).toISOString(), updatedAt: new Date(NOW).toISOString() }); |
| 162 | assert.ok(reachesBack([at(1), at(2)], since, 100)); |
| 163 | assert.ok(reachesBack([at(1), at(20)], since, 2)); |
| 164 | assert.ok(!reachesBack([at(1), at(3)], since, 2)); |
| 165 | }); |
| 166 | |
| 167 | test("landed today is the viewer's calendar day, newest first", () => { |
| 168 | const rows = landedToday([merged(0.1, "g1t", 3), merged(0.5, "syntaqx", 4), merged(1, "g1t", 2)], NOW, "UTC"); |
| 169 | assert.deepEqual( |
| 170 | rows.map((r) => [r.ref, r.byAgents]), |
| 171 | [ |
| 172 | ["#3", true], |
| 173 | ["#4", false], |
| 174 | ], |
| 175 | ); |
| 176 | assert.equal(rows[1].by?.name, "syntaqx"); |
| 177 | }); |
| 178 | |
| 179 | test("waiting on agents holds each pull request once, running ones first, and nothing that needs you", () => { |
| 180 | const pull = (number: number, minutesAgo: number) => ({ |
| 181 | number, |
| 182 | title: `Pull ${number}`, |
| 183 | agent: "g1t-agent", |
| 184 | updatedAt: new Date(NOW - minutesAgo * 60_000).toISOString(), |
| 185 | checkStatus: null, |
| 186 | files: [], |
| 187 | }); |
| 188 | const rows = waitingRows({ |
| 189 | active: [ |
| 190 | { pull: pull(1, 30), lifecycle: { stage: "checking", detail: "The acceptance checks are running.", revisions: 0 }, repo }, |
| 191 | { pull: pull(2, 5), lifecycle: { stage: "needs_you", detail: "Stopped.", revisions: 0 }, repo }, |
| 192 | { pull: pull(3, 50), lifecycle: { stage: "queued", detail: "In the merge queue.", revisions: 0 }, repo }, |
| 193 | { pull: pull(9, 1), lifecycle: { stage: "reviewing", detail: "Reviewing.", revisions: 0 }, repo }, |
| 194 | ], |
| 195 | live: [ |
| 196 | { |
| 197 | id: "r1", |
| 198 | repo, |
| 199 | number: 3, |
| 200 | title: "Pull 3", |
| 201 | kind: "update", |
| 202 | agent: "g1t-agent", |
| 203 | step: "Merging main in", |
| 204 | costUsd: 0.1, |
| 205 | startedAt: new Date(NOW - 60_000).toISOString(), |
| 206 | createdAt: new Date(NOW - 60_000).toISOString(), |
| 207 | updatedAt: new Date(NOW - 10_000).toISOString(), |
| 208 | }, |
| 209 | { |
| 210 | id: "r2", |
| 211 | repo, |
| 212 | number: null, |
| 213 | title: null, |
| 214 | kind: "plan", |
| 215 | agent: "g1t-agent", |
| 216 | step: null, |
| 217 | costUsd: null, |
| 218 | startedAt: null, |
| 219 | createdAt: new Date(NOW - 120_000).toISOString(), |
| 220 | updatedAt: new Date(NOW - 120_000).toISOString(), |
| 221 | }, |
| 222 | ], |
| 223 | drafts: [ |
| 224 | { ...pull(1, 30), repo }, |
| 225 | { ...pull(4, 20), repo }, |
| 226 | { ...pull(5, 20), agent: "syntaqx", repo }, |
| 227 | ], |
| 228 | needKeys: new Set(["acme/web#9"]), |
| 229 | }); |
| 230 | assert.deepEqual( |
| 231 | rows.map((r) => [r.ref, r.chip, r.live]), |
| 232 | [ |
| 233 | ["#3", "In queue", true], |
| 234 | [null, "Planning", true], |
| 235 | ["#4", "Working", false], |
| 236 | ["#1", "Checking", false], |
| 237 | ], |
| 238 | ); |
| 239 | assert.equal(rows[0].detail, "Merging main in"); |
| 240 | assert.equal(rows[0].run, "/acme/web/agents/runs/r1"); |
| 241 | assert.equal(rows[1].title, "Planning in web"); |
| 242 | }); |
| 243 | |
| 244 | test("tabs and sorting come from the address", () => { |
| 245 | assert.equal(parseTab("waiting"), "waiting"); |
| 246 | assert.equal(parseTab("nope"), null); |
| 247 | assert.equal(parseSort("newest"), "newest"); |
| 248 | assert.equal(parseSort(null), "impact"); |
| 249 | const rows = [{ at: 1 }, { at: 3 }, { at: 2 }]; |
| 250 | assert.deepEqual(sortRows(rows, "impact"), rows); |
| 251 | assert.deepEqual( |
| 252 | sortRows(rows, "newest").map((r) => r.at), |
| 253 | [3, 2, 1], |
| 254 | ); |
| 255 | }); |
| 256 | |
| 257 | test("the summary says the week honestly", () => { |
| 258 | assert.equal(summaryLine({ total: 47, byAgents: 39, live: 2, needs: 8 }), "Agents landed 39 of 47 changes this week without you."); |
| 259 | assert.equal(summaryLine({ total: 3, byAgents: 3, live: 0, needs: 0 }), "Agents landed all 3 changes this week without you."); |
| 260 | assert.equal(summaryLine({ total: 2, byAgents: 0, live: 0, needs: 0 }), "2 changes landed this week, each merged by a person."); |
| 261 | assert.equal(summaryLine({ total: 0, byAgents: 0, live: 1, needs: 0 }), "1 agent is at work. Nothing has landed this week yet."); |
| 262 | assert.match(summaryLine({ total: 0, byAgents: 0, live: 0, needs: 0 }), /Assign an issue/); |
| 263 | }); |