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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/10699-a-healthy-forge-s-home-and-board-carry-n → main 6 commits fork 4 threads · 4 unresolved
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 | ||
| 16 | 16 | takes layout space beside a button, tab or the top bar: the element carries `hintAttr(key)` (`data-hint`), which shows |
| 17 | 17 | 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, |
| 18 | 18 | 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 @@ | ||
| 37 | 37 | * accent token such as `--accent-2`; only `--accent` and `--accent-fg` exist). |
| 38 | 38 | * They match markup and style, never a paragraph's words. Named in docs/design/areas/quiet.md. |
| 39 | 39 | * |
| 40 | * `banner` (#10699) refuses a standing alert in the home and board builders; see `banner()`. | |
| 41 | * | |
| 40 | 42 | * A red names the file:line, the literal and what to use instead, so one pass |
| 41 | 43 | * fixes it; run `bun scripts/design-lint.ts` before pushing. A new class is named |
| 42 | 44 | * 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[]> { | ||
| 410 | 412 | return out; |
| 411 | 413 | } |
| 412 | 414 | |
| 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 | ||
| 413 | 439 | const BASELINE = "docs/design/baseline.json"; |
| 414 | 440 | const DESIGN = "docs/DESIGN.md"; |
| 415 | 441 | const SECTIONS = "docs/design/sections"; |
| @@ -426,7 +452,7 @@ if (import.meta.main) { | ||
| 426 | 452 | } else { |
| 427 | 453 | const base: Baseline = JSON.parse(readFileSync(join(root, BASELINE), "utf8")); |
| 428 | 454 | 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)]; | |
| 430 | 456 | for (const p of problems) console.error(p); |
| 431 | 457 | if (problems.length) process.exit(1); |
| 432 | 458 | 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"; | ||
| 15 | 15 | import { style } from "../term/style.ts"; |
| 16 | 16 | import { truncate } from "../term/width.ts"; |
| 17 | 17 | import { fill, len, split } from "../ui/layout.ts"; |
| 18 | import { egressReportedLine } from "../../system/egress_line.ts"; | |
| 18 | 19 | import { ListState, confirmScreen, drawList, later, promptScreen, type ListRow } from "./common.ts"; |
| 19 | 20 | |
| 20 | 21 | /** 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 { | ||
| 67 | 68 | }; |
| 68 | 69 | }); |
| 69 | 70 | // 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); | |
| 72 | return [...guards, selfSwapRow(listed.value.selfSwap), ...(egress === null ? [] : [{ title: egress, tone: "attn" as const }])]; | |
| 71 | 73 | }; |
| 72 | 74 | |
| 73 | 75 | const plan = (row: CapacityRow, typed: string): void => { |
src/tui/screens/system.ts +8
| @@ -34,6 +34,7 @@ import { dialog } from "../ui/widgets.ts"; | ||
| 34 | 34 | import { Scroll, confirmScreen, drawLines, hitScroll, later, pickerScreen } from "./common.ts"; |
| 35 | 35 | import type { PaletteEntry } from "./entries.ts"; |
| 36 | 36 | import { readHealth } from "../../actions/health.ts"; |
| 37 | import { egressReportedLine } from "../../system/egress_line.ts"; | |
| 37 | 38 | import { featureHealth } from "../../system/feature_health.ts"; |
| 38 | 39 | import { readerPaces } from "../../system/telemetry_reads.ts"; |
| 39 | 40 | import { live as liveMachine } from "../../system/live.ts"; |
| @@ -308,6 +309,12 @@ export function memoryScreen(session: Session, ctx: Ctx): Screen { | ||
| 308 | 309 | }; |
| 309 | 310 | } |
| 310 | 311 | |
| 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 | ||
| 311 | 318 | /** The machine, as /system says it, and which agents may read it: the owner's, a person (#1789). */ |
| 312 | 319 | export const entry: PaletteEntry = { |
| 313 | 320 | id: "system", |
| @@ -317,6 +324,7 @@ export const entry: PaletteEntry = { | ||
| 317 | 324 | offered: (session) => session.user?.isAdmin === true && session.user.kind !== "agent", |
| 318 | 325 | run: (session, ctx) => ctx.push(systemScreen(ctx, [ |
| 319 | 326 | ...forgeLines(readHealth(session.app, session.user)), |
| 327 | ...egressLines(session), | |
| 320 | 328 | "", |
| 321 | 329 | ...systemLines(liveMachine().read()), |
| 322 | 330 | ], session)), |
src/web/pages/capacity.ts +2 −7
| @@ -8,6 +8,7 @@ | ||
| 8 | 8 | |
| 9 | 9 | import { DERIVED_WORDS, changeCapacity, edgeOffDryRun, listCapacity, type CapacityRow } from "../../actions/capacity.ts"; |
| 10 | 10 | import { CAPACITY, belowCeiling, type CapacityKey } from "../../core/capacity.ts"; |
| 11 | import { EGRESS_REPORTED } from "../../system/egress_line.ts"; | |
| 11 | 12 | import { bytes } from "../../system/format.ts"; |
| 12 | 13 | import { egressRowsFor } from "../../spawn/process_counters.ts"; |
| 13 | 14 | import type { Ctx } from "../../http/context.ts"; |
| @@ -219,13 +220,7 @@ export function capacityPage(ctx: Ctx): Response { | ||
| 219 | 220 | </p> |
| 220 | 221 | ${when( |
| 221 | 222 | 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>`, | |
| 229 | 224 | )} |
| 230 | 225 | ${block("Capacity", html`<ul class="list setting-list">${rows}</ul>`)} |
| 231 | 226 | ${block( |
src/web/pages/system.ts +5
| @@ -38,6 +38,7 @@ import { RECENT_BYTES, recentEncodings } from "../../http/compress.ts"; | ||
| 38 | 38 | import { KEPT_PAGE_BYTES, keptPageStats } from "./kept_page.ts"; |
| 39 | 39 | import { COLUMN_BYTES, PARTS_BYTES, keptPartsStats, treeCacheStats } from "./tree_cache.ts"; |
| 40 | 40 | import { featureHealth, type FeatureHealth } from "../../system/feature_health.ts"; |
| 41 | import { egressReportedLine } from "../../system/egress_line.ts"; | |
| 41 | 42 | import { badge, chartBox, meter, pill, row } from "../ui.ts"; |
| 42 | 43 | |
| 43 | 44 | /** 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 { | ||
| 266 | 267 | pids.by === "kernel" |
| 267 | 268 | ? 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>` |
| 268 | 269 | : 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). |
||
| 269 | 273 | const memory = memorySection(ctx); |
| 270 | 274 | const stalls = timePhase(ctx.request, "stalls", () => stallHour(loopWatch.tally(performance.now()))); |
| 271 | 275 | const rendering = performance.now(); |
| @@ -287,6 +291,7 @@ export function systemPage(ctx: Ctx): Response { | ||
| 287 | 291 | <ul class="list" id="mem">${at("mem")}</ul> |
| 288 | 292 | ${caps} |
| 289 | 293 | ${processCaps} |
| 294 | ${egressLine} | |
| 290 | 295 | <p class="small mono" id="temps">${at("temps")}</p></section> |
| 291 | 296 | <section class="settings-block"><h2>Disks and network</h2> |
| 292 | 297 | <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) | ||
| 27 | 27 | |
| 28 | 28 | export function alarmLines(alarms: readonly FiredAlarm[]): Html { |
| 29 | 29 | if (alarms.length === 0) return html``; |
| 30 | return html`<div class="stack" role="alert"> | |
| 30 | return html`<div class="stack"> | |
| 31 | 31 | ${alarms.map((a) => html`<p class="small"><a href="${a.href}">${a.says}</a></p>`)} |
| 32 | 32 | </div>`; |
| 33 | 33 | } |
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", () => { | ||
| 56 | 56 | for (const path of ["/boards", "/"]) { |
| 57 | 57 | const body = await (await fetch(servers.url + path, { headers })).text(); |
| 58 | 58 | 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); | |
| 60 | 60 | } |
| 61 | 61 | await servers.stop?.(); |
| 62 | 62 | } |
test/tui-capacity.test.ts +23 −2
| @@ -15,6 +15,7 @@ import { CAPACITY_KEYS, capacityDefault, capacityValue } from "../src/core/capac | ||
| 15 | 15 | import { createUser, type User } from "../src/core/users.ts"; |
| 16 | 16 | import { createForgeApp, forgeCommands } from "../src/tui/forge.ts"; |
| 17 | 17 | import { capacityScreen } from "../src/tui/screens/capacity.ts"; |
| 18 | import { EGRESS_REPORTED } from "../src/system/egress_line.ts"; | |
| 18 | 19 | import { handover } from "../src/release/swap.ts"; |
| 19 | 20 | import { headlessTerminal } from "../src/tui/term/terminal.ts"; |
| 20 | 21 | import { quietLogs } from "./helpers/globals.ts"; |
| @@ -31,8 +32,8 @@ let bot: User; | ||
| 31 | 32 | const identity = (user?: User) => ({ sshUsername: user?.username ?? "git", remoteAddress: "127.0.0.1", fingerprint: "SHA256:test", user }); |
| 32 | 33 | const crew = () => capacityValue(app.db, "crew.concurrency"); |
| 33 | 34 | |
| 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 }); | |
| 36 | 37 | tui.start(); |
| 37 | 38 | tui.ctx.push(capacityScreen(session, tui.ctx)); |
| 38 | 39 | const press = async (...keys: string[]) => { |
| @@ -86,6 +87,26 @@ describe("tui capacity", () => { | ||
| 86 | 87 | h.stop(); |
| 87 | 88 | }); |
| 88 | 89 | |
| 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 | ||
| 89 | 110 | test("a crew concurrency below its ceiling is marked, and not at it", async () => { |
| 90 | 111 | const ceiling = capacityDefault("crew.concurrency"); |
| 91 | 112 | try { |
test/tui-system.test.ts +18
| @@ -17,6 +17,8 @@ import { setTelemetryGrant } from "../src/actions/telemetry.ts"; | ||
| 17 | 17 | import { hasGrant } from "../src/core/telemetry.ts"; |
| 18 | 18 | import { countRead, resetTelemetryReads } from "../src/system/telemetry_reads.ts"; |
| 19 | 19 | 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"; | |
| 20 | 22 | import { headlessTerminal } from "../src/tui/term/terminal.ts"; |
| 21 | 23 | import { quietLogs } from "./helpers/globals.ts"; |
| 22 | 24 | import { settleApp } from "./helpers/tui.ts"; |
| @@ -279,6 +281,22 @@ describe("tui system: the forge's own state (#7296)", () => { | ||
| 279 | 281 | expect(lines[1]).toBe("rollout blocked: quota refused · 1 incident: oom:web: killed"); |
| 280 | 282 | }); |
| 281 | 283 | |
| 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 | ||
| 282 | 300 | test("a stranger's answer, as MCP gives it, never has a second line", async () => { |
| 283 | 301 | await seed({ down: "the edge answered 502" }); |
| 284 | 302 | expect(forgeLines(readHealth(app, undefined))).toEqual(["forge 9.8.7 · 0123456 · serving"]); |
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.