g1t/apps/web/app/lib/mission-control.test.ts

323 lines14,456 bytesCodeBlame
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 type Merged,
6 change,
7 checksFact,
8 confidenceAsk,
9 confidenceLine,
10 dayKey,
11 isTestFile,
12 landedByAgents,
13 landedToday,
14 parseSort,
15 parseTab,
16 pullFacts,
17 reachesBack,
18 reasonFor,
19 signedPercent,
20 sortRows,
21 stallReason,
22 summaryLine,
23 waitingRows,
24 weekOf,
25 whyFor,
26 withConfidence,
27} from "./mission-control.ts";
28
29const HOUR = 3_600_000;
30const DAY = 24 * HOUR;
31// Monday 2026-10-05, 18:00 UTC.
32const NOW = Date.parse("2026-10-05T18:00:00Z");
33const repo = { namespace: "acme", name: "web" };
34
35test("each kind of need gets its reason chip", () => {
36 assert.equal(reasonFor({ kind: "deploy", detail: "" }), "blocking");
37 assert.equal(reasonFor({ kind: "limit", detail: "" }), "blocking");
38 assert.equal(reasonFor({ kind: "review", detail: "" }), "asked_for_you");
39 assert.equal(reasonFor({ kind: "invitation", detail: "" }), "asked_for_you");
40 assert.equal(reasonFor({ kind: "ready", detail: "" }), "ready_to_merge");
41 assert.equal(reasonFor({ kind: "checks", detail: "" }), "checks_failing");
42 assert.equal(reasonFor({ kind: "stuck", detail: "" }), "stalled");
43});
44
45test("a stopped pull request's reason comes from the sentence g1t stopped with", () => {
46 assert.equal(stallReason("The required check CI still fails after the agent revised 3 times."), "checks_failing");
47 assert.equal(stallReason("The required checks CI and Lint still fail after the agent revised twice."), "checks_failing");
48 assert.equal(stallReason("CI / push still fails after the agent revised 2 times."), "checks_failing");
49 assert.equal(stallReason("It failed in the merge queue after the agent revised twice."), "checks_failing");
50 assert.equal(stallReason("Its checks could not be run."), "checks_failing");
51 assert.equal(
52 stallReason("g1t stopped the agent's implement run when it reached its cost cap. Raise the cap under Settings, Guardrails."),
53 "outside_guardrails",
54 );
55 assert.equal(stallReason("g1t stopped the agent's review run when it reached its time cap."), "outside_guardrails");
56 assert.equal(stallReason("g1t could not merge this: the branch moved"), "blocking");
57 assert.equal(stallReason("Needs 1 approving review from someone with write access."), "needs_review");
58 assert.equal(stallReason("The review still asks for changes after the agent revised 3 times."), "needs_review");
59 assert.equal(stallReason("alex asked for changes, and the agent has already revised 3 times."), "needs_review");
60 assert.equal(stallReason("syntaqx stopped the agent's implement run."), "stalled");
61 assert.equal(stallReason("g1t could not start the next step: no slot"), "stalled");
62});
63
64test("why a need needs a person is said for every reason", () => {
65 for (const kind of ["deploy", "limit", "invitation", "review", "stuck", "checks", "ready", "conflict"] as const) {
66 const need = { kind, detail: "" };
67 assert.ok(whyFor(reasonFor(need), need).length > 20, kind);
68 }
69 const stalled = { kind: "stalled" as const, detail: "It reached its cost cap." };
70 assert.match(whyFor(reasonFor(stalled), stalled), /Guardrails/);
71});
72
73test("what the agent knows lists only what is known", () => {
74 const facts = pullFacts({
75 checkStatus: "failed",
76 files: [
77 { path: "src/retry.ts", additions: 40, deletions: 2 },
78 { path: "src/retry.test.ts", additions: 60, deletions: 0 },
79 ],
80 lifecycle: { revisions: 2 },
81 runs: [
82 { kind: "implement", costUsd: 0.5 },
83 { kind: "revise", costUsd: 0.25 },
84 { kind: "checks", costUsd: null },
85 ],
86 });
87 const by = Object.fromEntries(facts.map((f) => [f.label, f]));
88 assert.equal(by["Required checks"].value, "Failed in the merge queue");
89 assert.equal(by["Required checks"].tone, "bad");
90 assert.equal(by["Files changed"].value, "2");
91 assert.equal(by.Lines.value, "+100 −2");
92 assert.equal(by.Tests.value, "1 file");
93 assert.equal(by["Sent back"].value, "2 times");
94 assert.equal(by["Agent runs"].value, "2 · $0.75");
95
96 const bare = pullFacts({ checkStatus: null, files: [] });
97 assert.deepEqual(bare, [{ label: "Required checks", value: "Not known yet", tone: null }]);
98});
99
100test("the required checks are read from where the agent's pull request stands", () => {
101 const at = (stage: Parameters<typeof checksFact>[1] extends infer L ? NonNullable<L>["stage"] : never, detail: string) =>
102 checksFact(null, { stage, detail });
103 assert.deepEqual(at("checking", "Waiting for CI / pull_request to finish."), { label: "Required checks", value: "Running", tone: null });
104 assert.equal(at("checking", "Waiting for the required check Deploy to report on its latest commit.").value, "Not reported yet");
105 assert.equal(at("revising", "CI / pull_request failed. The agent is being sent back to fix it.").value, "Failing");
106 assert.equal(at("needs_you", "The required check CI still fails after the agent revised twice.").value, "Failing");
107 // Past the checks: a review, an approval or a person's decision holds it, not them.
108 assert.equal(at("revising", "The review asked for changes. The agent is being sent back to make them.").value, "Passing");
109 assert.equal(at("needs_you", "This repository requires 1 approving review before a pull request merges; this one has 0.").value, "Passing");
110 assert.equal(at("ready", "Everything this repository asks for is met. Ready to merge.").tone, "good");
111 assert.equal(at("working", "A g1t agent is making the change.").value, "Not run yet");
112 assert.equal(checksFact("failed", { stage: "revising", detail: "It failed in the merge queue." }).value, "Failed in the merge queue");
113});
114
115test("a change held for low confidence is its own reason, ahead of what it would read as", () => {
116 const held =
117 "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.";
118 // Its reasons name checks and approval, but it is held for its confidence.
119 assert.equal(stallReason(held), "low_confidence");
120 assert.equal(reasonFor({ kind: "stalled", detail: held }), "low_confidence");
121
122 const low = { level: "low" as const, reasons: ["tests not added", "3 revisions"], uncertainAbout: ["the retry limit"] };
123 assert.equal(withConfidence("ready_to_merge", low), "low_confidence");
124 assert.equal(withConfidence("needs_review", low), "low_confidence");
125 // A failure that needs someone anyway keeps its own chip.
126 assert.equal(withConfidence("checks_failing", low), "checks_failing");
127 assert.equal(withConfidence("ready_to_merge", { level: "medium" }), "ready_to_merge");
128 assert.equal(withConfidence("ready_to_merge", null), "ready_to_merge");
129
130 assert.equal(confidenceLine(low), "Low — tests not added, 3 revisions");
131 assert.equal(confidenceLine({ level: "high", reasons: [] }), "High");
132 assert.match(confidenceAsk(low), /not sure of the change: tests not added, 3 revisions\. Approve it/);
133
134 const why = whyFor("low_confidence", { kind: "stalled", detail: held }, low);
135 assert.match(why, /from tests not added, 3 revisions\./);
136 assert.match(why, /unsure about the retry limit/);
137 assert.match(why, /nothing merges it until you approve it/);
138 // Ready, not held: it says to look before merging.
139 assert.match(whyFor("low_confidence", { kind: "ready", detail: "" }, low), /Look at it before you merge it/);
140});
141
142test("what the agent knows gains its confidence, across the row", () => {
143 const facts = pullFacts({
144 checkStatus: "passed",
145 files: [{ path: "src/retry.ts", additions: 40, deletions: 2 }],
146 lifecycle: { revisions: 3 },
147 confidence: { level: "low", reasons: ["tests not added", "3 revisions"] },
148 });
149 const fact = facts.find((f) => f.label === "Confidence");
150 assert.deepEqual(fact, { label: "Confidence", value: "Low — tests not added, 3 revisions", tone: "bad", wide: true });
151 assert.equal(facts.at(-1)?.label, "Confidence");
152});
153
154test("test files are recognised by the names test runners use", () => {
155 assert.ok(isTestFile("src/a.test.ts"));
156 assert.ok(isTestFile("src/__tests__/a.ts"));
157 assert.ok(isTestFile("tests/api.rs"));
158 assert.ok(isTestFile("pkg/store_test.go"));
159 assert.ok(!isTestFile("src/testing-utils.ts"));
160 assert.ok(!isTestFile("src/contest.ts"));
161});
162
163const merged = (daysAgo: number, mergedBy: string | null, number = 1): Merged => ({
164 repo,
165 number,
166 title: `Change ${number}`,
167 agent: "g1t-agent",
168 mergedBy,
169 mergedAt: new Date(NOW - daysAgo * DAY).toISOString(),
170 files: [],
171});
172
173test("a change landed without a person when g1t merged it", () => {
174 assert.ok(landedByAgents({ mergedBy: "g1t" }));
175 assert.ok(landedByAgents({ mergedBy: "g1t-agent" }));
176 assert.ok(landedByAgents({ mergedBy: null }));
177 assert.ok(!landedByAgents({ mergedBy: "syntaqx" }));
178});
179
180test("the week is seven days, each split by who landed it, with the week before", () => {
181 const week = weekOf(
182 [merged(0, "g1t"), merged(0, "syntaqx"), merged(1, "g1t"), merged(6, "g1t"), merged(8, "g1t"), merged(10, "alex"), merged(20, "g1t")],
183 NOW,
184 "UTC",
185 );
186 assert.equal(week.days.length, 7);
187 assert.equal(week.days[6].key, "2026-10-05");
188 assert.equal(week.days[6].label, "Mon");
189 assert.deepEqual(
190 week.days.map((d) => [d.agents, d.people]),
191 [[1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 1]],
192 );
193 assert.equal(week.total, 4);
194 assert.equal(week.byAgents, 3);
195 assert.equal(week.previous, 2);
196 // Not knowing the week before is not the same as nothing in it.
197 assert.equal(weekOf([merged(0, "g1t")], NOW, "UTC", false).previous, null);
198});
199
200test("days follow the viewer's time zone", () => {
201 // 02:00 UTC on the 5th is still the 4th in Los Angeles.
202 const late = Date.parse("2026-10-05T02:00:00Z");
203 assert.equal(dayKey(late, "America/Los_Angeles"), "2026-10-04");
204 assert.equal(dayKey(late, "UTC"), "2026-10-05");
205 assert.equal(dayKey(late, "Not/AZone"), "2026-10-05");
206 const week = weekOf([{ mergedAt: new Date(late).toISOString(), mergedBy: "g1t" }], NOW, "America/Los_Angeles");
207 assert.equal(week.days.find((d) => d.key === "2026-10-04")?.agents, 1);
208});
209
210test("the change from last week, and how it reads", () => {
211 assert.equal(change(12, 10), 0.2);
212 assert.equal(change(5, 0), null);
213 assert.equal(change(5, null), null);
214 assert.equal(signedPercent(0.2), "+20%");
215 assert.equal(signedPercent(-0.054), "−5%");
216 assert.equal(signedPercent(0.001), "0%");
217});
218
219test("a repository's merged list reaches back when it is not full or its oldest is old enough", () => {
220 const since = NOW - 14 * DAY;
221 const at = (days: number) => ({ mergedAt: new Date(NOW - days * DAY).toISOString(), updatedAt: new Date(NOW).toISOString() });
222 assert.ok(reachesBack([at(1), at(2)], since, 100));
223 assert.ok(reachesBack([at(1), at(20)], since, 2));
224 assert.ok(!reachesBack([at(1), at(3)], since, 2));
225});
226
227test("landed today is the viewer's calendar day, newest first", () => {
228 const rows = landedToday([merged(0.1, "g1t", 3), merged(0.5, "syntaqx", 4), merged(1, "g1t", 2)], NOW, "UTC");
229 assert.deepEqual(
230 rows.map((r) => [r.ref, r.byAgents]),
231 [
232 ["#3", true],
233 ["#4", false],
234 ],
235 );
236 assert.equal(rows[1].by?.name, "syntaqx");
237});
238
239test("waiting on agents holds each pull request once, running ones first, and nothing that needs you", () => {
240 const pull = (number: number, minutesAgo: number) => ({
241 number,
242 title: `Pull ${number}`,
243 agent: "g1t-agent",
244 updatedAt: new Date(NOW - minutesAgo * 60_000).toISOString(),
245 checkStatus: null,
246 files: [],
247 });
248 const rows = waitingRows({
249 active: [
250 { pull: pull(1, 30), lifecycle: { stage: "checking", detail: "Waiting for CI / pull_request to finish.", revisions: 0 }, repo },
251 { pull: pull(2, 5), lifecycle: { stage: "needs_you", detail: "Stopped.", revisions: 0 }, repo },
252 { pull: pull(3, 50), lifecycle: { stage: "queued", detail: "In the merge queue.", revisions: 0 }, repo },
253 { pull: pull(9, 1), lifecycle: { stage: "reviewing", detail: "Reviewing.", revisions: 0 }, repo },
254 ],
255 live: [
256 {
257 id: "r1",
258 repo,
259 number: 3,
260 title: "Pull 3",
261 kind: "update",
262 agent: "g1t-agent",
263 step: "Merging main in",
264 costUsd: 0.1,
265 startedAt: new Date(NOW - 60_000).toISOString(),
266 createdAt: new Date(NOW - 60_000).toISOString(),
267 updatedAt: new Date(NOW - 10_000).toISOString(),
268 },
269 {
270 id: "r2",
271 repo,
272 number: null,
273 title: null,
274 kind: "plan",
275 agent: "g1t-agent",
276 step: null,
277 costUsd: null,
278 startedAt: null,
279 createdAt: new Date(NOW - 120_000).toISOString(),
280 updatedAt: new Date(NOW - 120_000).toISOString(),
281 },
282 ],
283 drafts: [
284 { ...pull(1, 30), repo },
285 { ...pull(4, 20), repo },
286 { ...pull(5, 20), agent: "syntaqx", repo },
287 ],
288 needKeys: new Set(["acme/web#9"]),
289 });
290 assert.deepEqual(
291 rows.map((r) => [r.ref, r.chip, r.live]),
292 [
293 ["#3", "In queue", true],
294 [null, "Planning", true],
295 ["#4", "Working", false],
296 ["#1", "Checking", false],
297 ],
298 );
299 assert.equal(rows[0].detail, "Merging main in");
300 assert.equal(rows[0].run, "/acme/web/agents/runs/r1");
301 assert.equal(rows[1].title, "Planning in web");
302});
303
304test("tabs and sorting come from the address", () => {
305 assert.equal(parseTab("waiting"), "waiting");
306 assert.equal(parseTab("nope"), null);
307 assert.equal(parseSort("newest"), "newest");
308 assert.equal(parseSort(null), "impact");
309 const rows = [{ at: 1 }, { at: 3 }, { at: 2 }];
310 assert.deepEqual(sortRows(rows, "impact"), rows);
311 assert.deepEqual(
312 sortRows(rows, "newest").map((r) => r.at),
313 [3, 2, 1],
314 );
315});
316
317test("the summary says the week honestly", () => {
318 assert.equal(summaryLine({ total: 47, byAgents: 39, live: 2, needs: 8 }), "Agents landed 39 of 47 changes this week without you.");
319 assert.equal(summaryLine({ total: 3, byAgents: 3, live: 0, needs: 0 }), "Agents landed all 3 changes this week without you.");
320 assert.equal(summaryLine({ total: 2, byAgents: 0, live: 0, needs: 0 }), "2 changes landed this week, each merged by a person.");
321 assert.equal(summaryLine({ total: 0, byAgents: 0, live: 1, needs: 0 }), "1 agent is at work. Nothing has landed this week yet.");
322 assert.match(summaryLine({ total: 0, byAgents: 0, live: 0, needs: 0 }), /Assign an issue/);
323});