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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9212-every-preview-of-the-forge-pictures-the → main 2 commits fork 2 threads · 2 unresolved
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 | ||
| 13 | 13 | there that `app.looks` does not name is a manifest error. The forge names |
| 14 | 14 | `/`, `/flow` and `/talk`. |
| 15 | 15 | |
| 16 | **Four looks every forge preview takes.** Whatever `forge.json` lists, a | |
| 16 | **Six pages every forge preview takes.** Whatever `forge.json` lists, a | |
| 17 | 17 | preview of the forge's own repository (`FORGE_REPO`) is pictured as its |
| 18 | 18 | 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 | |
| 20 | 22 | `PREVIEW_OWNER_LOOKS` in `src/spawn/preview-looks.ts`, which the looks |
| 21 | 23 | service adds to the manifest's pages (#8974). Production, and every other |
| 22 | 24 | app, take only what their manifest lists. |
| @@ -26,7 +28,10 @@ app, take only what their manifest lists. | ||
| 26 | 28 | a small world for it once (`src/spawn/preview-seed.ts`): a public repository |
| 27 | 29 | `preview-admin/sample` with two issues and an open pull, three agent runs |
| 28 | 30 | 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 | |
| 30 | 35 | `x-forge-identity` is signed under `FORGE_APP_IDENTITY_KEY` and names an |
| 31 | 36 | admin is that admin (`src/spawn/preview-owner.ts`) — the parent's looks, and |
| 32 | 37 | 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 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * The looks every preview of the forge takes as its owner (#8974): home |
| 3 | 3 | * 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 | |
| 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. | |
| 8 | 8 | * |
| 9 | 9 | * Plain data and no imports beyond a type: the seed (`preview-seed.ts`) |
| 10 | 10 | * 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 | ||
| 41 | 41 | { owner: "preview-dee", of: "preview-ada" }, |
| 42 | 42 | ]; |
| 43 | 43 | |
| 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 | ||
| 44 | 63 | export interface OwnerLook { |
| 45 | 64 | readonly page: string; |
| 46 | 65 | readonly kind: LookKind; |
| @@ -53,6 +72,7 @@ export const PREVIEW_OWNER_LOOKS: readonly OwnerLook[] = [ | ||
| 53 | 72 | PREVIEW_REPO_HOME, |
| 54 | 73 | PREVIEW_PULL_PAGE, |
| 55 | 74 | PREVIEW_ITERATIONS_PAGE, |
| 75 | PREVIEW_HELD_PAGE, | |
| 56 | 76 | ].flatMap((page) => (["desktop", "phone"] as const).map((kind) => ({ page, kind }))); |
| 57 | 77 | |
| 58 | 78 | /** |
src/spawn/preview-seed.ts +34
| @@ -24,6 +24,9 @@ import { openIssue, setIssueState } from "../actions/issues.ts"; | ||
| 24 | 24 | import { pinBuild } from "../actions/builds.ts"; |
| 25 | 25 | import { openPull } from "../actions/pulls.ts"; |
| 26 | 26 | 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"; | |
| 27 | 30 | import { iterationsOf } from "../core/iterations.ts"; |
| 28 | 31 | import { turnValveFor } from "../actions/valves.ts"; |
| 29 | 32 | import { insertBet } from "../core/bets.ts"; |
| @@ -40,6 +43,8 @@ import { PREVIEW_CONFIG_FILES } from "./preview-config.ts"; | ||
| 40 | 43 | import { |
| 41 | 44 | PREVIEW_ADMIN, |
| 42 | 45 | PREVIEW_FORKS, |
| 46 | PREVIEW_HELD_PAGE, | |
| 47 | PREVIEW_HELD_THEMES, | |
| 43 | 48 | PREVIEW_ITERATIONS_PAGE, |
| 44 | 49 | PREVIEW_PULL, |
| 45 | 50 | PREVIEW_PULL_PAGE, |
| @@ -49,6 +54,8 @@ import { | ||
| 49 | 54 | export { |
| 50 | 55 | PREVIEW_ADMIN, |
| 51 | 56 | PREVIEW_FORKS, |
| 57 | PREVIEW_HELD_PAGE, | |
| 58 | PREVIEW_HELD_THEMES, | |
| 52 | 59 | PREVIEW_ITERATIONS_PAGE, |
| 53 | 60 | PREVIEW_OWNER_LOOKS, |
| 54 | 61 | PREVIEW_PULL, |
| @@ -213,6 +220,8 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> { | ||
| 213 | 220 | if (!bets.ok) return bets; |
| 214 | 221 | const family = await seedForkFamily(app, admin, repo.value, start.value.commitOid); |
| 215 | 222 | if (!family.ok) return family; |
| 223 | const gated = seedLaterGate(app, admin, repo.value); | |
| 224 | if (!gated.ok) return gated; | |
| 216 | 225 | const valve = turnValveFor(app, admin, `${PREVIEW_ADMIN}/${PREVIEW_REPO}`, 60); |
| 217 | 226 | if (!valve.ok) return err(`the valve: ${valve.error}`); |
| 218 | 227 | |
| @@ -401,3 +410,28 @@ async function seedForkFamily(app: App, admin: User, repo: Repo, first: string): | ||
| 401 | 410 | } |
| 402 | 411 | return ok(); |
| 403 | 412 | } |
| 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
|
test/looks-owner.test.ts +2 −2
| @@ -45,7 +45,7 @@ import { | ||
| 45 | 45 | seedPreview, |
| 46 | 46 | } from "../src/spawn/preview-seed.ts"; |
| 47 | 47 | 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"; | |
| 49 | 49 | import { iterationsOf } from "../src/core/iterations.ts"; |
| 50 | 50 | import { localSource } from "../src/view/local.ts"; |
| 51 | 51 | import { isFormConfig, parseConfig } from "../src/web/render/config-format.ts"; |
| @@ -124,7 +124,7 @@ describe("the preview seed", () => { | ||
| 124 | 124 | const issues = app.db |
| 125 | 125 | .query<{ n: number }, [string]>("SELECT COUNT(*) AS n FROM issue WHERE repo_id = ?1 AND kind = 'issue'") |
| 126 | 126 | .get(repo.id); |
| 127 | expect(issues?.n).toBe(11); | |
| 127 | expect(issues?.n).toBe(11 + 1 + PREVIEW_HELD_THEMES.flatMap((t) => t.titles).length); | |
| 128 | 128 | expect(listPulls(app.db, repo.id).length).toBe(1); |
| 129 | 129 | expect(listRuns(app.db, repo.id, { kind: "agent" }).every((r) => r.state === "ok")).toBe(true); |
| 130 | 130 | 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 | }); |
new above
offline
This comment line is now far past the file wrap width. Re-wrap it.