Public: anyone can read this; the crew can change it.
Settings › Capacity answers a warm request under 50 ms and works with JavaScript off, and a test holds both #9730
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9725-settings-capacity-answers-a-warm-request → main 2 commits fork
2 files
changed +133
−0
test/capacity-nojs.test.ts added +101
| @@ -0,0 +1,101 @@ | ||
| 1 | /** | |
| 2 | * Settings › Capacity works with JavaScript off (#9725): no script carries | |
| 3 | * its function, and every change on it is a plain POST form that the server | |
| 4 | * holds to a same-origin check. | |
| 5 | */ | |
| 6 | import { afterAll, beforeAll, 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 { serve, type Servers } from "../src/http/server.ts"; | |
| 12 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 13 | ||
| 14 | let dataDir = ""; | |
| 15 | let app: App, servers: Servers, bearer = ""; | |
| 16 | ||
| 17 | beforeAll(async () => { | |
| 18 | dataDir = await mkdtemp(join(tmpdir(), "forge-capnojs-")); | |
| 19 | app = createApp({ dataDir, port: 0 }); | |
| 20 | servers = await serve(app); | |
| 21 | const admin = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" }); | |
| 22 | if (!admin.ok) throw new Error(admin.error); | |
| 23 | const minted = await fetch(`${servers.url}/api/v1/tokens`, { | |
| 24 | method: "POST", | |
| 25 | headers: { "content-type": "application/json" }, | |
| 26 | body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }), | |
| 27 | }); | |
| 28 | bearer = ((await minted.json()) as { token: string }).token; | |
| 29 | }); | |
| 30 | ||
| 31 | afterAll(async () => { | |
| 32 | servers.stop(); | |
| 33 | app.close(); | |
| 34 | await rm(dataDir, { recursive: true, force: true }); | |
| 35 | }); | |
| 36 | ||
| 37 | const page = async () => { | |
| 38 | const res = await fetch(`${servers.url}/settings/capacity`, { headers: { authorization: `Bearer ${bearer}` } }); | |
| 39 | expect(res.status).toBe(200); | |
| 40 | return res.text(); | |
| 41 | }; | |
| 42 | ||
| 43 | /** | |
| 44 | * The page's own content: from `.settings-main` to the end of <main>. The | |
| 45 | * shared settings nav before it (its `js-only` filter) is chrome, held by | |
| 46 | * its own tests; this page must not lean on it. | |
| 47 | */ | |
| 48 | const main = (html: string) => /<div class="settings-main"[\s\S]*<\/main>/.exec(html)?.[0] ?? ""; | |
| 49 | ||
| 50 | test("the page's content carries no script and no inline handler", async () => { | |
| 51 | const body = main(await page()); | |
| 52 | expect(body).not.toBe(""); | |
| 53 | expect(body).not.toMatch(/<script\b/i); | |
| 54 | expect(body).not.toMatch(/\son[a-z]+\s*=/i); | |
| 55 | expect(body).not.toMatch(/javascript:/i); | |
| 56 | }); | |
| 57 | ||
| 58 | test("every change is a plain POST form to /settings/capacity, crew size and CPUs among them; self-swap is read, not changed", async () => { | |
| 59 | const body = main(await page()); | |
| 60 | const forms = [...body.matchAll(/<form\b([^>]*)>([\s\S]*?)<\/form>/g)]; | |
| 61 | expect(forms.length).toBeGreaterThan(0); | |
| 62 | for (const [, attrs] of forms) { | |
| 63 | expect(attrs).toMatch(/method="post"/); | |
| 64 | expect(attrs).toMatch(/action="\/settings\/capacity"/); | |
| 65 | } | |
| 66 | const keys = forms.map(([, , inner]) => /name="key" value="([^"]+)"/.exec(inner!)?.[1]); | |
| 67 | // Crew size, and the two settings sized from the pod's CPUs. | |
| 68 | expect(keys).toEqual(expect.arrayContaining(["crew.concurrency", "checks.concurrency", "checks.shards"])); | |
| 69 | // Nothing outside a form asks to be pressed: a button out of a form needs a script. | |
| 70 | const outside = body.replace(/<form\b[\s\S]*?<\/form>/g, ""); | |
| 71 | expect(outside).not.toMatch(/<(button|input|select|textarea)\b/); | |
| 72 | expect(outside).toContain('id="self-swap"'); | |
| 73 | }); | |
| 74 | ||
| 75 | test("a change from another origin is refused and changes nothing", async () => { | |
| 76 | const before = await page(); | |
| 77 | const form = /<form\b[^>]*>([\s\S]*?)<\/form>/.exec(main(before))![1]!; | |
| 78 | const key = /name="key" value="([^"]+)"/.exec(form)![1]!; | |
| 79 | const res = await fetch(`${servers.url}/settings/capacity`, { | |
| 80 | method: "POST", | |
| 81 | redirect: "manual", | |
| 82 | headers: { authorization: `Bearer ${bearer}`, origin: "https://elsewhere.example", "content-type": "application/x-www-form-urlencoded" }, | |
| 83 | body: new URLSearchParams({ key, reset: "1" }), | |
| 84 | }); | |
| 85 | expect(res.status).toBe(403); | |
| 86 | expect(main(await page())).not.toContain("→"); | |
| 87 | }); | |
| 88 | ||
| 89 | test("the same change from this origin lands, with no script involved", async () => { | |
| 90 | const form = /<form\b[^>]*>([\s\S]*?)<\/form>/.exec(main(await page()))![1]!; | |
| 91 | const key = /name="key" value="([^"]+)"/.exec(form)![1]!; | |
| 92 | const value = /<input[^>]*type="number"[^>]*min="(\d+)"/.exec(form)?.[1]; | |
| 93 | const res = await fetch(`${servers.url}/settings/capacity`, { | |
| 94 | method: "POST", | |
| 95 | redirect: "manual", | |
| 96 | headers: { authorization: `Bearer ${bearer}`, origin: servers.url, "content-type": "application/x-www-form-urlencoded" }, | |
| 97 | body: new URLSearchParams({ key, value: value ?? "" }), | |
| 98 | }); | |
| 99 | expect(res.status).toBe(303); | |
| 100 | expect(res.headers.get("location")).toContain("/settings/capacity"); | |
| 101 | }); |
test/capacity-timing.test.ts +32
| @@ -6,6 +6,7 @@ import { join } from "node:path"; | ||
| 6 | 6 | import { createApp, type App } from "../src/app.ts"; |
| 7 | 7 | import { serve, type Servers } from "../src/http/server.ts"; |
| 8 | 8 | import { seedAdmin } from "./helpers/admin.ts"; |
| 9 | import { expectWork, requestStatements } from "./helpers/work.ts"; | |
| 9 | 10 | |
| 10 | 11 | let dataDir = ""; |
| 11 | 12 | let app: App, servers: Servers, bearer = ""; |
| @@ -63,3 +64,34 @@ test("render does not include the time already named as errors", async () => { | ||
| 63 | 64 | errors.recent = real; |
| 64 | 65 | } |
| 65 | 66 | }); |
| 67 | ||
| 68 | // A warm answer costs a fixed, small number of SQL statements (#9725): work is | |
| 69 | // counted, not timed (#7824), so a regression — an N+1 over the settings or | |
| 70 | // the change history, a lost cache — fails every run, not a lucky one. | |
| 71 | // 58 warm today, 59 once a setting is away from its default; the rest is margin. | |
| 72 | const WARM_STATEMENTS = 64; | |
| 73 | ||
| 74 | test("a warm capacity page costs a bounded number of statements, however many changes it lists", async () => { | |
| 75 | const url = `${servers.url}/settings/capacity`; | |
| 76 | const init = { headers: { authorization: `Bearer ${bearer}` } }; | |
| 77 | await (await fetch(url, init)).text(); | |
| 78 | const warm = await requestStatements(app.db, url, init); | |
| 79 | expectWork(warm, { max: WARM_STATEMENTS }); | |
| 80 | const change = async (n: number) => { | |
| 81 | const res = await fetch(url, { | |
| 82 | method: "POST", | |
| 83 | redirect: "manual", | |
| 84 | headers: { ...init.headers, origin: servers.url, "content-type": "application/x-www-form-urlencoded" }, | |
| 85 | body: new URLSearchParams({ key: "queue.batch", value: String(n) }), | |
| 86 | }); | |
| 87 | expect(res.status).toBe(303); | |
| 88 | }; | |
| 89 | // A setting away from its default reads who changed it; after that, more | |
| 90 | // history costs nothing more: twelve changes and twenty-four cost the same. | |
| 91 | for (let n = 1; n <= 12; n++) await change((n % 7) + 1); | |
| 92 | const some = await requestStatements(app.db, url, init); | |
| 93 | for (let n = 13; n <= 24; n++) await change((n % 7) + 1); | |
| 94 | const more = await requestStatements(app.db, url, init); | |
| 95 | expectWork(some, { max: WARM_STATEMENTS }); | |
| 96 | expect(more).toBe(some); | |
| 97 | }); |