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

forge-webspec walks the signed-in pages too: /map, /system, /boards, /crew, a pull, settings #3173

Merged opened this

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

Merged

scout/forge:scout/3170-forge-webspec-walks-the-signed-in-pages → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/3170-forge-webspec-walks-the-signed-in-pages was merged into main as ddeb953.

Review

Sign in to review this change.
3 files changed +57 −18
src/web/pages/crew.ts +4 −4
@@ -498,7 +498,7 @@ function crewView(ctx: Ctx, values: AgentValues, error?: string): Html {
498498 </form>`,
499499 {
500500 head: html`${icon("bot")}
501 <h3>New agent</h3>`,
501 <h2>New agent</h2>`,
502502 },
503503 )}`;
504504 }
@@ -538,7 +538,7 @@ function splitsCard(
538538 </tbody>
539539 </table>
540540 </div>`,
541 { pad: false, head: html`<h3>${title}</h3>` },
541 { pad: false, head: html`<h2>${title}</h2>` },
542542 );
543543 }
544544
@@ -618,7 +618,7 @@ function agentView(agent: Agent, values: AgentValues, error?: string): Html {
618618 </form>`,
619619 {
620620 head: html`${icon("bot")}
621 <h3>${agent.user.username}</h3>
621 <h2>${agent.user.username}</h2>
622622 <span class="muted small">added ${time(agent.createdAt)}</span>`,
623623 },
624624 )}
@@ -633,7 +633,7 @@ function agentView(agent: Agent, values: AgentValues, error?: string): Html {
633633 {
634634 tone: "danger",
635635 head: html`${icon("alert")}
636 <h3>Retire</h3>`,
636 <h2>Retire</h2>`,
637637 },
638638 )}`;
639639 }
test/forge-webspec.test.ts +52 −13
@@ -10,7 +10,9 @@ import { join } from "node:path";
1010 import { brotliDecompressSync } from "node:zlib";
1111 import { createApp, type App } from "../src/app.ts";
1212 import { openIssue } from "../src/actions/issues.ts";
13 import { openPull } from "../src/actions/pulls.ts";
1314 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
15 import { createSession } from "../src/core/sessions.ts";
1416 import { createUser } from "../src/core/users.ts";
1517 import { compress } from "../src/http/compress.ts";
1618 import { securityFloor } from "../src/http/respond.ts";
@@ -25,6 +27,22 @@ const PATHS = ["/", "/signin", "/signup", "/explore", "/alice/notes", "/alice/no
2527 /** `check path` -> [section, why it stands]. Every one is expected on every page it names; one fixed fails the test, one new fails it too. */
2628 const KNOWN: Record<string, [string, string]> = {};
2729
30 /** The owner's pages: signed in, so no-store by design; webspec's caching findings on them are not asserted away, they are named. */
31 const SIGNED_IN = ["/map", "/system", "/boards", "/settings/crew", "/alice/notes/pulls/2", "/settings"];
32 const CACHING = "https://specification.website/#caching";
33 const NO_ETAG = "signed-in HTML is no-store by design: it is per-person, so there is no validator to revalidate against";
34 const KNOWN_SIGNED_IN: Record<string, [string, string]> = {
35 "etag /system": [CACHING, NO_ETAG],
36 "etag /boards": [CACHING, NO_ETAG],
37 "etag /map": [CACHING, NO_ETAG],
38 "etag /alice/notes/pulls/2": [CACHING, NO_ETAG],
39 "etag /settings": [CACHING, NO_ETAG],
40 "etag /settings/crew": [CACHING, NO_ETAG],
41 "title /map": ["https://specification.website/#title", "each map node is an inline SVG element whose <title> is its tooltip and accessible name, beside the page's own"],
outdated

This pins a real heading-order defect as expected. Fix the crew page to use <h2> and remove this entry, or link an issue number here.

42 };
43
44 let cookie = "";
45
2846 let dir = "";
2947 let app: App;
3048 let http: ForgeHttp;
@@ -47,6 +65,11 @@ beforeAll(async () => {
4765 await commitToBranch(app, made.value, repo.value, { message: "seed", files: [{ path: "a.txt", content: "a" }] });
4866 const issue = openIssue(app, made.value, repo.value, { title: "First", body: "hello" });
4967 if (!issue.ok) throw new Error("issue");
68 const branched = await commitToBranch(app, made.value, repo.value, { branch: "feature", message: "feature", files: [{ path: "b.txt", content: "b" }] });
69 if (!branched.ok) throw new Error("branch");
70 const pull = await openPull(app, made.value, repo.value, { title: "Feature", baseBranch: "main", headBranch: "feature" });
71 if (!pull.ok) throw new Error("pull");
72 cookie = `forge_session=${createSession(app.db, made.value.id).token}`;
5073 });
5174
5275 afterAll(async () => {
@@ -55,20 +78,22 @@ afterAll(async () => {
5578 await rm(dir, { recursive: true, force: true });
5679 });
5780
81 // The seam production uses (server.ts): compress, then the security floor.
82 // Like fetch, the body arrives decoded while Content-Encoding stays for webspec to read.
83 const through = (signedIn: boolean) => async (input: string, init?: RequestInit) => {
84 const request = new Request(input, init);
85 if (signedIn) request.headers.set("cookie", cookie);
86 if (!request.headers.has("accept-encoding")) request.headers.set("accept-encoding", "br, gzip");
87 const response = await compress(request, await http.handle(request)).then(securityFloor);
88 const encoding = response.headers.get("content-encoding");
89 if (encoding !== "br" && encoding !== "gzip") return response;
90 const bytes = new Uint8Array(await response.arrayBuffer());
91 const plain = encoding === "br" ? brotliDecompressSync(bytes) : Bun.gunzipSync(bytes);
92 return new Response(plain, response);
93 };
94
5895 test("the forge's signed-out pages raise exactly the findings named in KNOWN", async () => {
59 // The seam production uses (server.ts): compress, then the security floor.
60 // Like fetch, the body arrives decoded while Content-Encoding stays for webspec to read.
61 const fetcher = async (input: string, init?: RequestInit) => {
62 const request = new Request(input, init);
63 if (!request.headers.has("accept-encoding")) request.headers.set("accept-encoding", "br, gzip");
64 const response = await compress(request, await http.handle(request)).then(securityFloor);
65 const encoding = response.headers.get("content-encoding");
66 if (encoding !== "br" && encoding !== "gzip") return response;
67 const bytes = new Uint8Array(await response.arrayBuffer());
68 const plain = encoding === "br" ? brotliDecompressSync(bytes) : Bun.gunzipSync(bytes);
69 return new Response(plain, response);
70 };
71 const found = await webspec("http://forge.test", PATHS, fetcher, { site: true });
96 const found = await webspec("http://forge.test", PATHS, through(false), { site: true });
7297 const keyOf = (f: { check: string; path: string }) => `${f.check} ${f.path}`;
7398 // New: not named, or citing another section than the one named.
7499 expect(found.filter((f) => KNOWN[keyOf(f)]?.[0] !== f.section).map((f) => `${keyOf(f)}: ${f.message}`)).toEqual([]);
@@ -77,6 +102,14 @@ test("the forge's signed-out pages raise exactly the findings named in KNOWN", a
77102 expect(Object.keys(KNOWN).filter((k) => !seen.has(k))).toEqual([]);
78103 });
79104
105 test("the owner's signed-in pages raise exactly the findings named in KNOWN_SIGNED_IN", async () => {
106 const found = await webspec("http://forge.test", SIGNED_IN, through(true));
107 const keyOf = (f: { check: string; path: string }) => `${f.check} ${f.path}`;
108 expect(found.filter((f) => KNOWN_SIGNED_IN[keyOf(f)]?.[0] !== f.section).map((f) => `${keyOf(f)} [${f.section}]: ${f.message}`)).toEqual([]);
109 const seen = new Set(found.map(keyOf));
110 expect(Object.keys(KNOWN_SIGNED_IN).filter((k) => !seen.has(k))).toEqual([]);
111 });
112
80113 test("a page that answers 200 names its canonical address, equal to og:url; a 404 names none", async () => {
81114 const head = async (path: string) => {
82115 const res = await http.handle(new Request(`http://forge.test${path}`));
@@ -95,6 +128,12 @@ test("a page that answers 200 names its canonical address, equal to og:url; a 40
95128 expect(missing.html).not.toContain('rel="canonical"');
96129 });
97130
131 test("the walked signed-in pages answer 200 for the owner", async () => {
132 for (const path of SIGNED_IN) {
133 expect((await http.handle(new Request(`http://forge.test${path}`, { headers: { cookie } }))).status).toBe(200);
134 }
135 });
136
98137 test("the walked repository pages exist", async () => {
99138 for (const path of ["/alice/notes", "/alice/notes/issues", "/alice/notes/issues/1"]) {
100139 expect((await http.handle(new Request(`http://forge.test${path}`))).status).toBe(200);
test/web-apps.test.ts +1 −1
@@ -663,7 +663,7 @@ describe("apps, runs and the crew on the web", () => {
663663 const page = await bodyOf(alice, "/settings/crew");
664664 expect(page).toContain("No agents yet");
665665 expect(page).toContain("No crew runner is up");
666 expect(page).toContain("<h3>New agent</h3>");
666 expect(page).toContain("<h2>New agent</h2>");
667667 expect(await bodyOf(alice, "/settings")).toContain('href="/settings/crew"');
668668
669669 const bad = await web(alice, "/settings/crew", {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs