Skip to content
428 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 type Merged,
6 placePushes,
7 historyCovers,
8 change,
9 checksFact,
10 confidenceAsk,
11 confidenceLine,
12 dayKey,
13 isTestFile,
14 landedByAgents,
15 landedToday,
16 parseSort,
17 parseTab,
18 pullFacts,
19 reachesBack,
20 reasonFor,
21 signedPercent,
22 sortRows,
23 stallReason,
24 summaryLine,
25 pushedByPerson,
26 pushedCommits,
27 waitingRows,
28 weekOf,
29 whyFor,
30 withConfidence,
31} from "./mission-control.ts";
32
33const HOUR = 3_600_000;
34const DAY = 24 * HOUR;
35// Monday 2026-10-05, 18:00 UTC.
36const NOW = Date.parse("2026-10-05T18:00:00Z");
37const repo = { namespace: "acme", name: "web" };
38
39test("each kind of need gets its reason chip", () => {
40 assert.equal(reasonFor({ kind: "deploy", detail: "" }), "blocking");
41 assert.equal(reasonFor({ kind: "limit", detail: "" }), "blocking");
42 assert.equal(reasonFor({ kind: "review", detail: "" }), "asked_for_you");
43 assert.equal(reasonFor({ kind: "invitation", detail: "" }), "asked_for_you");
44 assert.equal(reasonFor({ kind: "ready", detail: "" }), "ready_to_merge");
45 assert.equal(reasonFor({ kind: "checks", detail: "" }), "checks_failing");
46 assert.equal(reasonFor({ kind: "stuck", detail: "" }), "stalled");
47});
48
49test("a stopped pull request's reason comes from the sentence g1t stopped with", () => {
50 assert.equal(stallReason("The required check CI still fails after the agent revised 3 times."), "checks_failing");
51 assert.equal(stallReason("The required checks CI and Lint still fail after the agent revised twice."), "checks_failing");
52 assert.equal(stallReason("CI / push still fails after the agent revised 2 times."), "checks_failing");
53 assert.equal(stallReason("It failed in the merge queue after the agent revised twice."), "checks_failing");
54 assert.equal(stallReason("Its checks could not be run."), "checks_failing");
55 assert.equal(
56 stallReason("g1t stopped the agent's implement run when it reached its cost cap. Raise the cap under Settings, Guardrails."),
57 "outside_guardrails",
58 );
59 assert.equal(stallReason("g1t stopped the agent's review run when it reached its time cap."), "outside_guardrails");
60 assert.equal(stallReason("g1t could not merge this: the branch moved"), "blocking");
61 assert.equal(stallReason("Needs 1 approving review from someone with write access."), "needs_review");
62 assert.equal(stallReason("The review still asks for changes after the agent revised 3 times."), "needs_review");
63 assert.equal(stallReason("alex asked for changes, and the agent has already revised 3 times."), "needs_review");
64 assert.equal(stallReason("syntaqx stopped the agent's implement run."), "stalled");
65 assert.equal(stallReason("g1t could not start the next step: no slot"), "stalled");
66});
67
68test("why a need needs a person is said for every reason", () => {
69 for (const kind of ["deploy", "limit", "invitation", "review", "stuck", "checks", "ready", "conflict"] as const) {
70 const need = { kind, detail: "" };
71 assert.ok(whyFor(reasonFor(need), need).length > 20, kind);
72 }
73 const stalled = { kind: "stalled" as const, detail: "It reached its cost cap." };
74 assert.match(whyFor(reasonFor(stalled), stalled), /Guardrails/);
75});
76
77test("what the agent knows lists only what is known", () => {
78 const facts = pullFacts({
79 checkStatus: "failed",
80 files: [
81 { path: "src/retry.ts", additions: 40, deletions: 2 },
82 { path: "src/retry.test.ts", additions: 60, deletions: 0 },
83 ],
84 lifecycle: { revisions: 2 },
85 runs: [
86 { kind: "implement", costUsd: 0.5 },
87 { kind: "revise", costUsd: 0.25 },
88 { kind: "checks", costUsd: null },
89 ],
90 });
91 const by = Object.fromEntries(facts.map((f) => [f.label, f]));
92 assert.equal(by["Required checks"].value, "Failed in the merge queue");
93 assert.equal(by["Required checks"].tone, "bad");
94 assert.equal(by["Files changed"].value, "2");
95 assert.equal(by.Lines.value, "+100 −2");
96 assert.equal(by.Tests.value, "1 file");
97 assert.equal(by["Sent back"].value, "2 times");
98 assert.equal(by["Agent runs"].value, "2 · $0.75");
99
100 const bare = pullFacts({ checkStatus: null, files: [] });
101 assert.deepEqual(bare, [{ label: "Required checks", value: "Not known yet", tone: null }]);
102});
103
104test("the required checks are read from where the agent's pull request stands", () => {
105 const at = (stage: Parameters<typeof checksFact>[1] extends infer L ? NonNullable<L>["stage"] : never, detail: string) =>
106 checksFact(null, { stage, detail });
107 assert.deepEqual(at("checking", "Waiting for CI / pull_request to finish."), { label: "Required checks", value: "Running", tone: null });
108 assert.equal(at("checking", "Waiting for the required check Deploy to report on its latest commit.").value, "Not reported yet");
109 assert.equal(at("revising", "CI / pull_request failed. The agent is being sent back to fix it.").value, "Failing");
110 assert.equal(at("needs_you", "The required check CI still fails after the agent revised twice.").value, "Failing");
111 // Past the checks: a review, an approval or a person's decision holds it, not them.
112 assert.equal(at("revising", "The review asked for changes. The agent is being sent back to make them.").value, "Passing");
113 assert.equal(at("needs_you", "This repository requires 1 approving review before a pull request merges; this one has 0.").value, "Passing");
114 assert.equal(at("ready", "Everything this repository asks for is met. Ready to merge.").tone, "good");
115 assert.equal(at("working", "g1t is making the change.").value, "Not run yet");
116 assert.equal(checksFact("failed", { stage: "revising", detail: "It failed in the merge queue." }).value, "Failed in the merge queue");
117});
118
119test("a change held for low confidence is its own reason, ahead of what it would read as", () => {
120 const held =
121 "The agent's confidence in this change is low (checks failing, tests not added). This repository asks a person before merging it: approve it to let it land, or ask for changes.";
122 // Its reasons name checks and approval, but it is held for its confidence.
123 assert.equal(stallReason(held), "low_confidence");
124 assert.equal(reasonFor({ kind: "stalled", detail: held }), "low_confidence");
125
126 const low = { level: "low" as const, reasons: ["tests not added", "3 revisions"], uncertainAbout: ["the retry limit"] };
127 assert.equal(withConfidence("ready_to_merge", low), "low_confidence");
128 assert.equal(withConfidence("needs_review", low), "low_confidence");
129 // A failure that needs someone anyway keeps its own chip.
130 assert.equal(withConfidence("checks_failing", low), "checks_failing");
131 assert.equal(withConfidence("ready_to_merge", { level: "medium" }), "ready_to_merge");
132 assert.equal(withConfidence("ready_to_merge", null), "ready_to_merge");
133
134 assert.equal(confidenceLine(low), "Low — tests not added, 3 revisions");
135 assert.equal(confidenceLine({ level: "high", reasons: [] }), "High");
136 assert.match(confidenceAsk(low), /not sure of the change: tests not added, 3 revisions\. Approve it/);
137
138 const why = whyFor("low_confidence", { kind: "stalled", detail: held }, low);
139 assert.match(why, /from tests not added, 3 revisions\./);
140 assert.match(why, /unsure about the retry limit/);
141 assert.match(why, /nothing merges it until you approve it/);
142 // Ready, not held: it says to look before merging.
143 assert.match(whyFor("low_confidence", { kind: "ready", detail: "" }, low), /Look at it before you merge it/);
144});
145
146test("what the agent knows gains its confidence, across the row", () => {
147 const facts = pullFacts({
148 checkStatus: "passed",
149 files: [{ path: "src/retry.ts", additions: 40, deletions: 2 }],
150 lifecycle: { revisions: 3 },
151 confidence: { level: "low", reasons: ["tests not added", "3 revisions"] },
152 });
153 const fact = facts.find((f) => f.label === "Confidence");
154 assert.deepEqual(fact, { label: "Confidence", value: "Low — tests not added, 3 revisions", tone: "bad", wide: true });
155 assert.equal(facts.at(-1)?.label, "Confidence");
156});
157
158test("test files are recognised by the names test runners use", () => {
159 assert.ok(isTestFile("src/a.test.ts"));
160 assert.ok(isTestFile("src/__tests__/a.ts"));
161 assert.ok(isTestFile("tests/api.rs"));
162 assert.ok(isTestFile("pkg/store_test.go"));
163 assert.ok(!isTestFile("src/testing-utils.ts"));
164 assert.ok(!isTestFile("src/contest.ts"));
165 assert.ok(isTestFile("test.js"));
166 assert.ok(isTestFile("lib/spec.rb"));
167});
168
169const merged = (daysAgo: number, mergedBy: string | null, number = 1, authoredByAgent = true): Merged => ({
170 repo,
171 number,
172 title: `Change ${number}`,
173 agent: "g1t",
174 authoredByAgent,
175 mergedBy,
176 mergedAt: new Date(NOW - daysAgo * DAY).toISOString(),
177 files: [],
178});
179
180test("a push to the default branch counts a person's own commits, not merges or g1t's", () => {
181 const c = (hash: string, email: string, parents = 1) => ({ hash, author: { name: "g1t", email }, parents: Array(parents).fill("p") });
182 // Every commit is named "g1t": the name decides nothing, the address does.
183 const history = [c("e", "chase@example.com"), c("d", "agent@g1t.sh"), c("m", "chase@example.com", 2), c("b", "chase@example.com"), c("a", "chase@example.com")];
184 const byG1t = (commit: { author: { email: string } }) => commit.author.email === "agent@g1t.sh";
185 assert.deepEqual(pushedCommits(history, "a", byG1t).map((x) => x.hash), ["e", "b"]);
186 // Where it pointed before was not read: everything read counts.
187 assert.deepEqual(pushedCommits(history, "zz", byG1t).map((x) => x.hash), ["e", "b", "a"]);
188});
189
190test("who pushed is the account that signed in, not the name on the commits", () => {
191 assert.ok(pushedByPerson({ actor: "usr_chase", data: {} }));
192 assert.ok(pushedByPerson({ actor: null, data: {} }));
193 assert.ok(!pushedByPerson({ actor: "usr_g1t_agent", data: {} }));
194 assert.ok(!pushedByPerson({ actor: "g1t_policy", data: {} }));
195 // A workflow job's own token is not a person either.
196 assert.ok(!pushedByPerson({ actor: "usr_chase", data: { causedByJob: "run_1" } }));
197
198 const c = (hash: string, name: string) => ({ hash, parents: ["p"], author: { name, email: `${hash}@example.com` } });
199 // A person whose git name is "g1t" pushed m1; g1t pushed w1 under a person's name.
200 const history = [c("w1", "Chase Pierce"), c("m1", "g1t"), c("old", "Chase Pierce")];
201 const pushes = [
202 { time: "2026-10-07T12:00:00Z", actor: "usr_g1t_agent", data: { after: "w1", before: "m1" } },
203 { time: "2026-10-05T12:00:00Z", actor: "usr_chase", data: { after: "m1", before: "old" } },
204 ];
205 assert.deepEqual(placePushes(history, pushes), [{ hash: "m1", at: "2026-10-05T12:00:00Z" }]);
206});
207
208test("a short read of the branch is enough when it reaches the oldest push", () => {
209 const h = (...hashes: string[]) => hashes.map((hash) => ({ hash }));
210 const pushes = [
211 { time: "2026-10-07T12:00:00Z", data: { after: "c1", before: "c2" } },
212 { time: "2026-10-05T12:00:00Z", data: { after: "c2", before: "c3" } },
213 ];
214 // Holds the oldest push's before.
215 assert.ok(historyCovers(h("c1", "c2", "c3"), pushes, 3));
216 // Full, and the oldest push reaches past it: read further.
217 assert.ok(!historyCovers(h("c1", "c2", "x"), pushes, 3));
218 // Shorter than asked: the whole branch.
219 assert.ok(historyCovers(h("c1", "c2"), pushes, 3));
220 // The oldest push made the branch: only the whole branch will do.
221 assert.ok(!historyCovers(h("c1", "c2", "c3"), [{ time: "", data: { after: "c1" } }], 3));
222});
223
224test("a change landed without a person when g1t merged it", () => {
225 assert.ok(landedByAgents({ mergedBy: "g1t" }));
226 assert.ok(landedByAgents({ mergedBy: "g1t" }));
227 assert.ok(landedByAgents({ mergedBy: null }));
228 assert.ok(!landedByAgents({ mergedBy: "syntaqx" }));
229});
230
231test("the week is seven days, each split by who did the work, with the week before", () => {
232 const week = weekOf(
233 [
234 merged(0, "g1t"),
235 merged(0, "syntaqx"),
236 // A person's own change, merged by them and auto-merged by g1t: both theirs.
237 merged(0, "syntaqx", 2, false),
238 merged(1, "g1t", 3, false),
239 merged(1, "g1t"),
240 merged(6, "g1t"),
241 merged(8, "g1t"),
242 merged(10, "alex"),
243 merged(20, "g1t"),
244 ],
245 NOW,
246 "UTC",
247 );
248 assert.equal(week.days.length, 7);
249 assert.equal(week.days[6].key, "2026-10-05");
250 assert.equal(week.days[6].label, "Mon");
251 assert.deepEqual(
252 week.days.map((d) => [d.agents, d.assisted, d.people]),
253 [[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [1, 0, 1], [1, 1, 1]],
254 );
255 assert.equal(week.total, 6);
256 assert.equal(week.byAgents, 3);
257 assert.equal(week.agentChanges, 4);
258 assert.equal(week.people, 2);
259 assert.equal(week.previous, 2);
260 // Not knowing the week before is not the same as nothing in it.
261 assert.equal(weekOf([merged(0, "g1t")], NOW, "UTC", false).previous, null);
262});
263
264test("days follow the viewer's time zone", () => {
265 // 02:00 UTC on the 5th is still the 4th in Los Angeles.
266 const late = Date.parse("2026-10-05T02:00:00Z");
267 assert.equal(dayKey(late, "America/Los_Angeles"), "2026-10-04");
268 assert.equal(dayKey(late, "UTC"), "2026-10-05");
269 assert.equal(dayKey(late, "Not/AZone"), "2026-10-05");
270 const week = weekOf([{ mergedAt: new Date(late).toISOString(), mergedBy: "g1t", authoredByAgent: true }], NOW, "America/Los_Angeles");
271 assert.equal(week.days.find((d) => d.key === "2026-10-04")?.agents, 1);
272});
273
274test("the change from last week, and how it reads", () => {
275 assert.equal(change(12, 10), 0.2);
276 assert.equal(change(5, 0), null);
277 assert.equal(change(5, null), null);
278 assert.equal(signedPercent(0.2), "+20%");
279 assert.equal(signedPercent(-0.054), "−5%");
280 assert.equal(signedPercent(0.001), "0%");
281});
282
283test("a repository's merged list reaches back when it is not full or its oldest is old enough", () => {
284 const since = NOW - 14 * DAY;
285 const at = (days: number) => ({ mergedAt: new Date(NOW - days * DAY).toISOString(), updatedAt: new Date(NOW).toISOString() });
286 assert.ok(reachesBack([at(1), at(2)], since, 100));
287 assert.ok(reachesBack([at(1), at(20)], since, 2));
288 assert.ok(!reachesBack([at(1), at(3)], since, 2));
289});
290
291test("landed today is the viewer's calendar day, newest first", () => {
292 const rows = landedToday([merged(0.1, "g1t", 3), merged(0.5, "syntaqx", 4), merged(1, "g1t", 2)], NOW, "UTC");
293 assert.deepEqual(
294 rows.map((r) => [r.ref, r.byAgents]),
295 [
296 ["#3", true],
297 ["#4", false],
298 ],
299 );
300 assert.equal(rows[1].by?.name, "syntaqx");
301});
302
303test("waiting on agents holds each pull request once, running ones first, and nothing that needs you", () => {
304 const pull = (number: number, minutesAgo: number) => ({
305 number,
306 title: `Pull ${number}`,
307 agent: "g1t",
308 updatedAt: new Date(NOW - minutesAgo * 60_000).toISOString(),
309 checkStatus: null,
310 files: [],
311 });
312 const rows = waitingRows({
313 active: [
314 { pull: pull(1, 30), lifecycle: { stage: "checking", detail: "Waiting for CI / pull_request to finish.", revisions: 0 }, repo },
315 { pull: pull(2, 5), lifecycle: { stage: "needs_you", detail: "Stopped.", revisions: 0 }, repo },
316 { pull: pull(3, 50), lifecycle: { stage: "queued", detail: "In the merge queue.", revisions: 0 }, repo },
317 { pull: pull(9, 1), lifecycle: { stage: "reviewing", detail: "Reviewing.", revisions: 0 }, repo },
318 ],
319 live: [
320 {
321 id: "r1",
322 repo,
323 number: 3,
324 title: "Pull 3",
325 kind: "update",
326 agent: "g1t",
327 step: "Merging main in",
328 costUsd: 0.1,
329 startedAt: new Date(NOW - 60_000).toISOString(),
330 createdAt: new Date(NOW - 60_000).toISOString(),
331 updatedAt: new Date(NOW - 10_000).toISOString(),
332 },
333 {
334 id: "r2",
335 repo,
336 number: null,
337 title: null,
338 kind: "plan",
339 agent: "g1t",
340 step: null,
341 costUsd: null,
342 startedAt: null,
343 createdAt: new Date(NOW - 120_000).toISOString(),
344 updatedAt: new Date(NOW - 120_000).toISOString(),
345 },
346 ],
347 drafts: [
348 { ...pull(1, 30), repo },
349 { ...pull(4, 20), repo },
350 { ...pull(5, 20), agent: "syntaqx", repo },
351 ],
352 needKeys: new Set(["acme/web#9"]),
353 });
354 assert.deepEqual(
355 rows.map((r) => [r.ref, r.chip, r.live]),
356 [
357 ["#3", "In queue", true],
358 [null, "Planning", true],
359 ["#4", "Working", false],
360 ["#1", "Checking", false],
361 ],
362 );
363 assert.equal(rows[0].detail, "Merging main in");
364 assert.equal(rows[0].run, "/acme/web/agents/runs/r1");
365 assert.equal(rows[1].title, "Planning in web");
366});
367
368test("tabs and sorting come from the address", () => {
369 assert.equal(parseTab("waiting"), "waiting");
370 assert.equal(parseTab("nope"), null);
371 assert.equal(parseSort("newest"), "newest");
372 assert.equal(parseSort(null), "impact");
373 const rows = [{ at: 1 }, { at: 3 }, { at: 2 }];
374 assert.deepEqual(sortRows(rows, "impact"), rows);
375 assert.deepEqual(
376 sortRows(rows, "newest").map((r) => r.at),
377 [3, 2, 1],
378 );
379});
380
381test("the summary says the week honestly", () => {
382 const none = { total: 0, byAgents: 0, agentChanges: 0, people: 0, live: 0, needs: 0 };
383 assert.equal(
384 summaryLine({ ...none, total: 47, byAgents: 39, agentChanges: 47 }),
385 "Agents landed 39 of their 47 changes this week without you.",
386 );
387 assert.equal(
388 summaryLine({ ...none, total: 3, byAgents: 3, agentChanges: 3 }),
389 "Agents landed all 3 changes of theirs this week without you.",
390 );
391 assert.equal(
392 summaryLine({ ...none, total: 2, agentChanges: 2 }),
393 "Agents made 2 changes this week, each merged by a person.",
394 );
395 // People's own work is counted as theirs, never as agents' that needed help.
396 assert.equal(
397 summaryLine({ ...none, total: 7, byAgents: 4, agentChanges: 5, people: 2 }),
398 "Agents landed 4 of their 5 changes this week without you, and people landed 2 changes of their own.",
399 );
400 assert.equal(summaryLine({ ...none, total: 3, people: 3 }), "People landed 3 changes this week; none were agents'.");
401 assert.equal(summaryLine({ ...none, live: 1 }), "1 agent is at work. Nothing has landed this week yet.");
402 assert.match(summaryLine(none), /Assign an issue/);
403});
404
405test("a job waiting for a self-hosted runner says so, not that an agent went quiet", () => {
406 const runner = { kind: "runner" as const, detail: "No runner with labels self-hosted, docker is online." };
407 assert.equal(reasonFor(runner), "stalled");
408 const why = whyFor(reasonFor(runner), runner);
409 assert.match(why, /self-hosted runner/);
410 assert.doesNotMatch(why, /agent/);
411});
412
413test("each push to the default branch places the commits it brought, at its time, from one read of the history", () => {
414 const c = (hash: string, parents = ["p"], email = "chase@example.com") => ({ hash, parents, author: { name: "Chase Pierce", email } });
415 // Newest first: a Wednesday push of two, a Monday push of one, a merge, and g1t's own commit.
416 const history = [c("w2"), c("w1"), c("m1"), c("merge", ["a", "b"]), c("bot", ["p"], "g1t@users.noreply.g1t.sh"), c("old")];
417 const pushes = [
418 { time: "2026-10-07T12:00:00Z", actor: "usr_chase", data: { after: "w2", before: "m1" } },
419 { time: "2026-10-05T12:00:00Z", actor: "usr_chase", data: { after: "m1", before: "old" } },
420 { time: "2026-10-01T12:00:00Z", actor: "usr_chase", data: { after: "gone", before: "older" } },
421 ];
422 const byG1t = (commit: { author: { email: string } }) => commit.author.email.endsWith("@users.noreply.g1t.sh");
423 assert.deepEqual(placePushes(history, pushes, byG1t), [
424 { hash: "m1", at: "2026-10-05T12:00:00Z" },
425 { hash: "w2", at: "2026-10-07T12:00:00Z" },
426 { hash: "w1", at: "2026-10-07T12:00:00Z" },
427 ]);
428});