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

/system shows each first-opened page's measured median beside its budget, read from PAGE_BUDGETS #11467

opened this

Check · Review · Land · Live — Its check is running. Next: reviewer, pushed since the last review.

Ready to land

torque/forge:torque/11457-system-shows-each-first-opened-page-s-me → main 8 commits fork 4 threads · 4 unresolved

Review

Sign in to review this change.
13 files changed +295 −12
docs/MCP.md +5
@@ -314,6 +314,11 @@ the blocked host counted last, read by `violationsByDirective` in
314314 `src/http/csp_report.ts` as the security page reads its week. A forge admin
315315 only; anyone else is `forbidden`.
316316
317 `system` also answers `budgets` (#11457): one row per `PAGE_BUDGETS` page,
318 `{ page, pattern, budgetMs, budgetStatements, medianMs, requests, over, says }`,
319 `medianMs` null until 5 real requests; the rows /system and
320 `GET /api/v1/telemetry/system` show (`src/perf/budget_read.ts`).
321
317322 `flow` with `{ q, action }` is the fleet's write door (#450): the plan
318323 `POST /api/v1/fleet/ops` answers for a dry run, `{ dryRun: true, query, kind,
319324 verb, value, reversible, rows, counts }`, with nothing changed and only a read
docs/design/attention-budget.json +2 −2
@@ -173,12 +173,12 @@
173173 "system": {
174174 "390": {
175175 "words": 212,
176 "blocks": 65,
176 "blocks": 40,
177177 "colours": 0
178178 },
179179 "1280": {
180180 "words": 212,
181 "blocks": 65,
181 "blocks": 40,
182182 "colours": 0
183183 },
184184 "note": "Measured on the test forge's empty state with the machine reading replaced by an empty one and the two cell-cap sentences fixed (the live page's counts depend on the host's cores, disks, processes and cgroups); nothing seeded or waited for."
docs/features/page-budget.md added +17
@@ -0,0 +1,17 @@
1 # Page budget on /system
2
3 `PAGE_BUDGETS` (`src/perf/page_budget.ts`) names the pages people open first,
4 each with the median it answers in and the SQL statements one request may run.
5 `/system` shows one list row per page: the budget (ms, statements) and the median of
6 the last 15 minutes of real requests from the rolling record
7 (`src/perf/budget_read.ts`, `src/web/pages/system-budget.ts`). A page over its
8 budget says its sentence (`says`) in its own row; the others show
9 `50 ms, 46 SQL, median 90 ms`. Fewer than 5 requests is "not
10 measured yet". Statements are budgeted but not recorded per request, so only
11 `test/page-budget.test.ts` measures them.
12
13 The same rows (`budgets`, with `says`) are in `GET /api/v1/telemetry/system` and
14 the MCP `system` tool, and in the terminal's System screen (`budgetLines` in
15 `src/tui/screens/system.ts`), shown to whoever `telemetryReader` admits and to
16 no one else. The record is read at most once per 5 seconds.
17 `test/system-budget.test.ts` fails if the page, the API, the MCP tool and the terminal stop saying the same string.
scripts/unreached-allow.json +1
@@ -7,6 +7,7 @@
77 "src/flow/rebase_first.ts",
88 "src/flow/review_rounds.ts",
99 "src/flow/shared_red.ts",
10 "src/flow/stale_fold.ts",
1011 "src/perf/page_budget.ts",
1112 "src/spawn/pod.ts",
1213 "src/tui/ui/qr.ts",
src/http/mcp/tools/system.ts +1 −1
@@ -10,7 +10,7 @@ import { good, bad, type Tool } from "../shared.ts";
1010
1111 export const tool: Tool = {
1212 name: "system",
13 doc: "the machine the forge runs on, now: CPU, load, memory, disk, network, processes, `stalls`: the last hour's main-thread stalls by route (`<route> (in flight)` when still running at the wake), or `job:<name>`, as {cause, ms, count}, `readings`: the last 20 minutes' worst loop lag as {at, ms, what} beside `barMs` (100) and `over`, and `readers`: who polls the telemetry and how fast; a forge admin, or an agent granted `telemetry:read`. `{ kind: \"csp\" }`: the last 24 h of Content-Security-Policy violations browsers reported, `{ directive, n, newest }` per directive (`newest` the blocked host last counted), most first, counted per UTC hour from `since`, and `scriptErrors` `{ page, where, message, n, last }`, as the security page reads them; a forge admin only",
13 doc: "the machine the forge runs on, now: CPU, load, memory, disk, network, processes, `stalls`: the last hour's main-thread stalls by route (`<route> (in flight)` when still running at the wake), or `job:<name>`, as {cause, ms, count}, `readings`: the last 20 minutes' worst loop lag as {at, ms, what} beside `barMs` (100) and `over`, and `readers`: who polls the telemetry and how fast, `budgets`: each first-opened page's budget and measured median as {page, budgetMs, medianMs, over, says}; a forge admin, or an agent granted `telemetry:read`. `{ kind: \"csp\" }`: the last 24 h of Content-Security-Policy violations browsers reported, `{ directive, n, newest }` per directive (`newest` the blocked host last counted), most first, counted per UTC hour from `since`, and `scriptErrors` `{ page, where, message, n, last }`, as the security page reads them; a forge admin only",
1414 description:
1515 "The forge machine now: CPU, memory, disk, network. Admin or telemetry:read; `kind: csp`: 24 h of CSP violations (admin).",
1616 inputSchema: { type: "object", properties: { kind: { type: "string", enum: ["csp"] } } },
src/perf/budget_read.ts added +68
@@ -0,0 +1,68 @@
1 /**

Both doors: the terminal has no screen for this. Add the rows to src/tui/screens/system.ts, using the same budgetRowsCached and says.

2 * The page budget beside what real requests measured (#11457): one row per
3 * `PAGE_BUDGETS` entry, its budget and the median of the last 15 minutes from
4 * the rolling record (`record.ts`, memory only). /system, the API and the MCP
5 * `system` tool all say it with `budgetLine`. The record is read once per few
6 * seconds, so a busy /system never sorts the samples again. SQL statements are
7 * budgeted but not recorded per request, so only the walk in
8 * `test/page-budget.test.ts` measures them.
9 */
10 import { PAGE_BUDGETS } from "./page_budget.ts";
11 import { percentile, WINDOW_MS, type PerfRecord } from "./record.ts";
12
13 /** Fewer real requests than this and a median says nothing. */
14 export const MIN_MEASURED = 5;
15 const KEEP_MS = 5000;
16
17 export interface BudgetRow {
18 readonly page: string;
19 readonly pattern: string;
20 readonly budgetMs: number;
21 readonly budgetStatements: number;
22 /** Median ms of the window's requests, null when fewer than `MIN_MEASURED`. */
23 readonly medianMs: number | null;
24 readonly requests: number;
25 readonly over: boolean;
26 readonly says: string;
27 }
28
29 /** The one plain line for a row: over budget is a sentence, not an alarm. */
30 export function budgetLine(r: Pick<BudgetRow, "page" | "budgetMs" | "budgetStatements" | "medianMs" | "over">): string {
31 const budget = `budget ${r.budgetMs} ms, ${r.budgetStatements} SQL statements`;
32 if (r.medianMs === null) return `${r.page}: ${budget}; not measured yet.`;
33 return r.over
34 ? `${r.page}: median ${r.medianMs} ms is over its ${r.budgetMs} ms budget (${budget}).`
35 : `${r.page}: median ${r.medianMs} ms, within its ${r.budgetMs} ms (${budget}).`;
36 }
37
38 /** Every budgeted page with its measured median, in table order. */
39 export function budgetRows(record: PerfRecord, now: number): BudgetRow[] {
40 return PAGE_BUDGETS.map((b) => {
41 const xs = record
42 .traffic(b.pattern, now - WINDOW_MS)
43 .filter((s) => s.signedIn === (b.signedOut !== true) && s.crawler !== true && s.internal !== true && s.refused !== true)
44 .map((s) => s.ms);
45 const med = xs.length < MIN_MEASURED ? null : Math.round(percentile(xs, 50)!);
46 const row = {
47 page: b.page,
48 pattern: b.pattern,
49 budgetMs: b.medianMs,
50 budgetStatements: b.statements,
51 medianMs: med,
52 requests: xs.length,
53 over: med !== null && med > b.medianMs,
54 };
55 return { ...row, says: budgetLine(row) };
56 });
57 }
58
59 let kept: { record: PerfRecord; at: number; rows: BudgetRow[] } | undefined;
60 /** `budgetRows`, kept a few seconds; a change shows on reload. */
61 export function budgetRowsCached(record: PerfRecord, now = Date.now()): BudgetRow[] {
62 if (kept === undefined || kept.record !== record || now - kept.at > KEEP_MS) kept = { record, at: now, rows: budgetRows(record, now) };
63 return kept.rows;
64 }
65 /** Drop the kept rows, so the next read measures again. */
66 export function forgetBudgetRows(): void {
67 kept = undefined;
68 }
src/system/telemetry.ts +4 −2
@@ -28,6 +28,8 @@ import type { Reader } from "../actions/telemetry.ts";
2828 import { agentView, live } from "./live.ts";
2929 import type { SystemView } from "./view.ts";
3030 import { readerPaces, type ReaderPace } from "./telemetry_reads.ts";
31 import { budgetRowsCached, type BudgetRow } from "../perf/budget_read.ts";
32 import { perfRecord } from "../perf/live.ts";
3133 import { featureHealth, type FeatureHealth } from "./feature_health.ts";
3234
3335 export function systemFor(app: App, reader: Reader): SystemView {
@@ -40,8 +42,8 @@ export function systemFor(app: App, reader: Reader): SystemView {
4042 * rows /system draws (#9088). Names are accounts, so only a telemetry reader gets
4143 * here. Call it before `countRead` so the read does not count into its own answer.
4244 */
43 export function systemWithReaders(app: App, reader: Reader): SystemView & { readers: ReaderPace[]; features: FeatureHealth[] } {
44 return { ...systemFor(app, reader), readers: readerPaces(), features: featureHealth(app, reader) };
45 export function systemWithReaders(app: App, reader: Reader): SystemView & { readers: ReaderPace[]; features: FeatureHealth[]; budgets: BudgetRow[] } {
46 return { ...systemFor(app, reader), readers: readerPaces(), features: featureHealth(app, reader), budgets: budgetRowsCached(perfRecord) };
4547 }
4648
4749 /**
src/tui/screens/system.ts +14
@@ -40,6 +40,8 @@ import type { PaletteEntry } from "./entries.ts";
4040 import { readHealth } from "../../actions/health.ts";
4141 import { egressReportedLine } from "../../system/egress_line.ts";
4242 import { featureHealth } from "../../system/feature_health.ts";
43 import { budgetRowsCached } from "../../perf/budget_read.ts";
44 import { perfRecord } from "../../perf/live.ts";
4345 import { readerPaces } from "../../system/telemetry_reads.ts";
4446 import { live as liveMachine } from "../../system/live.ts";
4547 import { childDirOf, listChildrenNow } from "../../actions/spawn.ts";
@@ -197,6 +199,16 @@ export function featuresLines(session: Session): string[] {
197199 ];
198200 }
199201
202 /**
203 * Each first-opened page's budget beside its measured median, a line a page in
204 * the row's own `says` words, as /system's "Page budgets" says it (#11457).
205 * Empty for anyone /system refuses: the page's gate, `telemetryReader`.
206 */
207 export function budgetLines(session: Session, now = Date.now()): string[] {
208 if (!telemetryReader(session.app, session.user)) return [];
209 return ["Page budgets, last 15 min", ...budgetRowsCached(perfRecord, now).flatMap((r) => wrap(r.says, WIDTH - 8).map((l, i) => (i === 0 ? "" : " ") + l))];
210 }
211
200212 /** The agents holding the grant, in one line. */
201213 export function grantLine(session: Session): string {
202214 const held = grantable(session.db).filter((a) => a.granted).map((a) => a.username);
@@ -247,6 +259,7 @@ export function systemScreen(ctx: Ctx, lines: readonly string[], session: Sessio
247259 keys: [binding(["h"], "window"), ...(offered ? [binding(["g"], "grant or take back telemetry")] : []), binding(["enter", "esc", "q"], "close")],
248260 render(area, s) {
249261 const features = featuresLines(session);
262 const budgets = budgetLines(session);
250263 const buttons = dialog(s, area, ctx.theme.c, {
251264 title: "System",
252265 body: [
@@ -255,6 +268,7 @@ export function systemScreen(ctx: Ctx, lines: readonly string[], session: Sessio
255268 ...history,
256269 ...(offered ? ["", ...readerLines(session)] : []),
257270 ...(features.length === 0 ? [] : ["", ...features]),
271 ...(budgets.length === 0 ? [] : ["", ...budgets]),
258272 ...(offered ? ["", grantLine(session), "g: grant or take back telemetry:read"] : []),
259273 ],
260274 actions: [{ label: "Close" }],
src/web/pages/system-budget.ts added +21
@@ -0,0 +1,21 @@
1 /**
2 * /system's page budget (#11457): each first-opened page's budget beside its
3 * measured median, rows from `perf/budget_read.ts` (the same the API, the MCP
4 * `system` tool and the terminal screen answer), one list row a page, each in
5 * the row's own `says` words.
6 */
7 import type { BudgetRow } from "../../perf/budget_read.ts";
8 import { html, type Html } from "../html.ts";
9 import { row } from "../ui.ts";
10
11 export function budgetSection(rows: readonly BudgetRow[]): Html {
outdated

Principle 8: 'No measured page is over its budget.' is fine, but the over-budget case joins every says into one paragraph that also repeats '(budget N ms, M SQL statements)'. With several pages over, that is a wall of text. Make each page its own row, or its own line.

12 return html`<section class="settings-block" id="sys-budget"><h2>Page budgets</h2>
outdated

Principles 2 and 7: the actual rows are hidden behind a closed <details> and drawn as a 5-column table (a hand-rolled table for facts). Draw one row per page (row() in ../ui.ts or a list), page name, then 'budget 50 ms · 46 SQL, median 90 ms', and leave the pattern out or make it muted. Principle 1: the aria-label, the <summary> and the <h2> all say the same thing twice, so keep one.

13 <ul class="list">${rows.map((r) =>
14 row({ title: r.over ? r.says : detail(r), data: { page: r.pattern, over: r.over ? "1" : "0" } }),
15 )}</ul></section>`;
16 }
17
18 /** "home: 50 ms · 46 SQL · median 90 ms": a page within its budget, its figures on one line under the heading. */
19 function detail(r: BudgetRow): string {
20 return `${r.page}: ${r.budgetMs} ms, ${r.budgetStatements} SQL, ${r.medianMs === null ? "not measured yet" : `median ${r.medianMs} ms`}`;
21 }
src/web/pages/system.ts +11 −5
@@ -33,7 +33,7 @@ import { readerPaces, type ReaderPace } from "../../system/telemetry_reads.ts";
3333 import { slowPageLine, slowPages, type SlowPage } from "../../perf/watch.ts";
3434 import { stallHour } from "../../metrics/stalls.ts";
3535 import { HOUR_MS, type RouteStall } from "../../perf/loop.ts";
36 import { lagMinutes, loopWatch, stallBlame } from "../../perf/live.ts";
36 import { lagMinutes, loopWatch, perfRecord, stallBlame } from "../../perf/live.ts";
3737 import { LAG_BAR_MS, LAG_READINGS, overBar, type LagReading } from "../../perf/lag_minutes.ts";
3838 import { WORST_STALLS, type BlamedStall } from "../../perf/stall_blame.ts";
3939 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
@@ -46,6 +46,8 @@ import { KEPT_PAGE_BYTES, keptPageStats } from "./kept_page.ts";
4646 import { COLUMN_BYTES, PARTS_BYTES, keptPartsStats, treeCacheStats } from "./tree_cache.ts";
4747 import { featureHealth, type FeatureHealth } from "../../system/feature_health.ts";
4848 import { egressReportedLine } from "../../system/egress_line.ts";
49 import { budgetSection } from "./system-budget.ts";
50 import { budgetRowsCached } from "../../perf/budget_read.ts";
4951 import { badge, chartBox, disclosure, meter, pill, row, time } from "../ui.ts";
5052 import { childDirOf, listChildren } from "../../actions/spawn.ts";
5153 import { childrenUse, realSelf, selfUse, shareOf, sizeWords, useWords, type ChildUseRow, type SelfUse } from "../../spawn/usage.ts";
@@ -180,10 +182,13 @@ export const pageCaches = (): CacheRow[] => [
180182 export function cachesSection(rows: readonly CacheRow[]): Html {
181183 const rate = (r: CacheRow) => (r.hits + r.misses === 0 ? "—" : `${((100 * r.hits) / (r.hits + r.misses)).toFixed(1)}%`);
182184 return html`<section class="settings-block"><h2>Code page caches, since start</h2>
183 <div class="table-scroll"><table class="table" aria-label="Code page caches">
184 <thead><tr><th scope="col">Cache</th><th scope="col" class="num">Entries</th><th scope="col" class="num">Bytes</th><th scope="col" class="num">Cap</th><th scope="col" class="num">Hits</th><th scope="col" class="num">Misses</th><th scope="col" class="num">Hit rate</th></tr></thead>
185 <tbody>${rows.map((r) => html`<tr data-cache="${r.name}" data-bytes="${String(r.bytes)}"><td>${r.name}</td><td class="num mono">${String(r.entries)}</td><td class="num mono">${mib(r.bytes)}</td><td class="num mono">${mib(r.capBytes)}</td><td class="num mono">${String(r.hits)}</td><td class="num mono">${String(r.misses)}</td><td class="num mono">${rate(r)}</td></tr>`)}</tbody>
186 </table></div></section>`;
185 <ul class="list" aria-label="Code page caches">${rows.map((r) =>
186 row({
187 title: r.name,
188 meta: html`<span class="mono small">${mib(r.bytes)}/${mib(r.capBytes)} MiB, ${String(r.entries)} entries, ${String(r.hits)} hits, ${String(r.misses)} misses${r.hits + r.misses === 0 ? "" : `, ${rate(r)}`}</span>`,
189 data: { cache: r.name, bytes: String(r.bytes) },
190 }),
191 )}</ul></section>`;
187192 }
188193
189194 /** Absorbed apps and whether production is serving (#9887); `featureHealth`'s rows. Nothing absorbed, nothing drawn. */
@@ -404,6 +409,7 @@ export async function systemPage(ctx: Ctx): Promise<Response> {
404409 ${featuresSection(timePhase(ctx.request, "features", () => featureHealth(ctx.app, reader)))}
405410 ${lagSection(lagMinutes.readings())}
406411 <div id="sys-stalls">${stallSection(stalls, worstStallsTable(worst, nowMono, Date.now()))}</div>
412 ${budgetSection(timePhase(ctx.request, "budgets", () => budgetRowsCached(perfRecord)))}
407413 <div id="sys-slow-pages">${slowPagesSection(timePhase(ctx.request, "slowpages", () => slowPagesCached(ctx.app.db)))}</div>
408414 <div id="sys-readers">${readersSection(readerPaces())}</div>
409415 <div id="sys-caches">${cachesSection(pageCaches())}</div>
test/system-budget.test.ts added +148
@@ -0,0 +1,148 @@
1 /** /system, the telemetry API and the MCP `system` tool read PAGE_BUDGETS beside the measured median (#11457). */

The drift test would still pass if the page's words and the API's words diverged, and nothing covers budgetRowsCached or the terminal. See change request 2.

2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createToken } from "../src/core/tokens.ts";
8 import { createUser, type User } from "../src/core/users.ts";
9 import { serve, type Servers } from "../src/http/server.ts";
10 import { budgetLine, budgetRows, budgetRowsCached, forgetBudgetRows } from "../src/perf/budget_read.ts";
11 import { PAGE_BUDGETS } from "../src/perf/page_budget.ts";
12 import { perfRecord } from "../src/perf/live.ts";
13 import { createPerfRecord } from "../src/perf/record.ts";
14 import { createForgeApp, Session } from "../src/tui/forge.ts";
15 import { budgetLines, systemScreen } from "../src/tui/screens/system.ts";
16 import { headlessTerminal } from "../src/tui/term/terminal.ts";
17 import { settleApp } from "./helpers/tui.ts";
18 import { quietLogs } from "./helpers/globals.ts";
19
20 quietLogs();
21
22 /** Five signed-in home requests at 90 ms: over home's 50 ms budget, in the process-wide record the page reads. */
23 function seedOver(at = Date.now()): void {
24 for (let i = 0; i < 5; i++) perfRecord.add("/", { at, ms: 90, signedIn: true, phases: [] });
25 forgetBudgetRows();
26 }
27
28 let dataDir = "";
29 let app: App;
30 let servers: Servers;
31 let token = "";
32 let alice: User;
33
34 beforeAll(async () => {
35 dataDir = await mkdtemp(join(tmpdir(), "forge-sysbudget-"));
36 app = createApp({ dataDir, port: 0 });
37 servers = await serve(app);
38 const u = await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "correct horse battery 9", isAdmin: true });
39 if (!u.ok) throw new Error("alice");
40 alice = u.value;
41 token = createToken(app.db, u.value.id, "t", "admin").token;
42 }, 30_000);
43 afterAll(async () => {
44 servers.stop();
45 app.close();
46 await rm(dataDir, { recursive: true, force: true });
47 });
48
49 test("the median is the middle of the window's real requests; over budget is one plain line", () => {
50 const rec = createPerfRecord();
51 const now = Date.now();
52 // Home budget is 50 ms: five signed-in requests at 10, 20, 90, 100, 110 → median 90.
53 for (const ms of [10, 20, 90, 100, 110]) rec.add("/", { at: now, ms, signedIn: true, phases: [] });
54 rec.add("/", { at: now, ms: 1, signedIn: false, phases: [] }); // a stranger's read is not the signed-in budget's
55 rec.add("/explore", { at: now, ms: 5, signedIn: true, phases: [] }); // too few to judge
56 const rows = budgetRows(rec, now);
57 const home = rows.find((r) => r.pattern === "/")!;
58 expect(home.medianMs).toBe(90);
59 expect(home.requests).toBe(5);
60 expect(home.over).toBe(true);
61 expect(home.says).toBe("home: median 90 ms is over its 50 ms budget (budget 50 ms, 46 SQL statements).");
62 const explore = rows.find((r) => r.pattern === "/explore")!;
63 expect(explore.medianMs).toBeNull();
64 expect(explore.over).toBe(false);
65 expect(explore.says).toContain("not measured yet");
66 expect(budgetLine({ page: "x", budgetMs: 50, budgetStatements: 3, medianMs: 12, over: false })).toBe("x: median 12 ms, within its 50 ms (budget 50 ms, 3 SQL statements).");
67 });
68
69 test("one row per PAGE_BUDGETS entry, on /system, the API and the MCP tool, in the same words", async () => {
70 const auth = { authorization: `Bearer ${token}` };
71 const patterns = PAGE_BUDGETS.map((b) => b.pattern);
72 seedOver();
73 const page = await (await fetch(`${servers.url}/system`, { headers: auth })).text();
74 const section = page.slice(page.indexOf('id="sys-budget"'));
75 expect([...section.matchAll(/data-page="([^"]*)"/g)].map((m) => m[1]!.replaceAll("&#x2F;", "/").replaceAll("&#47;", "/"))).toEqual(patterns);
76
77 const api = (await (await fetch(`${servers.url}/api/v1/telemetry/system`, { headers: auth })).json()) as any;
78 expect(api.budgets.map((r: any) => r.pattern)).toEqual(patterns);
79
80 const res = await fetch(`${servers.url}/mcp`, {
81 method: "POST",
82 headers: { "content-type": "application/json", accept: "application/json, text/event-stream", ...auth },
83 body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "system", arguments: {} } }),
84 });
85 const mcp = JSON.parse(((await res.json()) as any).result.content[0].text);
86 expect(mcp.budgets).toEqual(api.budgets);
87 for (const r of api.budgets) {
88 const b = PAGE_BUDGETS.find((x) => x.pattern === r.pattern)!;
89 expect(r.budgetMs).toBe(b.medianMs);
90 expect(r.budgetStatements).toBe(b.statements);
91 }
92 });
93
94 const OVER = "home: median 90 ms is over its 50 ms budget (budget 50 ms, 46 SQL statements).";
95
96 test("an over-budget page says the same string on /system, the API, the MCP tool and the terminal", async () => {
97 seedOver();
98 const auth = { authorization: `Bearer ${token}` };
99 const page = await (await fetch(`${servers.url}/system`, { headers: auth })).text();
100 const api = (await (await fetch(`${servers.url}/api/v1/telemetry/system`, { headers: auth })).json()) as any;
101 const res = await fetch(`${servers.url}/mcp`, {
102 method: "POST",
103 headers: { "content-type": "application/json", accept: "application/json, text/event-stream", ...auth },
104 body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "system", arguments: {} } }),
105 });
106 const mcp = JSON.parse(((await res.json()) as any).result.content[0].text);
107 const says = (b: any[]) => b.find((r) => r.pattern === "/").says;
108 expect(says(api.budgets)).toBe(OVER);
109 expect(says(mcp.budgets)).toBe(OVER);
110 expect(page.replaceAll("&#x28;", "(").replaceAll("&#x29;", ")")).toContain(OVER);
111 // each over page is its own row, not one paragraph
112 expect(page).not.toContain("<details id=\"sys-budget-rows\"");
113 expect(page.match(/data-over="1"/g)?.length).toBe(1);
114 });
115
116 test("budgetRowsCached keeps rows for 5 s, and a different record is read afresh", () => {
117 const rec = createPerfRecord();
118 const t = 1_000_000_000_000;
119 forgetBudgetRows();
120 const first = budgetRowsCached(rec, t);
121 for (let i = 0; i < 5; i++) rec.add("/", { at: t, ms: 90, signedIn: true, phases: [] });
122 expect(budgetRowsCached(rec, t + 4000)).toBe(first); // kept
123 expect(budgetRowsCached(rec, t + 4000).find((r) => r.pattern === "/")!.over).toBe(false);
124 const later = budgetRowsCached(rec, t + 6000); // past the keep
125 expect(later).not.toBe(first);
126 expect(later.find((r) => r.pattern === "/")!.over).toBe(true);
127 const other = createPerfRecord();
128 expect(budgetRowsCached(other, t + 6001).find((r) => r.pattern === "/")!.over).toBe(false); // a new record busts it
129 });
130
131 test("the terminal's System screen shows the same rows to an admin and nothing to a guest", async () => {
132 seedOver();
133 const identity = (user?: User) => ({ sshUsername: user?.username ?? "git", remoteAddress: "127.0.0.1", fingerprint: "SHA256:t", user });
134 const lines = budgetLines(new Session(app, identity(alice)));
135 expect(lines[0]).toBe("Page budgets, last 15 min");
136 const flat = lines.join(" ").replace(/\s+/g, " ");
137 for (const r of budgetRowsCached(perfRecord)) expect(flat).toContain(r.says);
138 expect(flat).toContain(OVER);
139 expect(budgetLines(new Session(app, identity()))).toEqual([]);
140
141 const terminal = headlessTerminal(120, 60);
142 const { app: tui, session } = createForgeApp(app, identity(alice), { terminal, exitHooks: false });
143 tui.start();
144 tui.ctx.push(systemScreen(tui.ctx, ["CPU"], session));
145 await settleApp(tui);
146 expect(tui.screen.toText()).toContain("Page budgets");
147 tui.stop?.();
148 });
test/system-parts.test.ts +2 −1
@@ -54,12 +54,13 @@ test("one <h2> per part, each in a settings-block, and no card", async () => {
5454 "Processes",
5555 "Loop lag, last 20 minutes",
5656 "Main-thread stalls, last hour",
57 "Page budgets",
5758 "Telemetry readers",
5859 "Code page caches, since start",
5960 "Forge memory",
6061 ]);
6162 expect(main.match(/<section class="settings-block"/g)?.length).toBe(heads.length);
62 for (const id of ["sys-dup", "summary", "cores", "mem", "temps", "sys-disks", "sys-procs", "sys-lag", "sys-stalls", "sys-readers", "sys-caches"]) {
63 for (const id of ["sys-dup", "summary", "cores", "mem", "temps", "sys-disks", "sys-procs", "sys-lag", "sys-stalls", "sys-budget", "sys-readers", "sys-caches"]) {
6364 expect(main).toContain(`id="${id}"`);
6465 }
6566 expect(main).toMatch(/<section class="settings-block"><h2>Disks and network<\/h2>\s*<ul class="list" id="sys-disks">/);
test/tui-system.test.ts +1 −1
@@ -211,7 +211,7 @@ describe("tui system telemetry grant", () => {
211211 });
212212
213213 test("the System command passes the session, so the admin has the line and g", async () => {
214 const { app: tui, session } = createForgeApp(app, identity(carol), { terminal: headlessTerminal(120, 50), exitHooks: false });
214 const { app: tui, session } = createForgeApp(app, identity(carol), { terminal: headlessTerminal(120, 60), exitHooks: false });
215215 tui.start();
216216 const system = forgeCommands(session)(tui.ctx).find((c) => c.id === "system");
217217 system!.run();

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs