Public: anyone can read this; the crew can change it.

test/fork-pull-approval.test.ts runs in under 10 s, every test under 1 s, proving what it proves today #10246

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/10238-test-fork-pull-approval-test-ts-runs-in → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/10238-test-fork-pull-approval-test-ts-runs-in was merged into main as 772154f.

Review

Sign in to review this change.
1 file changed +137 −40
test/fork-pull-approval.test.ts +137 −40
@@ -5,7 +5,15 @@
55 * pull and the crew's run as they always have.
66 */
77
8 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import {
9 afterAll,
10 beforeAll,
11 describe,
12 expect,
13 spyOn,
14 test,
15 type Mock,
16 } from "bun:test";
917 import { mkdtemp, rm } from "node:fs/promises";
1018 import { tmpdir } from "node:os";
1119 import { join } from "node:path";
@@ -22,10 +30,40 @@ import type { Repo } from "../src/core/repos.ts";
2230 import { listRuns } from "../src/core/runs.ts";
2331 import { createUser, type User } from "../src/core/users.ts";
2432 import { serve, type Servers } from "../src/http/server.ts";
25 import { quietLogs } from "./helpers/globals.ts";
33 import { logLevel, setLogLevel, type LogLevel } from "../src/kernel/log.ts";
2634 import { promote } from "./helpers/members.ts";
2735
28 quietLogs();
36 /**
37 * The forge's info lines, held instead of printed: the gate says "waits for a
38 * writer's approval" when it refuses, and that line is the witness that the
39 * event was handled. A fixed sleep only guessed it had been.
40 */
41 const said: string[] = [];
42 let foundLevel: LogLevel;
43 const spies: Mock<(...a: unknown[]) => void>[] = [];
44 beforeAll(() => {
45 foundLevel = logLevel();
46 setLogLevel("info");
47 for (const m of ["info", "warn"] as const)
48 spies.push(
49 spyOn(console, m).mockImplementation((...a: unknown[]) => {
50 said.push(a.join(" "));
51 }),
52 );
53 });
54 afterAll(() => {
55 for (const spy of spies) spy.mockRestore();
56 setLogLevel(foundLevel);
57 });
58
59 /** Poll every 5 ms until `ok`, or fail naming `what`. */
60 async function until(ok: () => boolean, what: string, ms = 10_000) {
61 const deadline = Date.now() + ms;
62 while (!ok()) {
63 if (Date.now() > deadline) throw new Error(`timed out waiting for ${what}`);
64 await Bun.sleep(5);
65 }
66 }
2967
3068 const checkJson = (run: string) =>
3169 `{"check": {"run": ${JSON.stringify(run)}, "install": ""}}\n`;
@@ -57,8 +95,63 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
5795
5896 const forks = new Map<string, Repo>();
5997
60 /** Long enough for the event handlers to have queued whatever they would. */
61 const settle = () => Bun.sleep(400);
98 /** Whether `line` carries the field `key=value` exactly. */
outdated

refusals() matches only on message + env=pull-N (+commit). Pull numbers repeat across lib and notes (both have pull-1, pull-2, ...), and the preview line carries only the app slug, not the repo. A refusal logged for notes pull-N therefore satisfies checkDecided(lib, pull-N), and the count of 2 in the crewvote test can be met by two different repos. The wait then ends early and toHaveLength(0) is vacuous - exactly the guess the sleep made. Fix: also match the repo field (repo=alice/lib for the check line, app=<slug> for the preview line), or snapshot said.length before the action and count only lines after it, per repo. Keep the commit filter.

99 const field = (line: string, key: string, value: string) =>
100 ` ${line} `.includes(` ${key}=${value} `);
101
102 /**
103 * How many times the gate refused `env` in one repository — `scope` is
104 * `repo=owner/name` for a check, `app=slug` for a preview, since pull
105 * numbers repeat across repositories — at `commit`, when named.
106 */
107 const refusals = (
108 message: string,
109 scope: [string, string],
110 env: string,
111 commit?: string,
112 ) =>
113 said.filter(
114 (line) =>
115 line.includes(message) &&
116 field(line, ...scope) &&
117 field(line, "env", env) &&
118 (commit === undefined || field(line, "commit", commit.slice(0, 7))),
119 ).length;
120
121 /**
122 * The checks service has handled `pull`'s events up to the `times`th
123 * refusal — or, were the gate gone, queued a run the test then sees.
124 */
125 const checkDecided = (repo: Repo, pull: Pull, commit?: string, times = 1) =>
126 until(
127 () =>
128 refusals(
129 "check waits for a writer's approval",
130 ["repo", `${repo.ownerName}/${repo.name}`],
131 `pull-${pull.number}`,
132 commit,
133 ) >= times || runsFor(repo, "check", pull, commit).length > 0,
134 `the check gate on ${repo.name} pull ${pull.number}`,
135 );
136
137 /** The same for the apps runtime and `pull`'s preview on the notes app. */
138 const previewDecided = (repo: Repo, pull: Pull) =>
139 until(
140 () =>
141 refusals(
142 "preview waits for a writer's approval",
143 ["app", notesApp.slug],
144 `pull-${pull.number}`,
145 ) > 0 || runsFor(repo, "deploy", pull).length > 0,
146 `the preview gate on ${repo.name} pull ${pull.number}`,
147 );
148
149 /** Wait for a `kind` run of `pull` at `commit`. */
150 const ran = (repo: Repo, kind: "check" | "deploy", pull: Pull, commit: string) =>
151 until(
152 () => runsFor(repo, kind, pull, commit).length > 0,
153 `a ${kind} of pull ${pull.number}`,
154 );
62155
63156 async function forkPull(
64157 who: User,
@@ -155,7 +248,7 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
155248 if (!madeApp.ok) throw new Error(String(madeApp.error));
156249 notes = madeApp.value.repo;
157250 notesApp = madeApp.value.app;
158 }, 120_000);
251 }, 20_000);
159252
160253 afterAll(async () => {
161254 await checks.stop();
@@ -166,12 +259,14 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
166259 });
167260
168261 test("a reader's fork pull queues no check", async () => {
262 // Invariant: opening a reader's fork pull queues no check run.
169263 const { pull } = await forkPull(carol, lib, "readers");
170 await settle();
264 await checkDecided(lib, pull);
171265 expect(runsFor(lib, "check", pull)).toHaveLength(0);
172266 });
173267
174268 test("the pull's page says what it waits for, and stops saying it once approved", async () => {
269 // Invariant: the page says the pull waits for approval until a writer approves.
175270 const { pull } = await forkPull(carol, lib, "page");
176271 const page = () =>
177272 fetch(`${servers.url}/alice/lib/pulls/${pull.number}`, {
@@ -180,46 +275,47 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
180275 const first = await page();
181276 expect(first).toContain("wait for a writer to approve this head");
182277 await approve(alice, lib, pull);
183 await settle();
184278 expect(await page()).not.toContain("wait for a writer to approve this head");
185279 });
186280
187281 test("a reader's fork pull on an app deploys no preview", async () => {
282 // Invariant: opening a reader's fork pull on an app deploys no preview.
188283 const { pull } = await forkPull(carol, notes, "readers");
189 await settle();
284 await previewDecided(notes, pull);
190285 expect(runsFor(notes, "deploy", pull)).toHaveLength(0);
191286 });
192287
193288 test("a writer's approval of that head starts the check and the preview", async () => {
289 // Invariant: a writer's approval of the head runs its check and its preview.
194290 const one = await forkPull(carol, lib, "approved");
195291 const two = await forkPull(carol, notes, "approved");
196 await settle();
292 await checkDecided(lib, one.pull);
293 await previewDecided(notes, two.pull);
197294 expect(runsFor(lib, "check", one.pull)).toHaveLength(0);
295 expect(runsFor(notes, "deploy", two.pull)).toHaveLength(0);
198296 await approve(alice, lib, one.pull);
199297 await approve(alice, notes, two.pull);
200 await settle();
201 expect(runsFor(lib, "check", one.pull, one.head).length).toBeGreaterThan(0);
202 expect(runsFor(notes, "deploy", two.pull, two.head).length).toBeGreaterThan(
203 0,
204 );
205 }, 60_000);
298 await ran(lib, "check", one.pull, one.head);
299 await ran(notes, "deploy", two.pull, two.head);
300 });
206301
207302 test("a new push is a new head, and needs a new approval", async () => {
303 // Invariant: an approval holds for its head only; a pushed head runs nothing.
208304 const { pull, fork, head } = await forkPull(carol, lib, "pushes");
209305 await approve(alice, lib, pull);
210 await settle();
211 expect(runsFor(lib, "check", pull, head).length).toBeGreaterThan(0);
306 await ran(lib, "check", pull, head);
212307 const again = await commitToBranch(app, carol, fork, {
213308 branch: "pushes",
214309 message: "Again",
215310 files: [{ path: "pushes.md", content: "again\n" }],
216311 });
217312 if (!again.ok) throw new Error(JSON.stringify(again.error));
218 await settle();
313 await checkDecided(lib, pull, again.value.commitOid);
219314 expect(runsFor(lib, "check", pull, again.value.commitOid)).toHaveLength(0);
220 }, 60_000);
315 });
221316
222317 test("approval from the crew or from the pull's own reader does not count", async () => {
318 // Invariant: neither the crew's approval nor the author's own lets it run.
223319 const { pull } = await forkPull(carol, lib, "crewvote");
224320 const byBot = await submitReview(app, bot, lib, pull, {
225321 state: "approved",
@@ -229,27 +325,24 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
229325 state: "approved",
230326 });
231327 expect(byCarol.ok).toBe(false);
232 await settle();
328 // Refused once on opening, once more on the crew's approval.
329 await checkDecided(lib, pull, undefined, 2);
233330 expect(runsFor(lib, "check", pull)).toHaveLength(0);
234331 });
235332
236333 test("a writer's own fork pull and the crew's run as before", async () => {
334 // Invariant: a writer's and the crew's fork pulls are checked without approval.
237335 const mine = await forkPull(alice, lib, "writers");
238336 const crews = await forkPull(bot, lib, "crews");
239 await settle();
240 expect(runsFor(lib, "check", mine.pull, mine.head).length).toBeGreaterThan(
241 0,
242 );
243 expect(
244 runsFor(lib, "check", crews.pull, crews.head).length,
245 ).toBeGreaterThan(0);
246 }, 60_000);
337 await ran(lib, "check", mine.pull, mine.head);
338 await ran(lib, "check", crews.pull, crews.head);
339 });
247340
248341 test("a writer asking for changes on the approved head stops it running again", async () => {
342 // Invariant: a writer's changes request withdraws the approval of that head.
249343 const { pull, head } = await forkPull(carol, lib, "turned");
250344 await approve(alice, lib, pull);
251 await settle();
252 expect(runsFor(lib, "check", pull, head).length).toBeGreaterThan(0);
345 await ran(lib, "check", pull, head);
253346 const turned = await submitReview(app, alice, lib, pull, {
254347 state: "changes_requested",
255348 body: "Not this.",
@@ -264,14 +357,14 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
264357 const { pull, head } = await forkPull(carol, notes, branch);
265358 await approve(alice, notes, pull);
266359 const env = `pull-${pull.number}`;
267 const deadline = Date.now() + 90_000;
360 const deadline = Date.now() + 10_000;
268361 while (
269362 !listRuns(app.db, notes.id, { kind: "deploy", limit: 100 }).some(
270363 (r) => r.env === env && r.commitOid === head && r.state === "ok",
271364 )
272365 ) {
273366 if (Date.now() > deadline) throw new Error("preview did not deploy");
274 await Bun.sleep(200);
367 await Bun.sleep(5);
275368 }
276369 return { pull, head, env };
277370 }
@@ -292,6 +385,7 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
292385 }
293386
294387 test("a preview asleep at a head nobody approved is taken down, not woken", async () => {
388 // Invariant: waking an asleep preview asks for approval again.
295389 const { pull, env } = await deployedPreview("sleeper");
296390 await restart();
297391 expect(envOf(env)?.state).toBe("asleep");
@@ -299,17 +393,19 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
299393 app.db.run("DELETE FROM review WHERE issue_id = ?1", [pull.id]);
300394 expect(await runtime.wake(notesApp, env as never)).toBeUndefined();
301395 expect(envOf(env)).toBeUndefined();
302 }, 120_000);
396 }, 20_000);
303397
304398 test("at boot, a preview at a head nobody approved is taken down", async () => {
399 // Invariant: boot takes down a preview whose head nobody approved.
305400 const { pull, env } = await deployedPreview("booted");
306401 // As if it had been deployed before the gate: no approval stands.
307402 app.db.run("DELETE FROM review WHERE issue_id = ?1", [pull.id]);
308403 await restart();
309404 expect(envOf(env)).toBeUndefined();
310 }, 120_000);
405 }, 20_000);
311406
312407 test("a writer asking for changes on a running preview's head takes it down at once", async () => {
408 // Invariant: a changes request takes a running reader's preview down.
313409 const { pull, env } = await deployedPreview("running");
314410 expect(["running", "deploying"]).toContain(envOf(env)?.state ?? "");
315411 const turned = await submitReview(app, alice, notes, pull, {
@@ -319,11 +415,12 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
319415 expect(turned.ok).toBe(true);
320416 const deadline = Date.now() + 10_000;
321417 while (envOf(env) !== undefined && Date.now() < deadline)
322 await Bun.sleep(100);
418 await Bun.sleep(5);
323419 expect(envOf(env)).toBeUndefined();
324 }, 120_000);
420 }, 20_000);
325421
326422 test("a writer's own preview survives boot and a changes request", async () => {
423 // Invariant: a writer's own preview needs no approval to come back at boot.
327424 const mine = await commitToBranch(app, alice, notes, {
328425 branch: "own",
329426 message: "Own",
@@ -337,12 +434,12 @@ describe("a reader's fork pull runs nothing until a writer approves its head", (
337434 });
338435 if (!opened.ok) throw new Error(String(opened.error));
339436 const env = `pull-${opened.value.number}`;
340 const deadline = Date.now() + 90_000;
437 const deadline = Date.now() + 10_000;
341438 while (envOf(env)?.state !== "running") {
342439 if (Date.now() > deadline) throw new Error("preview did not deploy");
343 await Bun.sleep(200);
440 await Bun.sleep(5);
344441 }
345442 await restart();
346443 expect(envOf(env)?.state).toBe("asleep");
347 }, 120_000);
444 }, 20_000);
348445 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs