Public: anyone can read this; the crew can change it.
A settings notice never draws as a red banner over a page: a test scans every page's first paint #10832
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10825-a-settings-notice-never-draws-as-a-red-b → main 2 commits fork 3 threads · 3 unresolved
1 file
changed +104
−0
test/no-setting-banner.test.ts added +104
| @@ -0,0 +1,104 @@ | ||
| 1 | /** | |
| 2 | * The owner's rule (#10825): a setting is one line where it belongs, never a | |
| 3 | * banner over the board. Every first paint an owner lands on is read with the | |
| 4 | * forge's settings in their unusual states (egress reported, every limit at | |
| 5 | * its floor, a flag declared and set); nothing that draws as an alert may sit | |
| 6 | * above the page's content. There is no allowlist: a healthy forge draws no | |
| 7 | * red above its content, and a real failure is not a setting. | |
| 8 | */ | |
| 9 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 10 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 11 | import { tmpdir } from "node:os"; | |
| 12 | import { join } from "node:path"; | |
| 13 | import { createApp, type App } from "../src/app.ts"; | |
| 14 | import { changeCapacity } from "../src/actions/capacity.ts"; | |
| 15 | import { makeFlags } from "../src/core/flags.ts"; | |
| 16 | import { createSession } from "../src/core/sessions.ts"; | |
| 17 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 18 | import { LIMITS, setLimit, type LimitKey } from "../src/settings/limits.ts"; | |
| 19 | import { EGRESS_REPORTED } from "../src/system/egress_line.ts"; | |
| 20 | import { useFlagRegistry } from "../src/web/pages/flags.ts"; | |
| 21 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 22 | import { quietLogs } from "./helpers/globals.ts"; | |
| 23 | ||
| 24 | quietLogs(); | |
| 25 | ||
| 26 | const REASON = "test: an unusual setting"; | |
| 27 | ||
| 28 | /** Any element that draws as an alert or a danger notice. */ | |
| 29 | const ALERT = /<(?!script|style|svg|path)[a-z0-9]+\b[^>]*(?:role="alert"|data-tone="danger"|class="[^"]*\b(?:danger|toast|error)\b[^"]*")[^>]*>/g; | |
| 30 | ||
| 31 | /** What sits above a page's content: the markup before `<main`, and main's own top up to the page's first heading. */ | |
| 32 | function aboveContent(html: string): string { | |
| 33 | const main = html.indexOf("<main"); | |
| 34 | // The content starts at the page's heading, or at the board's grid (the board has no heading). | |
| 35 | const starts = main < 0 ? [] : ["<h1", "board-grid"].map((m) => html.indexOf(m, main)).filter((i) => i >= 0); | |
| 36 | return html.slice(0, starts.length === 0 ? main : Math.min(...starts)); | |
| 37 | } | |
| 38 | ||
| 39 | let root = ""; | |
| 40 | let app: App; | |
| 41 | let servers: Servers; | |
| 42 | let cookie = ""; | |
outdated
Only egress is set. Add the limits and a flag the issue names so every setting that could draw a banner is in its unusual state. |
||
| 43 | ||
| 44 | beforeAll(async () => { | |
| 45 | root = mkdtempSync(join(tmpdir(), "forge-no-setting-banner-")); | |
| 46 | app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" }); | |
| 47 | const made = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "a-good-passphrase" }); | |
| 48 | if (!made.ok) throw new Error(made.error); | |
| 49 | cookie = `forge_session=${createSession(app.db, made.value.id).token}`; | |
| 50 | // Every limit at its floor: the tightest state a limit can be in. | |
| 51 | for (const key of Object.keys(LIMITS) as LimitKey[]) { | |
| 52 | const r = setLimit(app.db, key, LIMITS[key].min, made.value.id); | |
| 53 | if (!r.ok) throw new Error(`${key}: ${r.error.message}`); | |
outdated
Scan only what precedes |
||
| 54 | } | |
| 55 | const set = changeCapacity(app, made.value, "cells.egress", "report", REASON); | |
| 56 | if (!set.ok) throw new Error(set.error); | |
| 57 | // A flag, declared and set. | |
| 58 | const flags = makeFlags({ probe: { why: "a flag the scan sets", default: false, issue: 10825 } }); | |
| 59 | expect(flags.set(app.db, "probe", { forge: true }, made.value.id)).toBe(true); | |
| 60 | useFlagRegistry(flags); | |
| 61 | servers = await serve(app); | |
| 62 | }); | |
| 63 | afterAll(() => { | |
| 64 | useFlagRegistry(undefined); | |
| 65 | servers.stop(); | |
| 66 | app.close(); | |
| 67 | rmSync(root, { recursive: true, force: true }); | |
| 68 | }); | |
| 69 | ||
| 70 | const read = async (path: string): Promise<string> => { | |
| 71 | const res = await fetch(`${servers.url}${path}`, { headers: { cookie }, redirect: "manual" }); | |
| 72 | expect(res.status).toBe(200); | |
| 73 | return res.text(); | |
| 74 | }; | |
| 75 | ||
| 76 | for (const path of ["/", "/board", "/system", "/apps"]) { | |
| 77 | test(`${path} draws no alert above its content because of a setting`, async () => { | |
| 78 | const html = await read(path); | |
| 79 | expect(html).toContain("<main"); | |
| 80 | // The scan must reach real content: it ends after `<main`, not at it. | |
| 81 | expect(aboveContent(html).length).toBeGreaterThan(html.indexOf("<main")); | |
| 82 | expect(aboveContent(html).match(ALERT) ?? []).toEqual([]); | |
| 83 | }); | |
| 84 | } | |
| 85 | ||
| 86 | test("the settings are in their states, and each says so on its own page with its reason", async () => { | |
| 87 | const system = await read("/system"); | |
| 88 | expect(system).toContain(EGRESS_REPORTED); | |
| 89 | const capacity = await read("/settings/capacity"); | |
| 90 | expect(capacity).toContain(REASON); | |
| 91 | const limits = await read("/admin/limits"); | |
| 92 | expect(limits).toContain(String(LIMITS["crew.concurrency"].min)); | |
| 93 | const flags = await read("/settings/flags"); | |
| 94 | expect(flags).toContain("a flag the scan sets"); | |
| 95 | }); | |
| 96 | ||
| 97 | test("the detector sees a banner above the content and ignores an error below it", () => { | |
| 98 | const page = (top: string, low: string) => | |
| 99 | `<header>${top}</header><main id="main"><h1>Board</h1>${low}</main>`; | |
| 100 | const banner = '<div class="toast" data-tone="danger" role="alert">Egress is reported</div>'; | |
| 101 | expect(aboveContent(page(banner, "")).match(ALERT)?.length).toBe(1); | |
| 102 | expect(aboveContent(page("", banner.replace("<div", "<div id=\"x\""))).match(ALERT) ?? []).toEqual([]); | |
| 103 | expect(aboveContent(`<main id="main">${banner}<h1>Board</h1></main>`).match(ALERT)?.length).toBe(1); | |
| 104 | }); |
These ids appear nowhere in src/. Allow only red that really exists, or assert zero.