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

Every preview of the forge shows the seeded pull page, repository head, home with pins and /flow, looked at as the owner #8875

Merged opened this

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

Merged

scout/forge:scout/8865-every-preview-of-the-forge-shows-the-see → main 2 commits fork

  • Merged. scout/8865-every-preview-of-the-forge-shows-the-see was merged into main as e7dac0b.

Review

Sign in to review this change.
6 files changed +101 −8
docs/design/areas/pull.md +2
@@ -1,5 +1,7 @@
11 # Pull
22
3 Every forge pull's preview carries desk and phone looks of this page as the owner, over the preview seed's `/preview-admin/sample/pulls/3` (#8865).
4
35 A pull is one change waiting on a person: read it, see its check, ship it or say what is off (#8773).
46
57 At rest: the state is a dot (`.dot`, `role=img`, one `aria-label`, no `title`), with the word written beside it once the pull is settled. The check is one muted line. The decision is a plain block holding the page's one primary action (Ship); Comment steps down while it is there. The path line carries "Next: …" in the same paragraph. The where strip is left to the repository's home.
docs/design/areas/repo-head.md +2
@@ -1,5 +1,7 @@
11 # Repository head
22
3 Every forge pull's preview carries desk and phone looks of this page as the owner, over the preview seed's `/preview-admin/sample` (#8865).
4
35 The head of a repository page names the repository and what you can do with it. `scopeHeader()` in `src/web/chrome.ts` draws it; the rules live in `src/web/style/content.ts`.
46
57 - `.repo-title` — the head's `owner / name` line: the owner muted, the slash faint, the name in full weight. It is an `h1` on a repository's own pages and a `div` where the page has its own `h1`.
forge.json +2 −2
@@ -3,8 +3,8 @@
33 "api.anthropic.com"
44 ],
55 "app": {
6 "looks": ["/", "/forges", "/family", "/front", "/explore", "/board", "/boards", "/notes", "/feed", "/review", "/search", "/new", "/audit", "/releases", "/changelog", "/apps", "/repos", "/map", "/system", "/talk", "/contribute", "/flow", "/resilience", "/settings"],
7 "looksAsOwner": ["/", "/flow", "/talk"],
6 "looks": ["/", "/forges", "/family", "/front", "/explore", "/board", "/notes", "/feed", "/review", "/search", "/new", "/audit", "/releases", "/apps", "/repos", "/map", "/system", "/talk", "/contribute", "/flow", "/resilience", "/settings", "/preview-admin/sample", "/preview-admin/sample/pulls/3"],
7 "looksAsOwner": ["/", "/flow", "/talk", "/preview-admin/sample", "/preview-admin/sample/pulls/3"],
88 "run": "bun run src/main.ts",
99 "install": "",
1010 "health": "/healthz",
src/spawn/preview-seed.ts +7
@@ -30,6 +30,12 @@ import { err, ok, type Result } from "../kernel/result.ts";
3030 export const PREVIEW_ADMIN = "preview-admin";
3131 /** The repository the admin's pages are about. */
3232 export const PREVIEW_REPO = "sample";
33 /**
34 * The seeded pull's number: two issues come first and share its count.
35 * forge.json `app.looksAsOwner` names its page, so the seed refuses any
36 * other number rather than leave that look on a 404.
37 */
38 export const PREVIEW_PULL = 3;
3339 /** The board whose panels are pinned to home. */
3440 const PREVIEW_BOARD = "preview";
3541 const SEEDED = "preview_seeded";
@@ -122,6 +128,7 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> {
122128 headBranch: "greeting",
123129 });
124130 if (!pull.ok) return err(`the pull: ${String(pull.error)}`);
131 if (pull.value.number !== PREVIEW_PULL) return err(`the pull is #${String(pull.value.number)}, not #${String(PREVIEW_PULL)}`);
125132
126133 // A flow week: agent runs that started and spent this week, and a valve
127134 // turned part way, so /flow has a pipe to draw and one to turn.
test/forge-looks.test.ts +18 −4
@@ -52,6 +52,8 @@ const NOT_A_PAGE: Record<string, string> = {
5252 "/apple-touch-icon.png": "an icon, not a page",
5353 "/apple-touch-icon-precomposed.png": "an icon, not a page",
5454 "/events": "a live event stream that never ends",
55 "/boards": "the list of boards; /board is pictured, and the slot went to the preview seed's repository as its owner (#8865); covered by test/boards-page.test.ts",
56 "/changelog": "the merged changes in words; /releases is pictured, and the slot went to the preview seed's pull as its owner (#8865); covered by test/changelog.test.ts",
5557 "/migrate": "an import form, nothing to picture until a job runs; the look slot went to /repos",
5658 };
5759
@@ -87,8 +89,11 @@ const topLevel = () =>
8789 .patterns()
8890 .filter((p) => p === "/" || /^\/[^/:*]+$/.test(p));
8991
90 const declared = (): string[] =>
91 JSON.parse(readFileSync(join(import.meta.dir, "..", "forge.json"), "utf8")).app.looks;
92 const manifest = (): { looks: string[]; looksAsOwner: string[] } =>
93 JSON.parse(readFileSync(join(import.meta.dir, "..", "forge.json"), "utf8")).app;
94 const isTopLevel = (p: string) => p === "/" || /^\/[^/]+$/.test(p);
95 /** The top-level pages; a deeper one is the preview seed's, taken as the owner (test/looks-owner.test.ts). */
96 const declared = (): string[] => manifest().looks.filter(isTopLevel);
9297
9398 const get = (path: string) =>
9499 fetch(`${servers.url}${path}`, {
@@ -114,6 +119,15 @@ test("every listed path answers 200 to an admin", async () => {
114119 });
115120
116121 test("app.looks stays within 24 paths, each once", () => {
117 expect(declared().length).toBeLessThanOrEqual(24);
118 expect(new Set(declared()).size).toBe(declared().length);
122 const { looks } = manifest();
123 expect(looks.length).toBeLessThanOrEqual(24);
124 expect(new Set(looks).size).toBe(looks.length);
125 });
126
127 test("a look below the top level is the preview seed's, taken as the owner", () => {
128 const { looks, looksAsOwner } = manifest();
129 for (const p of looks.filter((p) => !isTopLevel(p))) {
130 expect(p.startsWith("/preview-admin/sample")).toBe(true);
131 expect(looksAsOwner).toContain(p);
132 }
119133 });
test/looks-owner.test.ts +70 −2
@@ -8,7 +8,7 @@
88
99 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
1010 import { createHmac } from "node:crypto";
11 import { writeFileSync } from "node:fs";
11 import { readFileSync, writeFileSync } from "node:fs";
1212 import { mkdtemp, rm } from "node:fs/promises";
1313 import { tmpdir } from "node:os";
1414 import { join } from "node:path";
@@ -32,7 +32,7 @@ import { ok } from "../src/kernel/result.ts";
3232 import type { Capture, LookInput } from "../src/looks/capture.ts";
3333 import { startLooks, type LooksHandle } from "../src/looks/service.ts";
3434 import { previewIdentityKey } from "../src/spawn/preview-owner.ts";
35 import { PREVIEW_ADMIN, PREVIEW_REPO, seedPreview } from "../src/spawn/preview-seed.ts";
35 import { PREVIEW_ADMIN, PREVIEW_PULL, PREVIEW_REPO, seedPreview } from "../src/spawn/preview-seed.ts";
3636 import { quietLogs } from "./helpers/globals.ts";
3737
3838 quietLogs();
@@ -184,6 +184,74 @@ describe("a preview hears its parent's admin as the preview admin", () => {
184184 }, 30_000);
185185 });
186186
187 describe("the forge's own owner looks, over the preview seed", () => {
188 // Each page forge.json takes as the owner, and what shows it is not blank (#8865).
189 const repoHome = `/${PREVIEW_ADMIN}/${PREVIEW_REPO}`;
190 const pullPage = `${repoHome}/pulls/${String(PREVIEW_PULL)}`;
191 const KEY = "the-parents-identity-key";
192 let dataDir: string;
193 let app: App;
194 let s: Servers;
195
196 beforeAll(async () => {
197 dataDir = await mkdtemp(join(tmpdir(), "forge-owner-looks-"));
198 app = createApp({ dataDir, port: 0, previewIdentityKey: KEY });
199 const seeded = await seedPreview(app, { FORGE_APP_ENV: "preview" });
200 if (!seeded.ok) throw new Error(seeded.error);
201 s = await serve(app);
202 }, 30_000);
203 afterAll(async () => {
204 s.stop();
205 app.close();
206 await rm(dataDir, { recursive: true, force: true });
207 });
208
209 const asOwner = async (path: string) => {
210 const said = `u1|trevato|admin|${String(Date.now() + 60_000)}`;
211 const res = await fetch(`${s.url}${path}`, {
212 headers: { "x-forge-identity": said, "x-forge-signature": createHmac("sha256", KEY).update(said).digest("base64url") },
213 redirect: "manual",
214 });
215 expect(res.status).toBe(200);
216 return res.text();
217 };
218
219 test("forge.json takes home, /flow, the seeded repository and its pull as the owner, desk and phone", () => {
220 const manifest = parseManifest(readFileSync(join(import.meta.dir, "..", "forge.json"), "utf8"), undefined);
221 if (!manifest.ok) throw new Error(JSON.stringify(manifest.error));
222 for (const page of ["/", "/flow", repoHome, pullPage]) {
223 expect(manifest.value.looks).toContain(page);
224 expect(manifest.value.looksAsOwner).toContain(page);
225 }
226 const repo = getRepo(app.db, PREVIEW_ADMIN, PREVIEW_REPO)!;
227 expect(listPulls(app.db, repo.id).map((p) => p.number)).toEqual([PREVIEW_PULL]);
228 });
229
230 test("home holds the pins, and the fragment it fills them from draws them", async () => {
231 expect(await asOwner("/")).toContain('data-refresh="/live/home/panels"');
232 const pins = await asOwner("/live/home/panels");
233 expect(pins).toContain('aria-label="Pinned panels"');
234 expect(pins).toContain("Runs by kind");
235 });
236
237 test("/flow draws a pipe", async () => {
238 expect(await asOwner("/flow")).toContain('class="pipe ');
239 });
240
241 test("the repository's home carries the head's title", async () => {
242 const page = await asOwner(repoHome);
243 expect(page).toContain('<h1 class="repo-title">');
244 expect(page).toContain(`href="${repoHome}">${PREVIEW_REPO}`);
245 });
246
247 test("the pull's page carries the Ship decision", async () => {
248 const page = await asOwner(pullPage);
249 const decide = page.indexOf('id="decide"');
250 expect(decide).toBeGreaterThan(-1);
251 expect(page.slice(decide, decide + 2000)).toMatch(/data-variant="primary"[^>]*>\s*Ship\s*</);
252 });
253 });
254
187255 describe("looks of a public app", () => {
188256 let dataDir: string;
189257 let app: App;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs