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

A healthy forge's home and board carry no alert banner: design-lint refuses a standing alert there, and a test counts zero #10705

Merged opened this

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

Merged

mason/forge:mason/10699-a-healthy-forge-s-home-and-board-carry-n → main 6 commits fork 4 threads · 4 unresolved

  • Merged. mason/10699-a-healthy-forge-s-home-and-board-carry-n was merged into main as 3c9857f.

Review

Sign in to review this change.
12 files changed +189 −13
docs/design/areas/quiet.md +3
@@ -16,3 +16,6 @@ markup and style, never in a paragraph's words. What already breaks one on main
1616 takes layout space beside a button, tab or the top bar: the element carries `hintAttr(key)` (`data-hint`), which shows
1717 as a tooltip on hover or focus and as an overlay while Option is held, and never on touch. Only a menu or palette row,
1818 which has a column for it, draws the `<kbd>`.
19
20 - `banner` (#10699) — `role="alert"`, a `.banner` or a `.notice-danger` in the home and board builders (`board.ts`, `boards-view.ts`, `studio/home*.ts`, `alarm_lines.ts`), zero allowed. A setting that is off is one line on its own page and the system page; only a transient flash (its tag carries `data-flash`) may alert.
21 - `test/calm-banners.test.ts` renders `/` and `/board` for an admin on a healthy forge and with egress in report mode: neither carries an alert; the fault is one line on Capacity and System.
scripts/design-lint.ts +27 −1
@@ -37,6 +37,8 @@
3737 * accent token such as `--accent-2`; only `--accent` and `--accent-fg` exist).
3838 * They match markup and style, never a paragraph's words. Named in docs/design/areas/quiet.md.
3939 *
40 * `banner` (#10699) refuses a standing alert in the home and board builders; see `banner()`.
41 *
4042 * A red names the file:line, the literal and what to use instead, so one pass
4143 * fixes it; run `bun scripts/design-lint.ts` before pushing. A new class is named
4244 * in its own `docs/design/areas/<area>.md` rather than in DESIGN.md, which
@@ -410,6 +412,30 @@ export function scanQuiet(root: string): Record<string, QuietFinding[]> {
410412 return out;
411413 }
412414
415 /**
416 * The calm rule (#10699): a healthy forge's home and board carry no standing
417 * alert. `role="alert"`, a `.banner` or a `.notice-danger` in those builders is
418 * refused; a setting that is off is one line on its own page, never a banner
419 * over the board. The only opt-out is a transient flash: the same tag carries
420 * `data-flash`. Zero is the ceiling, so there is no debt list.
421 */
422 export const BANNER_FILES = /(^|\/)src\/web\/(pages\/(board|boards-view)\.ts|studio\/(home[\w-]*|alarm_lines)\.ts)$/;
423 export type BannerFinding = { line: number; text: string };
424 export function banner(text: string): BannerFinding[] {
425 const code = blanked(text);
426 const out: BannerFinding[] = [];
427 for (const m of code.matchAll(/<[a-z][^<>]*?(?:\brole="alert"|\bclass="[^"]*\b(?:banner|notice-danger)\b[^"]*")[^<>]*>/gi)) {
428 if (/\bdata-flash\b/.test(m[0])) continue;
429 out.push({ line: code.slice(0, m.index!).split("\n").length, text: m[0].slice(0, 80) });
430 }
431 return out;
432 }
433 export function scanBanner(root: string): string[] {
434 return sourceFiles(root)
435 .filter((f) => BANNER_FILES.test(f))
436 .flatMap((f) => banner(readFileSync(f, "utf8")).map((b) => `${relative(root, f)}:${b.line} ${b.text} → a standing alert on home or the board: say it as one line on its own settings page and in the system page (a transient flash carries data-flash)`));
437 }
438
413439 const BASELINE = "docs/design/baseline.json";
414440 const DESIGN = "docs/DESIGN.md";
415441 const SECTIONS = "docs/design/sections";
@@ -426,7 +452,7 @@ if (import.meta.main) {
426452 } else {
427453 const base: Baseline = JSON.parse(readFileSync(join(root, BASELINE), "utf8"));
428454 const debt: QuietDebt = JSON.parse(readFileSync(join(root, QUIET_DEBT), "utf8"));
429 const problems = [...compare(base, now, designText(root), areaFiles(root)), ...compareQuiet(debt, scanQuiet(root))];
455 const problems = [...compare(base, now, designText(root), areaFiles(root)), ...compareQuiet(debt, scanQuiet(root)), ...scanBanner(root)];
430456 for (const p of problems) console.error(p);
431457 if (problems.length) process.exit(1);
432458 console.log("design: no colour, size or component added outside the system");
src/system/egress_line.ts added +13
@@ -0,0 +1,13 @@
1 /**
2 * The one sentence for a forge whose cells' egress is reported, not enforced
3 * (#10699). Capacity, System and both terminal screens say exactly this and no
4 * more; null when egress is enforced, so a healthy forge says nothing.
5 */
6 import type { Database } from "bun:sqlite";
7 import { egressSetting } from "../core/capacity.ts";
8
9 export const EGRESS_REPORTED = "Egress is reported, not enforced: cells may reach any public host on ports 80 and 443.";
10
11 export function egressReportedLine(db: Database): string | null {
12 return egressSetting(db) === "report" ? EGRESS_REPORTED : null;
13 }
src/tui/screens/capacity.ts +3 −1
@@ -15,6 +15,7 @@ import type { Session } from "../session.ts";
1515 import { style } from "../term/style.ts";
1616 import { truncate } from "../term/width.ts";
1717 import { fill, len, split } from "../ui/layout.ts";
18 import { egressReportedLine } from "../../system/egress_line.ts";
1819 import { ListState, confirmScreen, drawList, later, promptScreen, type ListRow } from "./common.ts";
1920
2021 /** Whether the palette offers the screen: a forge admin who is a person. */
@@ -67,7 +68,8 @@ export function capacityScreen(f: Session, ctx: Ctx): Screen {
6768 };
6869 });
6970 // After the guards, so the cursor (bounded by the settings) never lands on it.
70 return [...guards, selfSwapRow(listed.value.selfSwap)];
71 const egress = egressReportedLine(f.app.db);

This egress row is added behaviour with no test. Render the Capacity screen with cells.egress=report and assert the row; assert it is absent when enforced.

72 return [...guards, selfSwapRow(listed.value.selfSwap), ...(egress === null ? [] : [{ title: egress, tone: "attn" as const }])];
7173 };
7274
7375 const plan = (row: CapacityRow, typed: string): void => {
src/tui/screens/system.ts +8
@@ -34,6 +34,7 @@ import { dialog } from "../ui/widgets.ts";
3434 import { Scroll, confirmScreen, drawLines, hitScroll, later, pickerScreen } from "./common.ts";
3535 import type { PaletteEntry } from "./entries.ts";
3636 import { readHealth } from "../../actions/health.ts";
37 import { egressReportedLine } from "../../system/egress_line.ts";
3738 import { featureHealth } from "../../system/feature_health.ts";
3839 import { readerPaces } from "../../system/telemetry_reads.ts";
3940 import { live as liveMachine } from "../../system/live.ts";
@@ -308,6 +309,12 @@ export function memoryScreen(session: Session, ctx: Ctx): Screen {
308309 };
309310 }
310311
312 /** The egress fault, one line, as /system says it; nothing when egress is enforced. */
313 export function egressLines(session: Session): string[] {
314 const said = egressReportedLine(session.db);
315 return said === null ? [] : [said];
316 }
317
311318 /** The machine, as /system says it, and which agents may read it: the owner's, a person (#1789). */
312319 export const entry: PaletteEntry = {
313320 id: "system",
@@ -317,6 +324,7 @@ export const entry: PaletteEntry = {
317324 offered: (session) => session.user?.isAdmin === true && session.user.kind !== "agent",
318325 run: (session, ctx) => ctx.push(systemScreen(ctx, [
319326 ...forgeLines(readHealth(session.app, session.user)),
327 ...egressLines(session),
320328 "",
321329 ...systemLines(liveMachine().read()),
322330 ], session)),
src/web/pages/capacity.ts +2 −7
@@ -8,6 +8,7 @@
88
99 import { DERIVED_WORDS, changeCapacity, edgeOffDryRun, listCapacity, type CapacityRow } from "../../actions/capacity.ts";
1010 import { CAPACITY, belowCeiling, type CapacityKey } from "../../core/capacity.ts";
11 import { EGRESS_REPORTED } from "../../system/egress_line.ts";
1112 import { bytes } from "../../system/format.ts";
1213 import { egressRowsFor } from "../../spawn/process_counters.ts";
1314 import type { Ctx } from "../../http/context.ts";
@@ -219,13 +220,7 @@ export function capacityPage(ctx: Ctx): Response {
219220 </p>
220221 ${when(
221222 reported,
222 () => html`<p class="muted small mb" id="egress-reported">
223 Egress is reported, not enforced: every new app,
224 preview, terminal and check cell may reach any public host on ports
225 80 and 443, declared or not. Private, loopback and cluster addresses,
226 and every other port, stay refused. Set it back to enforce once each
227 app's reported hosts are in its forge.json.
228 </p>`,
223 () => html`<p class="muted small mb" id="egress-reported">${EGRESS_REPORTED}</p>`,
229224 )}
230225 ${block("Capacity", html`<ul class="list setting-list">${rows}</ul>`)}
231226 ${block(
src/web/pages/system.ts +5
@@ -38,6 +38,7 @@ import { RECENT_BYTES, recentEncodings } from "../../http/compress.ts";
3838 import { KEPT_PAGE_BYTES, keptPageStats } from "./kept_page.ts";
3939 import { COLUMN_BYTES, PARTS_BYTES, keptPartsStats, treeCacheStats } from "./tree_cache.ts";
4040 import { featureHealth, type FeatureHealth } from "../../system/feature_health.ts";
41 import { egressReportedLine } from "../../system/egress_line.ts";
4142 import { badge, chartBox, meter, pill, row } from "../ui.ts";
4243
4344 /** A group's memory, and a cell's beside its cap and who holds the cap. */
@@ -266,6 +267,9 @@ export function systemPage(ctx: Ctx): Response {
266267 pids.by === "kernel"
267268 ? html`<p class="small" id="cell-pids" data-by="kernel">Each cell's processes are capped by the kernel at ${PROCESSES_MAX} (its cgroup's <code>pids.max</code>; not a crew run's): a fork past it fails inside that cell.</p>`
268269 : html`<p class="small" id="cell-pids" data-by="watch">Each cell's processes are counted once a second and the cell stopped past ${PROCESSES_MAX}; the kernel does not cap them (${pids.reason}).</p>`;
270 const egressSaid = egressReportedLine(ctx.app.db);
271 const egressLine: Html | null =
272 egressSaid === null ? null : html`<p class="small" id="egress-reported">${egressSaid} <a href="/settings/capacity">Capacity</a></p>`;
outdated

Principle 1: this repeats the paragraph already on Capacity in different words. Share one helper for the sentence. The terminal's system screen needs the same line (both doors).

269273 const memory = memorySection(ctx);
270274 const stalls = timePhase(ctx.request, "stalls", () => stallHour(loopWatch.tally(performance.now())));
271275 const rendering = performance.now();
@@ -287,6 +291,7 @@ export function systemPage(ctx: Ctx): Response {
287291 <ul class="list" id="mem">${at("mem")}</ul>
288292 ${caps}
289293 ${processCaps}
294 ${egressLine}
290295 <p class="small mono" id="temps">${at("temps")}</p></section>
291296 <section class="settings-block"><h2>Disks and network</h2>
292297 <ul class="list" id="sys-disks">${at("sys-disks")}</ul></section>
src/web/studio/alarm_lines.ts +1 −1
@@ -27,7 +27,7 @@ export function cachedAlarms(db: Database, now: Date, ceilingUsd: number | null)
2727
2828 export function alarmLines(alarms: readonly FiredAlarm[]): Html {
2929 if (alarms.length === 0) return html``;
30 return html`<div class="stack" role="alert">
30 return html`<div class="stack">
3131 ${alarms.map((a) => html`<p class="small"><a href="${a.href}">${a.says}</a></p>`)}
3232 </div>`;
3333 }
test/calm-banners.test.ts added +85
@@ -0,0 +1,85 @@
1 /**
2 * A healthy forge's home and board carry no alert banner (#10699), and a real
3 * fault (egress in report mode) is one line on its own settings page and on
4 * System, never over `/` or `/board`. The design-lint `banner` rule holds the
5 * source; this holds what is rendered.
6 */
7 import { afterAll, beforeAll, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { createApp, type App } from "../src/app.ts";
12 import { serve, type Servers } from "../src/http/server.ts";
13 import { changeCapacity } from "../src/actions/capacity.ts";
14 import { egressLines } from "../src/tui/screens/system.ts";
15 import type { Session } from "../src/tui/session.ts";
16 import { EGRESS_REPORTED } from "../src/system/egress_line.ts";
17 import { banner } from "../scripts/design-lint.ts";
18 import { seedAdmin } from "./helpers/admin.ts";
19
20 let dataDir = "";
21 let app: App, servers: Servers, bearer = "", admin: Awaited<ReturnType<typeof seedAdmin>> & { ok: true };
22
23 beforeAll(async () => {
24 dataDir = await mkdtemp(join(tmpdir(), "forge-calm-"));
25 app = createApp({ dataDir, port: 0 });
26 servers = await serve(app);
27 const made = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
28 if (!made.ok) throw new Error(made.error);
29 admin = made as typeof admin;
30 const minted = await fetch(`${servers.url}/api/v1/tokens`, {
31 method: "POST",
32 headers: { "content-type": "application/json" },
33 body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }),
34 });
35 bearer = ((await minted.json()) as { token: string }).token;
36 });
37
38 afterAll(async () => {
39 servers.stop();
40 app.close();
41 await rm(dataDir, { recursive: true, force: true });
42 });
43
44 const get = async (path: string): Promise<string> => {
45 const res = await fetch(`${servers.url}${path}`, { headers: { authorization: `Bearer ${bearer}` } });
46 expect(res.status).toBe(200);
47 return res.text();
48 };
49 const alarming = (body: string) => /role="alert"/.test(body) || /class="[^"]*\b(?:banner|notice-danger)\b/.test(body);
50
51 test("a healthy forge's home and board have no alert, banner or danger notice", async () => {
52 for (const path of ["/", "/board"]) {
53 const body = await get(path);
54 expect(body.length).toBeGreaterThan(500);
55 expect(alarming(body)).toBe(false);
56 }
57 });
outdated

Add the converse: with egress enforced, /system has no egress-reported line. Without it the line could always render and this test would still pass.

58
59 test("with egress enforced, System and Capacity say nothing about it, and neither does the terminal", async () => {
60 expect(await get("/system")).not.toContain("egress-reported");
61 expect(await get("/settings/capacity")).not.toContain("egress-reported");
62 expect(egressLines({ db: app.db } as Session)).toEqual([]);
63 });
64
65 test("egress in report mode is one line on Capacity and System, not on / or /board", async () => {
66 const changed = await changeCapacity(app, admin.value, "cells.egress", "report");
67 expect(changed.ok).toBe(true);
68 expect(await get("/settings/capacity")).toContain('id="egress-reported"');
69 expect(await get("/system")).toContain('id="egress-reported"');
70 expect(egressLines({ db: app.db } as Session)).toEqual([EGRESS_REPORTED]);
71 for (const path of ["/", "/board"]) {
72 const body = await get(path);
73 expect(body).not.toContain("egress-reported");
74 expect(body.toLowerCase()).not.toContain("egress is reported");
75 expect(alarming(body)).toBe(false);
76 }
77 });
78
79 test("the banner rule names a standing alert and lets a transient flash through", () => {
80 expect(banner('<div role="alert">x</div>')).toHaveLength(1);
81 expect(banner('<p class="notice-danger">x</p>')).toHaveLength(1);
82 expect(banner('<div class="stack banner">x</div>')).toHaveLength(1);
83 expect(banner('<div role="alert" data-flash>x</div>')).toHaveLength(0);
84 expect(banner('<p class="small">fine</p>')).toHaveLength(0);
85 });
test/flow-alarm-lines.test.ts +1 −1
@@ -56,7 +56,7 @@ describe("alarm lines", () => {
5656 for (const path of ["/boards", "/"]) {
5757 const body = await (await fetch(servers.url + path, { headers })).text();
5858 expect(body.includes("nothing has gone live"), `${path} ${String(merged)}`).toBe(want);

Assert role="alert" is absent for both cases instead of deleting the check.

59 expect(body.includes('role="alert"')).toBe(want);
59 expect(body.includes('role="alert"'), `${path} ${String(merged)} draws a line, not an alert`).toBe(false);
6060 }
6161 await servers.stop?.();
6262 }
test/tui-capacity.test.ts +23 −2
@@ -15,6 +15,7 @@ import { CAPACITY_KEYS, capacityDefault, capacityValue } from "../src/core/capac
1515 import { createUser, type User } from "../src/core/users.ts";
1616 import { createForgeApp, forgeCommands } from "../src/tui/forge.ts";
1717 import { capacityScreen } from "../src/tui/screens/capacity.ts";
18 import { EGRESS_REPORTED } from "../src/system/egress_line.ts";
1819 import { handover } from "../src/release/swap.ts";
1920 import { headlessTerminal } from "../src/tui/term/terminal.ts";
2021 import { quietLogs } from "./helpers/globals.ts";
@@ -31,8 +32,8 @@ let bot: User;
3132 const identity = (user?: User) => ({ sshUsername: user?.username ?? "git", remoteAddress: "127.0.0.1", fingerprint: "SHA256:test", user });
3233 const crew = () => capacityValue(app.db, "crew.concurrency");
3334
34 function boot(user: User) {
35 const { app: tui, session } = createForgeApp(app, identity(user), { terminal: headlessTerminal(140, 40), exitHooks: false });
35 function boot(user: User, rows = 40) {
36 const { app: tui, session } = createForgeApp(app, identity(user), { terminal: headlessTerminal(140, rows), exitHooks: false });
3637 tui.start();
3738 tui.ctx.push(capacityScreen(session, tui.ctx));
3839 const press = async (...keys: string[]) => {
@@ -86,6 +87,26 @@ describe("tui capacity", () => {
8687 h.stop();
8788 });
8889
90 test("egress in report mode is one row after the guards; enforced, there is none (#10699)", async () => {
91 const egressShown = async () => {
92 const h = boot(carol, 80);
93 await h.press();
94 const text = h.text();
95 h.stop();
96 expect(text).toContain("Crew runs at once");
97 return text.includes(EGRESS_REPORTED);
98 };
99 try {
100 expect(await egressShown()).toBe(false);
101 expect(changeCapacity(app, carol, "cells.egress", "report").ok).toBe(true);
102 expect(await egressShown()).toBe(true);
103 expect(changeCapacity(app, carol, "cells.egress", "enforce").ok).toBe(true);
104 expect(await egressShown()).toBe(false);
105 } finally {
106 changeCapacity(app, carol, "cells.egress", null);
107 }
108 });
109
89110 test("a crew concurrency below its ceiling is marked, and not at it", async () => {
90111 const ceiling = capacityDefault("crew.concurrency");
91112 try {
test/tui-system.test.ts +18
@@ -17,6 +17,8 @@ import { setTelemetryGrant } from "../src/actions/telemetry.ts";
1717 import { hasGrant } from "../src/core/telemetry.ts";
1818 import { countRead, resetTelemetryReads } from "../src/system/telemetry_reads.ts";
1919 import { forgeLines, historyLines, readerLines, memoryLines, memoryScreen, systemScreen } from "../src/tui/screens/system.ts";
20 import { changeCapacity } from "../src/actions/capacity.ts";
21 import { EGRESS_REPORTED } from "../src/system/egress_line.ts";
2022 import { headlessTerminal } from "../src/tui/term/terminal.ts";
2123 import { quietLogs } from "./helpers/globals.ts";
2224 import { settleApp } from "./helpers/tui.ts";
@@ -279,6 +281,22 @@ describe("tui system: the forge's own state (#7296)", () => {
279281 expect(lines[1]).toBe("rollout blocked: quota refused · 1 incident: oom:web: killed");
280282 });
281283
284 test("egress in report mode is one line on the System screen; enforced, nothing (#10699)", async () => {
285 await seed({});
286 try {
287 expect(await systemText(carol)).not.toContain(EGRESS_REPORTED.slice(0, 40));
288 expect(changeCapacity(app, carol, "cells.egress", "report").ok).toBe(true);
289 const text = await systemText(carol);
290 expect(text).toContain("forge 9.8.7 · 0123456 · serving");
291 // The dialog clips the sentence at its width; its opening words are one row.
292 expect(text.split(EGRESS_REPORTED.slice(0, 40)).length).toBe(2);
293 expect(changeCapacity(app, carol, "cells.egress", "enforce").ok).toBe(true);
294 expect(await systemText(carol)).not.toContain(EGRESS_REPORTED.slice(0, 40));
295 } finally {
296 changeCapacity(app, carol, "cells.egress", null);
297 }
298 });
299
282300 test("a stranger's answer, as MCP gives it, never has a second line", async () => {
283301 await seed({ down: "the edge answered 502" });
284302 expect(forgeLines(readHealth(app, undefined))).toEqual(["forge 9.8.7 · 0123456 · serving"]);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs