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

Every preview of the forge pictures the Later gate's proposal at desk and phone width, from a seeded gate holding more themes than fit at rest #9222

Merged opened this

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

Merged

scout/forge:scout/9212-every-preview-of-the-forge-pictures-the → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/9212-every-preview-of-the-forge-pictures-the was merged into main as 2d6d40b.

Review

Sign in to review this change.
5 files changed +126 −9
docs/features/preview-owner-looks.md +8 −3
@@ -13,10 +13,12 @@ every thumb too. Pictures taken as the owner are the writers' alone
1313 there that `app.looks` does not name is a manifest error. The forge names
1414 `/`, `/flow` and `/talk`.
1515
16 **Four looks every forge preview takes.** Whatever `forge.json` lists, a
16 **Six pages every forge preview takes.** Whatever `forge.json` lists, a
1717 preview of the forge's own repository (`FORGE_REPO`) is pictured as its
1818 owner at desk and phone width on home with its pins, /flow,
19 `/preview-admin/sample` and the seeded pull's page: the named list
19 `/preview-admin/sample`, the seeded pull's page, its Iterations page
20 (#9182) and `/preview-admin/sample/held`, the Later gate's proposal
21 (#9212): the named list
2022 `PREVIEW_OWNER_LOOKS` in `src/spawn/preview-looks.ts`, which the looks
2123 service adds to the manifest's pages (#8974). Production, and every other
2224 app, take only what their manifest lists.
@@ -26,7 +28,10 @@ app, take only what their manifest lists.
2628 a small world for it once (`src/spawn/preview-seed.ts`): a public repository
2729 `preview-admin/sample` with two issues and an open pull, three agent runs
2830 that spent this week and a valve turned to 60% (the flow week), and two
29 board panels pinned to home. In a preview, a request whose
31 board panels pinned to home. The seed also puts a Later gate on the sample
32 holding 24 issues in eight themes, two more than the held page draws at
33 rest (`THEMES_AT_REST`), so its fold line shows; it refuses to finish if
34 the page would be a 404 or draw no more themes than that. In a preview, a request whose
3035 `x-forge-identity` is signed under `FORGE_APP_IDENTITY_KEY` and names an
3136 admin is that admin (`src/spawn/preview-owner.ts`) — the parent's looks, and
3237 any writer who opens the preview from the forge. It counts as an edge
src/spawn/preview-looks.ts +24 −4
@@ -1,10 +1,10 @@
11 /**
22 * The looks every preview of the forge takes as its owner (#8974): home
33 * with its pins, /flow, the seeded repository's head, its pull's page and
4 * its Iterations page (#9182), each at desk and phone width. The looks
5 * service adds them to whatever forge.json lists for a preview of the
6 * forge's own repository (`FORGE_REPO`), so no edit of forge.json can
7 * drop one.
4 * its Iterations page (#9182) and the Later gate's proposal (#9212), each
outdated

This comment line is now far past the file wrap width. Re-wrap it.

5 * at desk and phone width. The looks service adds them to whatever
6 * forge.json lists for a preview of the forge's own repository
7 * (`FORGE_REPO`), so no edit of forge.json can drop one.
88 *
99 * Plain data and no imports beyond a type: the seed (`preview-seed.ts`)
1010 * makes what these pages show, and the looks service reads the list.
@@ -41,6 +41,25 @@ export const PREVIEW_FORKS: readonly { readonly owner: string; readonly of?: str
4141 { owner: "preview-dee", of: "preview-ada" },
4242 ];
4343
44 /** The Later gate's proposal on the sample (#9212): what the gate holds, grouped by theme. */
45 export const PREVIEW_HELD_PAGE = `${PREVIEW_REPO_HOME}/held`;
46 /**
47 * The seeded held issues: each theme is a word three titles share (the least
48 * the triage groups on), and there are more themes than the page draws at
49 * rest, so its fold line shows. Titles share no other word, so no issue
50 * falls under "Other" or reads as another's duplicate.
51 */
52 export const PREVIEW_HELD_THEMES: readonly { readonly theme: string; readonly titles: readonly string[] }[] = [
53 { theme: "billing", titles: ["Billing shows a receipt", "Billing offers refunds", "Billing remembers cards"] },
54 { theme: "search", titles: ["Search finds typos", "Search ranks fresh work", "Search saves queries"] },
55 { theme: "mobile", titles: ["Mobile opens offline", "Mobile pulls to refresh", "Mobile shares sheets"] },
56 { theme: "export", titles: ["Export writes archives", "Export schedules nightly", "Export signs bundles"] },
57 { theme: "onboarding", titles: ["Onboarding greets newcomers", "Onboarding offers tours", "Onboarding tracks progress"] },
58 { theme: "themes", titles: ["Themes follow daylight", "Themes allow contrast", "Themes preview swatches"] },
59 { theme: "webhooks", titles: ["Webhooks retry deliveries", "Webhooks sign payloads", "Webhooks replay events"] },
60 { theme: "digest", titles: ["Digest groups mentions", "Digest honours quiet hours", "Digest links threads"] },
61 ];
62
4463 export interface OwnerLook {
4564 readonly page: string;
4665 readonly kind: LookKind;
@@ -53,6 +72,7 @@ export const PREVIEW_OWNER_LOOKS: readonly OwnerLook[] = [
5372 PREVIEW_REPO_HOME,
5473 PREVIEW_PULL_PAGE,
5574 PREVIEW_ITERATIONS_PAGE,
75 PREVIEW_HELD_PAGE,
5676 ].flatMap((page) => (["desktop", "phone"] as const).map((kind) => ({ page, kind })));
5777
5878 /**
src/spawn/preview-seed.ts +34
@@ -24,6 +24,9 @@ import { openIssue, setIssueState } from "../actions/issues.ts";
2424 import { pinBuild } from "../actions/builds.ts";
2525 import { openPull } from "../actions/pulls.ts";
2626 import { commitToBranch, createRepo, forkRepo } from "../actions/repos.ts";
27 import { reviewGate } from "../actions/gate_review.ts";
28 import { linkIssue } from "../actions/dependencies.ts";
29 import { THEMES_AT_REST } from "../web/pages/gate.ts";
2730 import { iterationsOf } from "../core/iterations.ts";
2831 import { turnValveFor } from "../actions/valves.ts";
2932 import { insertBet } from "../core/bets.ts";
@@ -40,6 +43,8 @@ import { PREVIEW_CONFIG_FILES } from "./preview-config.ts";
4043 import {
4144 PREVIEW_ADMIN,
4245 PREVIEW_FORKS,
46 PREVIEW_HELD_PAGE,
47 PREVIEW_HELD_THEMES,
4348 PREVIEW_ITERATIONS_PAGE,
4449 PREVIEW_PULL,
4550 PREVIEW_PULL_PAGE,
@@ -49,6 +54,8 @@ import {
4954 export {
5055 PREVIEW_ADMIN,
5156 PREVIEW_FORKS,
57 PREVIEW_HELD_PAGE,
58 PREVIEW_HELD_THEMES,
5259 PREVIEW_ITERATIONS_PAGE,
5360 PREVIEW_OWNER_LOOKS,
5461 PREVIEW_PULL,
@@ -213,6 +220,8 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> {
213220 if (!bets.ok) return bets;
214221 const family = await seedForkFamily(app, admin, repo.value, start.value.commitOid);
215222 if (!family.ok) return family;
223 const gated = seedLaterGate(app, admin, repo.value);
224 if (!gated.ok) return gated;
216225 const valve = turnValveFor(app, admin, `${PREVIEW_ADMIN}/${PREVIEW_REPO}`, 60);
217226 if (!valve.ok) return err(`the valve: ${valve.error}`);
218227
@@ -401,3 +410,28 @@ async function seedForkFamily(app: App, admin: User, repo: Repo, first: string):
401410 }
402411 return ok();
403412 }
413
414 /**
415 * A Later gate holding issues across more themes than the held page draws at
416 * rest, so its fold line shows. Made last, so the numbers the other pages
417 * name stay where they are. Refused when the page would be a 404 or would
418 * not have a theme more than it draws at rest.
419 */
420 function seedLaterGate(app: App, admin: User, repo: Repo): Result<void, string> {
421 const gate = openIssue(app, admin, repo, {
422 title: "Later gate: nothing behind this starts until the owner opens it",
423 body: "Held for later.",
424 });
425 if (!gate.ok) return err(`the Later gate: ${String(gate.error)}`);
426 for (const { titles } of PREVIEW_HELD_THEMES)
427 for (const title of titles) {
428 const made = openIssue(app, admin, repo, { title, body: "Held behind the gate." });
429 if (!made.ok) return err(`a held issue: ${String(made.error)}`);
430 const linked = linkIssue(app, admin, repo, made.value, gate.value);
431 if (!linked.ok) return err(`holding "${title}": ${JSON.stringify(linked.error)}`);
432 }
433 const themes = reviewGate(app, repo)?.themes.length ?? 0;
434 if (themes <= THEMES_AT_REST)
435 return err(`${PREVIEW_HELD_PAGE} shows ${String(themes)} themes, not more than the ${String(THEMES_AT_REST)} drawn at rest`);
436 return ok();
437 }
outdated

reviewGate is called on a repo fetched again by name, but repo is already in scope. Use it, and the 404 branch can go. If it stays, the 404 guard is untested. Add a test, or drop the branch.

test/looks-owner.test.ts +2 −2
@@ -45,7 +45,7 @@ import {
4545 seedPreview,
4646 } from "../src/spawn/preview-seed.ts";
4747 import { PREVIEW_CONFIG_FILES } from "../src/spawn/preview-config.ts";
48 import { ownerLooksFor, PREVIEW_FORKS, PREVIEW_ITERATIONS_PAGE } from "../src/spawn/preview-looks.ts";
48 import { ownerLooksFor, PREVIEW_FORKS, PREVIEW_HELD_THEMES, PREVIEW_ITERATIONS_PAGE } from "../src/spawn/preview-looks.ts";
4949 import { iterationsOf } from "../src/core/iterations.ts";
5050 import { localSource } from "../src/view/local.ts";
5151 import { isFormConfig, parseConfig } from "../src/web/render/config-format.ts";
@@ -124,7 +124,7 @@ describe("the preview seed", () => {
124124 const issues = app.db
125125 .query<{ n: number }, [string]>("SELECT COUNT(*) AS n FROM issue WHERE repo_id = ?1 AND kind = 'issue'")
126126 .get(repo.id);
127 expect(issues?.n).toBe(11);
127 expect(issues?.n).toBe(11 + 1 + PREVIEW_HELD_THEMES.flatMap((t) => t.titles).length);
128128 expect(listPulls(app.db, repo.id).length).toBe(1);
129129 expect(listRuns(app.db, repo.id, { kind: "agent" }).every((r) => r.state === "ok")).toBe(true);
130130 const pipes = readPipes(app.db, admin.id);
test/preview-looks.test.ts added +58
@@ -0,0 +1,58 @@
1 /**
2 * The Later gate's proposal in every preview (#9212): the owner looks include
3 * the held page at desk and phone, and the seeded page draws exactly
4 * THEMES_AT_REST theme lines and one fold line at rest.
5 */
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { getRepo } from "../src/core/repos.ts";
12 import { PREVIEW_HELD_PAGE, PREVIEW_HELD_THEMES, ownerLooksFor, PREVIEW_ADMIN, PREVIEW_REPO } from "../src/spawn/preview-looks.ts";
13 import { seedPreview } from "../src/spawn/preview-seed.ts";
14 import { THEMES_AT_REST } from "../src/web/pages/gate.ts";
15 import { reviewGate } from "../src/actions/gate_review.ts";
16 import { serve, type Servers } from "../src/http/server.ts";
17
18 test("the owner looks for a forge preview include the held page at both widths", () => {
19 const looks = ownerLooksFor("trevato/forge", { ownerName: "trevato", name: "forge" }, "preview");
20 expect(PREVIEW_HELD_PAGE).toBe("/preview-admin/sample/held");
21 for (const kind of ["desktop", "phone"] as const)
22 expect(looks).toContainEqual({ page: PREVIEW_HELD_PAGE, kind });
23 expect(PREVIEW_HELD_THEMES.length).toBeGreaterThan(THEMES_AT_REST);
24 });
25
26 describe("the seeded held page", () => {
27 let dir: string;
28 let app: App;
29 let servers: Servers;
30 beforeAll(async () => {
31 dir = await mkdtemp(join(tmpdir(), "forge-preview-held-"));
32 app = createApp({ dataDir: dir, port: 0 });
33 const seeded = await seedPreview(app, { FORGE_APP_ENV: "preview" });
34 if (!seeded.ok) throw new Error(seeded.error);
35 servers = await serve(app);
36 }, 60_000);
37 afterAll(async () => {
38 await servers.stop();
39 await rm(dir, { recursive: true, force: true });
40 });
41
42 test("draws THEMES_AT_REST theme lines and one fold line at rest", async () => {
43 const repo = getRepo(app.db, PREVIEW_ADMIN, PREVIEW_REPO)!;
44 const review = reviewGate(app, repo)!;
45 expect(review.themes.length).toBe(PREVIEW_HELD_THEMES.length);
46 expect(review.themes.map((t) => t.name)).not.toContain("Other");
47 const res = await fetch(`${servers.url}${PREVIEW_HELD_PAGE}`);
48 expect(res.status).toBe(200);
49 const html = await res.text();
50 const fold = `${PREVIEW_HELD_THEMES.length - THEMES_AT_REST} more themes`;
51 expect(html.split(fold).length - 1).toBe(1);
52 // Outside the fold: one disclosure per theme at rest.
53 const rest = html.slice(0, html.indexOf(fold));
54 const shown = review.themes.slice(0, THEMES_AT_REST);
55 for (const t of shown) expect(rest).toContain(t.name);
56 for (const t of review.themes.slice(THEMES_AT_REST)) expect(rest).not.toContain(`${t.name} <`);
57 });
58 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs