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

test/looks.test.ts runs in under 10 s, every test under 1 s, proving what it proves today #10269

Merged opened this

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

Merged

mason/forge:mason/10258-test-looks-test-ts-runs-in-under-10-s-ev → main 1 commit fork

  • Merged. mason/10258-test-looks-test-ts-runs-in-under-10-s-ev was merged into main as 418b691.

Review

Sign in to review this change.
1 file changed +286 −216
test/looks.test.ts +286 −216
@@ -80,13 +80,23 @@ describe("the look", () => {
8080 let aliceCookie: string;
8181 let bobCookie: string;
8282
83 // The stand-in browser: writes a picture, or falls over, as told.
84 let mode: "ok" | "fail" | "nobrowser" = "ok";
83 // The stand-in browser: writes a picture, or falls over, as told — told
84 // per app host, since every preview of this file deploys at once.
85 const modes = new Map<string, "fail" | "nobrowser">();
86 const hostOf = (input: LookInput) => new URL(input.url).hostname;
87 /** The host the browser is sent to for one of `hello`'s environments. */
88 const helloHost = (env: string) =>
89 env === "production"
90 ? `hello.${DOMAIN}`
91 : `hello--${env.slice("pull-".length)}.${DOMAIN}`;
8592 const calls: LookInput[] = [];
8693 // The dark pictures, kept apart so what counts the light ones stays true.
8794 const darkCalls: LookInput[] = [];
95 /** The light pictures taken of one of `hello`'s environments. */
96 const callsAt = (env: string) => calls.filter((c) => hostOf(c) === helloHost(env));
8897 const fake: Capture = async (input) => {
8998 (input.dark === true ? darkCalls : calls).push(input);
99 const mode = modes.get(hostOf(input));
90100 if (mode === "fail") return err("the browser fell over");
91101 if (mode === "nobrowser") return err(NO_BROWSER);
92102 writeFileSync(input.out, input.width === 1280 ? DESKTOP_PNG : PNG);
@@ -125,7 +135,7 @@ describe("the look", () => {
125135 while (Date.now() < deadline) {
126136 const value = read();
127137 if (value !== undefined) return value;
128 await Bun.sleep(100);
138 await Bun.sleep(10);
129139 }
130140 throw new Error("did not happen in time");
131141 }
@@ -153,6 +163,86 @@ describe("the look", () => {
153163 return rows.length === 3 ? rows : undefined;
154164 }, 30_000);
155165
166 interface Opened {
167 readonly pull: Pull;
168 readonly commitOid: string;
169 }
170
171 /** A branch off the default with these files, and a pull of it opened by `who`. */
172 const openWith = async (
173 who: User,
174 on: Repo,
175 branch: string,
176 title: string,
177 files: { path: string; content: string }[],
178 onOpen?: (pullNumber: number) => void,
179 ): Promise<Opened> => {
180 const committed = await commitToBranch(app, alice, on, {
181 branch,
182 base: on.defaultBranch,
183 message: title,
184 files,
185 });
186 if (!committed.ok) throw new Error(JSON.stringify(committed.error));
187 const opened = await openPull(app, who, on, {
188 title,
189 baseBranch: on.defaultBranch,
190 headBranch: branch,
191 });
192 if (!opened.ok) throw new Error(String(opened.error));
193 onOpen?.(opened.value.number);
194 return { pull: opened.value, commitOid: committed.value.commitOid };
195 };
196
197 // The previews the tests read, opened and pictured once in `beforeAll`.
198 let greeting: Opened;
199 let unpictured: Opened;
200 let slowRed: Opened;
201 let ask: Opened;
202 let twoPages: Opened;
203 let gallery: Opened & { readonly repo: Repo; readonly app: AppRecord };
204 /** The real-browser app, deployed only when this machine has a Chromium. */
205 let peek: { readonly repo: Repo; readonly app: AppRecord } | undefined;
206
207 const chromium = findChromium();
208
209 /** An app that writes down who the forge said was there. */
210 const startPeek = async (): Promise<{ repo: Repo; app: AppRecord }> => {
211 const made = await createRepo(app, alice, {
212 name: "peek",
213 visibility: "private",
214 });
215 if (!made.ok) throw new Error(String(made.error));
216 const seeded = await commitToBranch(app, alice, made.value, {
217 message: "Start",
218 files: [
219 {
220 path: "package.json",
221 content: '{"name":"peek","scripts":{"start":"bun run server.ts"}}\n',
222 },
223 {
224 path: "server.ts",
225 content: [
226 'import { appendFileSync } from "node:fs";',
227 "Bun.serve({",
228 ' port: Number(process.env.PORT), hostname: "127.0.0.1",',
229 " fetch(request) {",
230 ' const who = request.headers.get("x-forge-identity") ?? "nobody";',
231 " appendFileSync(`${process.env.FORGE_APP_DATA}/seen.txt`, `${who}\\n`);",
232 ' return new Response(`<!doctype html><h1>peek</h1><p>${who}</p>`, { headers: { "content-type": "text/html" } });',
233 " },",
234 "});",
235 "",
236 ].join("\n"),
237 },
238 ],
239 });
240 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
241 const enabled = enableApp(app, alice, made.value, { slug: "peek" });
242 if (!enabled.ok) throw new Error(String(enabled.error));
243 return { repo: made.value, app: enabled.value };
244 };
245
156246 beforeAll(async () => {
157247 dataDir = await mkdtemp(join(tmpdir(), "forge-looks-"));
158248 app = createApp({ dataDir, port: 0 });
@@ -162,7 +252,8 @@ describe("the look", () => {
162252 scheme: "http",
163253 publicPort: servers.port,
164254 forgeUrl: servers.url,
165 concurrency: 2,
255 // Room for every preview `beforeAll` opens to deploy at once.
256 concurrency: 8,
166257 });
167258 await runtime.start();
168259 looks = startLooks(app, { localUrl: servers.url, capture: fake });
@@ -192,7 +283,117 @@ describe("the look", () => {
192283 if (!made.ok) throw new Error(String(made.error));
193284 repo = made.value.repo;
194285 record = made.value.app;
195 }, 30_000);
286
287 // Every preview a test only reads is opened here, at once: their deploys
288 // overlap instead of each test waiting out its own.
289 [greeting, unpictured, slowRed, ask, twoPages, gallery, peek] = await Promise.all([
290 openWith(alice, repo, "greeting", "Say hello", [
291 { path: "GREETING.md", content: "hi\n" },
292 ]),
293 // A pull that never had a picture: its browser falls over from the start.
294 openWith(alice, repo, "second", "Second", [{ path: "SECOND.md", content: "2\n" }], (n) =>
295 modes.set(helloHost(previewEnv(n)), "fail"),
296 ),
297 openWith(alice, repo, "slow-red-test", "Slow, then red, test", [
298 {
299 path: "forge.json",
300 content: `${JSON.stringify(
301 {
302 app: {
303 run: "bun run src/server.ts",
304 // Blocks on this file until the test opens it.
305 test: `while [ ! -e '${join(dataDir, "slow-red-test.gate")}' ]; do sleep 0.05; done; exit 1`,
306 tty: "bun run src/tty.ts",
307 },
308 },
309 null,
310 2,
311 )}\n`,
312 },
313 ]),
314 (async () => {
315 const scout = await createAgent(app, alice, { username: "scout" });
316 if (!scout.ok) throw new Error(String(scout.error));
317 if (!addToCrew(app, alice, repo, "scout").ok) throw new Error("scout not crew");
318 return openWith(scout.value.user, repo, "ask/a", "Ask a", [
319 { path: "a.md", content: "a\n" },
320 ]);
321 })(),
322 openWith(alice, repo, "two-pages", "Look at two pages", [
323 {
324 path: "forge.json",
325 content: `${JSON.stringify(
326 {
327 app: {
328 run: "bun run src/server.ts",
329 test: "bun test",
330 tty: "bun run src/tty.ts",
331 looks: ["/", "/board"],
332 },
333 },
334 null,
335 2,
336 )}\n`,
337 },
338 ]),
339 (async () => {
340 const madeGallery = await newApp(app, alice, { name: "gallery", visibility: "public" });
341 if (!madeGallery.ok) throw new Error(String(madeGallery.error));
342 const opened = await openWith(alice, madeGallery.value.repo, "frame", "A frame", [
343 { path: "FRAME.md", content: "frame\n" },
344 ]);
345 return { ...opened, repo: madeGallery.value.repo, app: madeGallery.value.app };
346 })(),
347 chromium === undefined ? Promise.resolve(undefined) : startPeek(),
348 ]);
349
350 // Each settled and pictured before any test reads it.
351 const done = (env: string, commitOid: string, onRepo = repo.id) =>
352 settled((r) => r.env === env && r.commitOid === commitOid, onRepo);
353 await Promise.all([
354 settled((r) => r.env === "production").then((run) =>
355 pictured("production", run.commitOid),
356 ),
357 ...[greeting, unpictured, ask].map(async (opened) => {
358 await done(previewEnv(opened.pull.number), opened.commitOid);
359 await pictured(previewEnv(opened.pull.number), opened.commitOid);
360 }),
361 // Held at its gate, still running: serving, and its pictures the
362 // test's own to wait for.
363 until(() =>
364 listEvents(app.db, { repoId: repo.id, limit: 200 }).some(
365 (e) => e.kind === "app.serving" && e.payload["commitOid"] === slowRed.commitOid,
366 )
367 ? true
368 : undefined,
369 ),
370 done(previewEnv(twoPages.pull.number), twoPages.commitOid).then(() =>
371 until(() => {
372 const env = previewEnv(twoPages.pull.number);
373 const light = looksForEnv(app.db, record.id, env).filter(
374 (l) => l.commitOid === twoPages.commitOid,
375 );
376 const dark = looksForEnv(app.db, record.id, env, "dark").filter(
377 (l) => l.commitOid === twoPages.commitOid,
378 );
379 return light.length === 6 && dark.length === 4 ? true : undefined;
380 }),
381 ),
382 done(previewEnv(gallery.pull.number), gallery.commitOid, gallery.repo.id).then(() =>
383 until(() =>
384 looksForEnv(app.db, gallery.app.id, previewEnv(gallery.pull.number)).filter(
385 (l) => l.commitOid === gallery.commitOid,
386 ).length === 3
387 ? true
388 : undefined,
389 ),
390 ),
391 peek === undefined
392 ? undefined
393 : settled((r) => r.env === "production", peek.repo.id),
394 ]);
395 await looks.idle();
396 }, 60_000);
196397
197398 afterAll(async () => {
198399 await looks.stop();
@@ -206,7 +407,8 @@ describe("the look", () => {
206407 let first = "";
207408 let second = "";
208409
209 test("a green deploy is pictured at all three widths, once per head — phone and desktop as the owner, the thumb as a stranger, reduced from the desktop page; a newer head replaces", async () => {
410 test("a green production deploy is pictured at all three widths as the owner, the thumb reduced from the desktop page", async () => {
411 // Invariant: production's head gets phone, desktop and thumb, each taken through the grant door as the owner, staged aside and moved in whole.
210412 const production = await settled((r) => r.env === "production");
211413 expect(production.state).toBe("ok");
212414 const rows = await pictured("production", production.commitOid);
@@ -224,17 +426,18 @@ describe("the look", () => {
224426 }
225427 // The browser was pointed at the grant door on the forge's own port,
226428 // and the grant names the owner as an admin.
227 expect(calls).toHaveLength(3);
429 const taken = callsAt("production");
430 expect(taken).toHaveLength(3);
228431 // Taken to one side and moved in whole: nothing half-written lives
229432 // under the environment, and nothing is left behind on the side.
230433 for (const call of calls)
231434 expect(call.out).toContain(join("looks", ".taking"));
232435 expect(readdirSync(join(dataDir, "looks", ".taking"))).toEqual([]);
233 const phone = calls.find((c) => c.width === 390)!;
436 const phone = taken.find((c) => c.width === 390)!;
234437 expect(phone.height).toBe(844);
235438 // Desktop and thumb are both taken in desktop's window: a browser does
236439 // not lay a page out at a tile's size.
237 const wide = calls.filter((c) => c.width === 1280);
440 const wide = taken.filter((c) => c.width === 1280);
238441 expect(wide.map((c) => c.height)).toEqual([800, 800]);
239442 const thumb = rows.find((l) => l.kind === "thumb")!;
240443 expect(pngSize(join(dataDir, thumb.path))).toEqual([640, 400]);
@@ -242,7 +445,7 @@ describe("the look", () => {
242445 expect(pngSize(join(dataDir, desktop.path))).toEqual([1280, 800]);
243446 // This app is private: a stranger is sent to sign in on another host,
244447 // so its thumb is taken as the owner, through the same grant door (#7830).
245 const thumbCall = calls.find((c) => c.out.endsWith("-thumb.png"))!;
448 const thumbCall = taken.find((c) => c.out.endsWith("-thumb.png"))!;
246449 const thumbGrant = readGrant(
247450 dataDir,
248451 new URL(thumbCall.url).searchParams.get("t") ?? "",
@@ -260,27 +463,17 @@ describe("the look", () => {
260463 env: "production",
261464 });
262465 expect(grant?.who).toEqual({ id: alice.id, name: "alice", role: "admin" });
466 });
263467
264 // A pull's preview, the same way.
265 const committed = await commitToBranch(app, alice, repo, {
266 branch: "greeting",
267 base: repo.defaultBranch,
268 message: "Say hello",
269 files: [{ path: "GREETING.md", content: "hi\n" }],
270 });
271 if (!committed.ok) throw new Error(JSON.stringify(committed.error));
272 first = committed.value.commitOid;
273 const opened = await openPull(app, alice, repo, {
274 title: "Say hello",
275 baseBranch: repo.defaultBranch,
276 headBranch: "greeting",
277 });
278 if (!opened.ok) throw new Error(String(opened.error));
279 pull = opened.value;
468 test("a pull's preview is pictured at its own host, and what the page recorded reaches a check made afterwards", async () => {
469 // Invariant: a green preview gets its three looks at `hello--<n>`, and the phone's LCP and desktop's CLS reach the vitals of a check created after the look.
470 pull = greeting.pull;
471 first = greeting.commitOid;
280472 const env = previewEnv(pull.number);
281473 expect((await settled((r) => r.env === env)).state).toBe("ok");
282474 expect(await pictured(env, first)).toHaveLength(3);
283 expect(calls).toHaveLength(6);
475 expect(callsAt(env)).toHaveLength(3);
476 expect(new URL(callsAt(env)[0]!.url).hostname).toBe(`hello--${pull.number}.${DOMAIN}`);
284477 // The entries the browser recorded reach the check's summary: the
285478 // phone's LCP and the desktop's CLS, graded; the thumb adds no row.
286479 // The look finished before any check run of this commit existed; the
@@ -309,20 +502,23 @@ describe("the look", () => {
309502 value: "0.3",
310503 grade: "poor",
311504 });
312 expect(new URL(calls[3]!.url).hostname).toBe(
313 `hello--${pull.number}.${DOMAIN}`,
314 );
505 });
315506
316 // The same head green again is not pictured again.
507 test("the same head green again is not pictured again", async () => {
508 // Invariant: a redeploy of an already-pictured head takes no new picture and leaves its three rows.
509 const env = previewEnv(pull.number);
317510 runtime.requestDeploy(record, repo, env, first, "redeploy");
318511 expect(
319512 (await settled((r) => r.env === env && r.reason === "redeploy")).state,
320513 ).toBe("ok");
321514 await looks.idle();
322 expect(calls).toHaveLength(6);
515 expect(callsAt(env)).toHaveLength(3);
323516 expect(looksForEnv(app.db, record.id, env)).toHaveLength(3);
517 });
324518
325 // A newer head is pictured, and the older head's pictures go.
519 test("a newer head is pictured, and the older head's pictures go", async () => {
520 // Invariant: a new green head of a preview is pictured once and replaces the older head's rows and files; production's are untouched.
521 const env = previewEnv(pull.number);
326522 const again = await commitToBranch(app, alice, repo, {
327523 branch: "greeting",
328524 message: "More greeting",
@@ -334,17 +530,18 @@ describe("the look", () => {
334530 (await settled((r) => r.env === env && r.commitOid === second)).state,
335531 ).toBe("ok");
336532 expect(await pictured(env, second)).toHaveLength(3);
337 expect(calls).toHaveLength(9);
533 expect(callsAt(env)).toHaveLength(6);
338534 await until(() =>
339535 looksForEnv(app.db, record.id, env).every((l) => l.commitOid === second)
340536 ? true
341537 : undefined,
342538 );
343 expect(existsSync(join(dataDir, rows[0]!.path))).toBe(true);
539 const production = looksForEnv(app.db, record.id, "production")[0]!;
540 expect(existsSync(join(dataDir, production.path))).toBe(true);
344541 expect(
345542 existsSync(join(dataDir, "looks", record.id, env, `${first}-phone.png`)),
346543 ).toBe(false);
347 }, 120_000);
544 });
348545
349546 test("the pull page shows both pictures; the route serves them cached, and refuses a stranger", async () => {
350547 const base = `/alice/hello`;
@@ -431,11 +628,10 @@ describe("the look", () => {
431628 expect(retaken.headers.get("etag")).not.toBe(etag);
432629 });
433630
434 let unpictured: Pull;
435
436631 test("a picture that could not be taken is recorded with its reason, and the page says so", async () => {
437 mode = "fail";
632 // Invariant: a failed capture is a row with its reason and no path; the pull keeps its last good pictures and says the head moved, and why.
438633 const env = `pull-${pull.number}`;
634 modes.set(helloHost(env), "fail");
439635 const moved = await commitToBranch(app, alice, repo, {
440636 branch: "greeting",
441637 message: "Once more",
@@ -463,38 +659,27 @@ describe("the look", () => {
463659 (await web(`/alice/hello/looks/${env}/${third}/phone.png`, aliceCookie))
464660 .status,
465661 ).toBe(404);
662 modes.delete(helloHost(env));
663 });
466664
467 // A pull that never had a picture says so, without one.
468 const branch = await commitToBranch(app, alice, repo, {
469 branch: "second",
470 base: repo.defaultBranch,
471 message: "Second",
472 files: [{ path: "SECOND.md", content: "2\n" }],
473 });
474 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
475 const opened = await openPull(app, alice, repo, {
476 title: "Second",
477 baseBranch: repo.defaultBranch,
478 headBranch: "second",
479 });
480 if (!opened.ok) throw new Error(String(opened.error));
481 unpictured = opened.value;
482 const env2 = `pull-${unpictured.number}`;
483 expect((await settled((r) => r.env === env2)).state).toBe("ok");
484 await pictured(env2, branch.value.commitOid);
665 test("a pull that never had a picture says so, with the reason, and links none", async () => {
666 // Invariant: a pull whose every capture failed shows the Look card's "No picture of" with the browser's reason and no picture URL.
667 const env2 = `pull-${unpictured.pull.number}`;
668 const rows = await pictured(env2, unpictured.commitOid);
669 expect(rows.every((l) => l.error === "the browser fell over")).toBe(true);
485670 const none = await (
486 await web(`/alice/hello/pulls/${unpictured.number}`, aliceCookie)
671 await web(`/alice/hello/pulls/${unpictured.pull.number}`, aliceCookie)
487672 ).text();
488673 expect(none).toContain("<h2>Look</h2>");
489674 expect(none).toContain("No picture of");
490675 expect(none).toContain("the browser fell over");
491676 expect(none).not.toContain(`/looks/${env2}/`);
492 mode = "ok";
493 }, 120_000);
677 });
494678
495679 test("with no browser the check's summary says so and stays green", async () => {
496 mode = "nobrowser";
497 const env = `pull-${unpictured.number}`;
680 // Invariant: NO_BROWSER is recorded for the check as "no browser" per look, words only, and leaves the check's state alone.
681 const env = `pull-${unpictured.pull.number}`;
682 modes.set(helloHost(env), "nobrowser");
498683 const moved = await commitToBranch(app, alice, repo, {
499684 branch: "second",
500685 message: "No browser",
@@ -522,8 +707,8 @@ describe("the look", () => {
522707 // Words only: the table has no verdict to turn a check red.
523708 expect(vitalsRows(got).map((g) => g.skipped)).toEqual(["no browser", "no browser"]);
524709 expect(listRuns(app.db, repo.id, { kind: "check" }).find((r) => r.id === check.id)!.state).toBe("queued");
525 mode = "ok";
526 }, 120_000);
710 modes.delete(helloHost(env));
711 });
527712
528713 test("a look recorded with no theme reads as light, and light and dark never share a slot", () => {
529714 const base = {
@@ -568,57 +753,31 @@ describe("the look", () => {
568753 });
569754
570755 test("a closed pull's pictures go with its preview", async () => {
571 const env = `pull-${unpictured.number}`;
756 // Invariant: closing a pull deletes its preview's look rows and folder, and its page shows no Look card.
757 const env = `pull-${unpictured.pull.number}`;
572758 expect(looksForEnv(app.db, record.id, env).length).toBeGreaterThan(0);
573759 const closed = await setIssueStateThrough(
574760 app,
575761 alice,
576762 repo,
577 unpictured,
763 unpictured.pull,
578764 "closed",
579765 );
580766 expect(closed.ok).toBe(true);
581767 expect(looksForEnv(app.db, record.id, env)).toHaveLength(0);
582768 expect(existsSync(join(dataDir, "looks", record.id, env))).toBe(false);
583769 const page = await (
584 await web(`/alice/hello/pulls/${unpictured.number}`, aliceCookie)
770 await web(`/alice/hello/pulls/${unpictured.pull.number}`, aliceCookie)
585771 ).text();
586772 expect(page).not.toContain("<h2>Look</h2>");
587773 });
588774
589775 test("a look is taken from `app.serving`, while the deploy's tests are still running, and a red check afterward does not remove it", async () => {
590 // The app's test blocks on this file until the look has been taken.
776 // Invariant: the look is taken on `app.serving` while the deploy's test still runs, and that test failing afterward leaves the pictures.
591777 const gate = join(dataDir, "slow-red-test.gate");
592 const branch = await commitToBranch(app, alice, repo, {
593 branch: "slow-red-test",
594 base: repo.defaultBranch,
595 message: "Slow, then red, test",
596 files: [
597 {
598 path: "forge.json",
599 content: `${JSON.stringify(
600 {
601 app: {
602 run: "bun run src/server.ts",
603 test: `while [ ! -e '${gate}' ]; do sleep 0.05; done; exit 1`,
604 tty: "bun run src/tty.ts",
605 },
606 },
607 null,
608 2,
609 )}\n`,
610 },
611 ],
612 });
613 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
614 const opened = await openPull(app, alice, repo, {
615 title: "Slow, then red, test",
616 baseBranch: repo.defaultBranch,
617 headBranch: "slow-red-test",
618 });
619 if (!opened.ok) throw new Error(String(opened.error));
620 const env = `pull-${opened.value.number}`;
621 const commitOid = branch.value.commitOid;
778 const opened = { value: slowRed.pull };
779 const env = `pull-${slowRed.pull.number}`;
780 const commitOid = slowRed.commitOid;
622781
623782 // Pictured while the test step is held at the gate — the run is still
624783 // open, checking.
@@ -650,10 +809,11 @@ describe("the look", () => {
650809 expect(
651810 (await setIssueStateThrough(app, alice, repo, opened.value, "closed")).ok,
652811 ).toBe(true);
653 }, 30_000);
812 });
654813
655814 test("two events for one environment, back to back, leave one queued look — at the newer commit", async () => {
656 const before = calls.length;
815 // Invariant: a second app.serving for an environment replaces the queued first, so the superseded commit is never pictured.
816 const before = callsAt("production").length;
657817 const live = runtime.envStatus(record, "production");
658818 if (live === undefined) throw new Error("production is not live");
659819 const superseded = `${"0".repeat(39)}1`;
@@ -683,7 +843,7 @@ describe("the look", () => {
683843 // The superseded commit was never even looked at: production's own
684844 // head was already pictured (from the first test), so nothing new
685845 // was captured for it either.
686 expect(calls.length).toBe(before);
846 expect(callsAt("production").length).toBe(before);
687847 expect(
688848 looksForEnv(app.db, record.id, "production").some(
689849 (l) => l.commitOid === superseded,
@@ -692,26 +852,10 @@ describe("the look", () => {
692852 });
693853
694854 test("the app's head shows production as a thumb", async () => {
695 const scout = await createAgent(app, alice, { username: "scout" });
696 if (!scout.ok) throw new Error(String(scout.error));
697 expect(addToCrew(app, alice, repo, "scout").ok).toBe(true);
698 const branch = await commitToBranch(app, alice, repo, {
699 branch: "ask/a",
700 base: repo.defaultBranch,
701 message: "Ask a",
702 files: [{ path: "a.md", content: "a\n" }],
703 });
704 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
705 const opened = await openPull(app, scout.value.user, repo, {
706 title: "Ask a",
707 baseBranch: repo.defaultBranch,
708 headBranch: "ask/a",
709 });
710 if (!opened.ok) throw new Error(String(opened.error));
711 const ask = opened.value;
712 const env = `pull-${ask.number}`;
855 // Invariant: the studio's app head shows production's thumb, and falls back to its desktop picture when no thumb row exists.
856 const env = `pull-${ask.pull.number}`;
713857 expect((await settled((r) => r.env === env)).state).toBe("ok");
714 const rows = await pictured(env, branch.value.commitOid);
858 const rows = await pictured(env, ask.commitOid);
715859 expect(rows.every((l) => l.error === null)).toBe(true);
716860
717861 const studio = `${aliceCookie}; forge_mode=studio`;
@@ -745,40 +889,13 @@ describe("the look", () => {
745889 expect(fallback).toContain(
746890 `src="/alice/hello/looks/production/${productionDesktop.commitOid}/desktop.png?v=`,
747891 );
748 }, 120_000);
892 });
749893
750894 test("app.looks naming two pages pictures both, distinctly, and the pull page groups them", async () => {
751 const branch = await commitToBranch(app, alice, repo, {
752 branch: "two-pages",
753 base: repo.defaultBranch,
754 message: "Look at two pages",
755 files: [
756 {
757 path: "forge.json",
758 content: `${JSON.stringify(
759 {
760 app: {
761 run: "bun run src/server.ts",
762 test: "bun test",
763 tty: "bun run src/tty.ts",
764 looks: ["/", "/board"],
765 },
766 },
767 null,
768 2,
769 )}\n`,
770 },
771 ],
772 });
773 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
774 const opened = await openPull(app, alice, repo, {
775 title: "Look at two pages",
776 baseBranch: repo.defaultBranch,
777 headBranch: "two-pages",
778 });
779 if (!opened.ok) throw new Error(String(opened.error));
895 // Invariant: each page `app.looks` names gets its own light and dark slots and files, grouped by page on the pull, dark folded away.
896 const opened = { value: twoPages.pull };
780897 const env = `pull-${opened.value.number}`;
781 const commitOid = branch.value.commitOid;
898 const commitOid = twoPages.commitOid;
782899 expect((await settled((r) => r.env === env)).state).toBe("ok");
783900
784901 const rows = await until(() => {
@@ -881,7 +998,7 @@ describe("the look", () => {
881998 ).text();
882999 expect(after).not.toContain(stale);
8831000 expect(after).toContain(`${commitOid}/phone-dark.png?v=`);
884 }, 120_000);
1001 });
8851002
8861003 test("a private app's thumb taken as a stranger by an older forge is deleted on boot, to be retaken", async () => {
8871004 const old = "c".repeat(40);
@@ -965,11 +1082,11 @@ describe("the look", () => {
9651082 join(dataDir, thumb.path),
9661083 Buffer.from(encodePng(200, 125, new Uint8Array(200 * 125 * 4))),
9671084 );
968 const before = calls.length;
1085 const before = callsAt("production").length;
9691086 looks.sweep();
9701087 await looks.idle();
971 expect(calls).toHaveLength(before + 1);
972 const retaken = calls.at(-1)!;
1088 expect(callsAt("production")).toHaveLength(before + 1);
1089 const retaken = callsAt("production").at(-1)!;
9731090 expect(retaken.out.endsWith("-thumb.png")).toBe(true);
9741091 expect([retaken.width, retaken.height]).toEqual([1280, 800]);
9751092 expect(
@@ -983,7 +1100,7 @@ describe("the look", () => {
9831100 // A whole set: nothing to take.
9841101 looks.sweep();
9851102 await looks.idle();
986 expect(calls).toHaveLength(before + 1);
1103 expect(callsAt("production")).toHaveLength(before + 1);
9871104
9881105 // A picture that failed at this head had its turn: the sweep leaves
9891106 // it to the next deploy rather than starting a browser every time.
@@ -997,8 +1114,8 @@ describe("the look", () => {
9971114 });
9981115 looks.sweep();
9991116 await looks.idle();
1000 expect(calls).toHaveLength(before + 1);
1001 }, 60_000);
1117 expect(callsAt("production")).toHaveLength(before + 1);
1118 });
10021119
10031120 test("pictures are shown only to those allowed to see them (#727): on a public repository hosting a private app, a stranger gets none, a signed-in reader the thumb, a writer all", async () => {
10041121 // `hello` is a private app; its repository is made public here.
@@ -1094,27 +1211,13 @@ describe("the look", () => {
10941211 });
10951212
10961213 test("a public app's previews are taken as nobody, so a reader of its pull sees the Look card; a picture taken as the owner stays the writers' (#738)", async () => {
1097 const made = await newApp(app, alice, { name: "gallery", visibility: "public" });
1098 if (!made.ok) throw new Error(String(made.error));
1099 const gallery = made.value.repo;
1100 const galleryApp = made.value.app;
1214 // Invariant: a public app's preview pictures carry no grant identity and are served to readers and strangers; owner-taken ones stay the writers'.
1215 const galleryApp = gallery.app;
11011216 expect(galleryApp.access).toBe("public");
1102 const branch = await commitToBranch(app, alice, gallery, {
1103 branch: "frame",
1104 base: gallery.defaultBranch,
1105 message: "A frame",
1106 files: [{ path: "FRAME.md", content: "frame\n" }],
1107 });
1108 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
1109 const opened = await openPull(app, alice, gallery, {
1110 title: "A frame",
1111 baseBranch: gallery.defaultBranch,
1112 headBranch: "frame",
1113 });
1114 if (!opened.ok) throw new Error(String(opened.error));
1217 const opened = { value: gallery.pull };
11151218 const env = previewEnv(opened.value.number);
1116 const commit = branch.value.commitOid;
1117 expect((await settled((r) => r.env === env, gallery.id)).state).toBe("ok");
1219 const commit = gallery.commitOid;
1220 expect((await settled((r) => r.env === env, gallery.repo.id)).state).toBe("ok");
11181221 const rows = await until(() => {
11191222 const found = looksForEnv(app.db, galleryApp.id, env).filter(
11201223 (l) => l.commitOid === commit,
@@ -1152,7 +1255,7 @@ describe("the look", () => {
11521255 await web(`/alice/gallery/pulls/${opened.value.number}`, bobCookie)
11531256 ).text();
11541257 expect(after).not.toContain("<h2>Look</h2>");
1155 }, 120_000);
1258 });
11561259
11571260 test("a public app made private after its pictures were taken: a reader gets them no more, and no Look card; its writer still does (#845)", async () => {
11581261 // Its own app, branch, pull and pictures: no other test's capture.
@@ -1266,52 +1369,19 @@ describe("the look", () => {
12661369 expect(memoryPressure(stat, "max\n", "3400000000\n")).toBeUndefined();
12671370 });
12681371
1269 const chromium = findChromium();
12701372 test.skipIf(chromium === undefined)(
12711373 chromium === undefined
12721374 ? "a real capture — skipped: no chromium on this machine (install one, or set FORGE_CHROMIUM)"
12731375 : `a real capture through the grant door, with ${chromium}`,
12741376 async () => {
1275 // An app that writes down who the forge said was there.
1276 const made = await createRepo(app, alice, {
1277 name: "peek",
1278 visibility: "private",
1279 });
1280 if (!made.ok) throw new Error(String(made.error));
1281 const peek = made.value;
1282 const seeded = await commitToBranch(app, alice, peek, {
1283 message: "Start",
1284 files: [
1285 {
1286 path: "package.json",
1287 content:
1288 '{"name":"peek","scripts":{"start":"bun run server.ts"}}\n',
1289 },
1290 {
1291 path: "server.ts",
1292 content: [
1293 'import { appendFileSync } from "node:fs";',
1294 "Bun.serve({",
1295 ' port: Number(process.env.PORT), hostname: "127.0.0.1",',
1296 " fetch(request) {",
1297 ' const who = request.headers.get("x-forge-identity") ?? "nobody";',
1298 " appendFileSync(`${process.env.FORGE_APP_DATA}/seen.txt`, `${who}\\n`);",
1299 ' return new Response(`<!doctype html><h1>peek</h1><p>${who}</p>`, { headers: { "content-type": "text/html" } });',
1300 " },",
1301 "});",
1302 "",
1303 ].join("\n"),
1304 },
1305 ],
1306 });
1307 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
1308 const enabled = enableApp(app, alice, peek, { slug: "peek" });
1309 if (!enabled.ok) throw new Error(String(enabled.error));
1377 // Invariant: a real Chromium sent through the grant door pictures the page at the window's size, records every entry type, and arrives as the owner.
1378 if (peek === undefined) throw new Error("no peek without a browser");
1379 const enabled = { value: peek.app };
13101380 expect(
1311 (await settled((r) => r.env === "production", peek.id)).state,
1381 (await settled((r) => r.env === "production", peek.repo.id)).state,
13121382 ).toBe("ok");
13131383 // The very URL the service builds: the grant door, on this listener.
1314 const door = openEnvUrl(app, alice, peek, "production");
1384 const door = openEnvUrl(app, alice, peek.repo, "production");
13151385 if (!door.ok) throw new Error(String(door.error));
13161386 const out = join(dataDir, "real-phone.png");
13171387 const taken = await takeLook({

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs