Public: anyone can read this; the crew can change it.
/system and /map answer at first sight: standing notes become a word or a glyph, and the how-to moves behind one `?` #11124
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/11111-system-and-map-answer-at-first-sight-sta → main 4 commits fork 4 threads · 4 unresolved
6 files
changed +146
−40
src/web/pages/map.ts +9 −10
| @@ -52,7 +52,7 @@ import { | ||
| 52 | 52 | import { liveRegionsJs } from "../client.ts"; |
| 53 | 53 | import { html, type Html } from "../html.ts"; |
| 54 | 54 | import { page } from "../layout.ts"; |
| 55 | import { btn, empty, row } from "../ui.ts"; | |
| 55 | import { btn, disclosure, empty, row } from "../ui.ts"; | |
| 56 | 56 | import { |
| 57 | 57 | parkedTwice, |
| 58 | 58 | struckOut, |
| @@ -1506,14 +1506,13 @@ export async function mapPage(ctx: Ctx): Promise<Response> { | ||
| 1506 | 1506 | const body = html`${repoHeader(ctx, repo, "map")} |
| 1507 | 1507 | <div class="page-head mt"> |
| 1508 | 1508 | <h1>Map</h1> |
| 1509 | <p class="muted"> | |
| 1510 | What waits for what. The top edge is ready now; the chain in the accent | |
| 1511 | is the least work that still has to happen in order. | |
| 1512 | <a href="${repoBase(repo)}/space">The same work as a space to move through</a>. | |
| 1513 | <a href="${repoBase(repo)}/milestones">How far each milestone is</a>. | |
| 1514 | <a href="${repoBase(repo)}/next">Next up</a> is the one ranked list the crew builds from. | |
| 1515 | </p> | |
| 1509 | <span class="spacer"></span> | |
| 1510 | <a href="${repoBase(repo)}/space">Space</a><a href="${repoBase(repo)}/milestones">Milestones</a><a href="${repoBase(repo)}/next">Next up</a> | |
| 1516 | 1511 | </div> |
| 1512 | ${disclosure("? How to read this", html`<div class="prose break" id="map-help"> | |
Principle 1: the third paragraph re-lists Space, Milestones and Next up, which the head already links. Remove it; the head links carry that. |
||
| 1513 | <p>What waits for what. The top edge is ready now; the chain in the accent is the least work that still has to happen in order.</p> | |
| 1514 | <p>The first list is what nothing stands in front of; when it is empty, everything open waits for something. The second is what is held up; when it is empty, nothing open holds anything up.</p> | |
| 1515 | </div>`, { id: "map-more" })} | |
| 1517 | 1516 | <div |
| 1518 | 1517 | class="map-wrap" |
| 1519 | 1518 | id="map-live" |
| @@ -1542,7 +1541,7 @@ export async function mapPage(ctx: Ctx): Promise<Response> { | ||
| 1542 | 1541 | ? "holds nothing up" |
| 1543 | 1542 | : `finishing it frees ${r.holdsUp}` |
| 1544 | 1543 | }`, |
| 1545 | "Nothing is ready: everything open waits for something.", | |
| 1544 | "Nothing ready.", | |
| 1546 | 1545 | tellHref, |
| 1547 | 1546 | { |
| 1548 | 1547 | writer: |
| @@ -1564,7 +1563,7 @@ export async function mapPage(ctx: Ctx): Promise<Response> { | ||
| 1564 | 1563 | blocked, |
| 1565 | 1564 | (r) => |
| 1566 | 1565 | `${wordFor(r.issue, readySet, marks)} · ${pillarOf(r)}${r.waitsFor > 0 ? `waits for ${r.waitsFor} · ` : ""}holds up ${r.holdsUp}`, |
| 1567 | "Nothing open holds anything up.", | |
| 1566 | "Nothing blocked.", | |
| 1568 | 1567 | tellHref, |
| 1569 | 1568 | )} |
| 1570 | 1569 | </aside> |
src/web/pages/system.ts +29 −22
| @@ -43,7 +43,7 @@ import { KEPT_PAGE_BYTES, keptPageStats } from "./kept_page.ts"; | ||
| 43 | 43 | import { COLUMN_BYTES, PARTS_BYTES, keptPartsStats, treeCacheStats } from "./tree_cache.ts"; |
| 44 | 44 | import { featureHealth, type FeatureHealth } from "../../system/feature_health.ts"; |
| 45 | 45 | import { egressReportedLine } from "../../system/egress_line.ts"; |
| 46 | import { badge, chartBox, meter, pill, row, time } from "../ui.ts"; | |
| 46 | import { badge, chartBox, disclosure, meter, pill, row, time } from "../ui.ts"; | |
| 47 | 47 | import { childDirOf, listChildren } from "../../actions/spawn.ts"; |
| 48 | 48 | import { childrenUse, realSelf, selfUse, shareOf, sizeWords, useWords, type ChildUseRow, type SelfUse } from "../../spawn/usage.ts"; |
| 49 | 49 | |
| @@ -84,9 +84,13 @@ export function selfSection(self: SelfUse | null): Html { | ||
| 84 | 84 | ${meterRow("cpu", use.cores, use.limits.cpu, use.cores === null ? `measuring · ${w.cpu}` : w.cpu)} |
| 85 | 85 | ${meterRow("disk", use.diskMiB, use.limits.diskMiB, w.disk)} |
| 86 | 86 | </ul> |
| 87 | <p class="small muted">Set by the forge that made this one. ${self.from === "process" ? "Memory and CPU count this process alone. " : ""}Disk is measured every 30 seconds.</p></section>`; | |
| 87 | </section>`; | |
| 88 | 88 | } |
| 89 | 89 | |
| 90 | /** What this forge's own size section leaves to the page's `?`: who set the size and what is measured. */ | |
| 91 | export const selfHelp = (self: SelfUse | null): Html | null => | |
| 92 | self === null ? null : html`<p id="sys-self-help">Size is set by the forge that made this one. ${self.from === "process" ? "Memory and CPU count this process alone. " : ""}Disk is measured every 30 seconds.</p>`; | |
| 93 | ||
| 90 | 94 | /** What a child row says beside its meter: a cell's three figures, or why there are none. */ |
| 91 | 95 | const childSaid = (r: ChildUseRow): string => { |
| 92 | 96 | if (r.use.kind === "shared") return "shared with this forge"; |
| @@ -142,7 +146,7 @@ function memorySection(ctx: Ctx): Html { | ||
| 142 | 146 | const series = (title: string, pick: (h: (typeof hours)[number]) => number) => |
| 143 | 147 | html`<div><h3 class="small">${title}</h3>${chartBox("line", hours.map((h) => ({ label: hhmm(h.at).slice(11), value: pick(h) / 1_048_576 })), title)}</div>`; |
| 144 | 148 | return html`<section class="settings-block" id="sys-memory"><h2>Forge memory</h2>${hours.length === 0 |
| 145 | ? html`<p class="muted">No samples yet: the forge samples its memory every minute, so the first hour appears shortly.</p>` | |
| 149 | ? html`<p class="muted">No samples yet.</p>` | |
| 146 | 150 | : html`<div class="plots">${series("RSS (MiB, hourly mean)", (h) => h.rssMean)}${series("JS heap (MiB, hourly mean)", (h) => h.heapMean)}</div> |
| 147 | 151 | <div class="table-scroll"><table class="table" aria-label="Forge memory by hour"> |
| 148 | 152 | <thead><tr><th scope="col">Hour (UTC)</th><th scope="col" class="num">RSS mean</th><th scope="col" class="num">RSS max</th><th scope="col" class="num">Heap mean</th><th scope="col" class="num">Heap max</th></tr></thead> |
| @@ -172,11 +176,10 @@ export const pageCaches = (): CacheRow[] => [ | ||
| 172 | 176 | /** `pageCaches`' rows as a table; the hit rate is hits over hits and misses since the process started. */ |
| 173 | 177 | export function cachesSection(rows: readonly CacheRow[]): Html { |
| 174 | 178 | const rate = (r: CacheRow) => (r.hits + r.misses === 0 ? "—" : `${((100 * r.hits) / (r.hits + r.misses)).toFixed(1)}%`); |
| 175 | return html`<section class="settings-block"><h2>Code page caches</h2> | |
| 176 | <p class="small muted">Counts since this process started.</p> | |
| 179 | return html`<section class="settings-block"><h2>Code page caches, since start</h2> | |
| 177 | 180 | <div class="table-scroll"><table class="table" aria-label="Code page caches"> |
| 178 | <thead><tr><th scope="col">Cache</th><th scope="col">Holds</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> | |
| 179 | <tbody>${rows.map((r) => html`<tr data-cache="${r.name}" data-bytes="${String(r.bytes)}"><td>${r.name}</td><td class="small muted">${r.holds}</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> | |
| 181 | <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> | |
| 182 | <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> | |
outdated
Principle 8 / keyboard and touch: |
||
| 180 | 183 | </table></div></section>`; |
| 181 | 184 | } |
| 182 | 185 | |
| @@ -199,8 +202,7 @@ export function featuresSection(rows: readonly FeatureHealth[]): Html { | ||
| 199 | 202 | |
| 200 | 203 | /** The last hour's stalls by route, most milliseconds first; the rows are `stallHour`'s. */ |
| 201 | 204 | export function stallSection(rows: readonly RouteStall[], worst: Html = html``): Html { |
| 202 | return html`<section class="settings-block"><h2>Main-thread stalls, last hour</h2> | |
| 203 | <p class="small muted">Past 200 ms, as of this page's load; <code>/metrics</code> has the series.</p>${ | |
| 205 | return html`<section class="settings-block"><h2>Main-thread stalls, last hour</h2>${ | |
| 204 | 206 | rows.length === 0 |
| 205 | 207 | ? html`<p class="muted">No stalls in the last hour.</p>` |
| 206 | 208 | : html`<div class="table-scroll"><table class="table" aria-label="Main-thread stalls by route, last hour"> |
| @@ -263,7 +265,7 @@ export function readersSection(rows: readonly ReaderPace[]): Html { | ||
| 263 | 265 | export function regions(v: SystemView): Record<string, string> { |
| 264 | 266 | const dup: Html = |
| 265 | 267 | v.duplicates.length === 0 |
| 266 | ? html`<p id="duplicates" data-count="0" class="muted">No test file is running in more than one check.</p>` | |
| 268 | ? html`<p id="duplicates" data-count="0" class="muted">✓ no repeated tests</p>` | |
| 267 | 269 | : html`<p id="duplicates" data-count="${String(v.duplicates.length)}"> |
| 268 | 270 | <strong>${String(v.duplicates.length)} test file${v.duplicates.length === 1 ? " is" : "s are"} |
| 269 | 271 | running in more than one check at once:</strong> |
| @@ -323,12 +325,14 @@ export async function systemPage(ctx: Ctx): Promise<Response> { | ||
| 323 | 325 | const [holder, pids] = timePhase(ctx.request, "caps", () => [memoryCapHolder(), processCapHolder()] as const); |
| 324 | 326 | const caps: Html = |
| 325 | 327 | holder.by === "kernel" |
| 326 | ? html`<p class="small" id="cell-caps" data-by="kernel">Each cell's memory is capped by the kernel (its own cgroup's <code>memory.max</code>): a cell over it is killed alone.</p>` | |
| 327 | : html`<p class="small" id="cell-caps" data-by="watch">Each cell's memory is watched once a second and the cell stopped over its cap; the kernel does not cap it (${holder.reason}).</p>`; | |
| 328 | ? html`<p id="cell-caps" data-by="kernel">Each cell's memory is capped by the kernel (its own cgroup's <code>memory.max</code>): a cell over it is killed alone.</p>` | |
| 329 | : html`<p id="cell-caps" data-by="watch">Each cell's memory is watched once a second and the cell stopped over its cap; the kernel does not cap it (${holder.reason}).</p>`; | |
| 328 | 330 | const processCaps: Html = |
| 329 | 331 | pids.by === "kernel" |
| 330 | ? 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>` | |
| 331 | : 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>`; | |
| 332 | ? html`<p 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>` | |
| 333 | : html`<p 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>`; | |
| 334 | // The two caps as a word each beside the machine's figures; the sentences are in the `?`. | |
| 335 | const capWords: Html = html`<div class="row-meta" id="cap-words">${pill(`cell memory: ${holder.by === "kernel" ? "kernel-capped" : "watched"}`)} ${pill(`cell processes: ${pids.by === "kernel" ? "kernel-capped" : "watched"}`)}</div>`; | |
| 332 | 336 | const egressSaid = egressReportedLine(ctx.app.db); |
| 333 | 337 | const egressLine: Html | null = |
| 334 | 338 | egressSaid === null ? null : html`<p class="small" id="egress-reported">${egressSaid} <a href="/settings/capacity">Capacity</a></p>`; |
| @@ -343,22 +347,25 @@ export async function systemPage(ctx: Ctx): Promise<Response> { | ||
| 343 | 347 | const rendering = performance.now(); |
| 344 | 348 | const r = regions(v); |
| 345 | 349 | const at = (id: string): Html => raw(r[id]!); |
| 350 | const help = disclosure("? How to read this", html`<div class="prose break" id="sys-help"> | |
| 351 | <p>The machine now, updated in place about every 2 s over one event stream. Its history is on <a href="/boards">Boards</a> (the machine source).</p> | |
| 352 | ${caps}${processCaps}${selfHelp(self)} | |
| 353 | <p>Stalls are main-thread pauses past 200 ms, as of this page's load; <code>/metrics</code> has the series.</p> | |
| 354 | <p>Cache counts are since this process started. What each cache holds:</p> | |
| 355 | <ul>${pageCaches().map((c) => html`<li><strong>${c.name}</strong>: ${c.holds}</li>`)}</ul> | |
| 356 | <p>The forge samples its memory every minute, so the first hour of its chart appears shortly after a start.</p> | |
| 357 | </div>`, { id: "sys-more" }); | |
| 346 | 358 | const body = html`<div class="page-head"> |
| 347 | 359 | <h1>System</h1> |
| 348 | <p class="muted"> | |
| 349 | The machine now, updated in place about every 2 s over one event | |
| 350 | stream; <a href="/system?pause">pause</a>. Its history is on | |
| 351 | <a href="/boards">Boards</a> (the machine source); the pipeline is on | |
| 352 | <a href="/boards">Boards</a>. | |
| 353 | </p> | |
| 360 | <span class="spacer"></span><a href="/system?pause">pause</a> | |
| 354 | 361 | </div> |
| 362 | ${help} | |
| 355 | 363 | <div id="sys-dup">${at("sys-dup")}</div> |
| 356 | 364 | <section class="settings-block"><h2>Machine</h2> |
| 357 | 365 | <div id="summary" class="row-meta">${at("summary")}</div> |
| 358 | 366 | <ul class="list" id="cores" aria-label="Per-core CPU, percent, with its last readings">${at("cores")}</ul> |
| 359 | 367 | <ul class="list" id="mem">${at("mem")}</ul> |
| 360 | ${caps} | |
| 361 | ${processCaps} | |
| 368 | ${capWords} | |
| 362 | 369 | ${egressLine} |
| 363 | 370 | <p class="small mono" id="temps">${at("temps")}</p></section> |
| 364 | 371 | ${selfSection(self)} |
test/first-sight-notes.test.ts added +99
| @@ -0,0 +1,99 @@ | ||
| 1 | /** | |
| 2 | * /system and /map answer at first sight (#11111): the standing explanatory notes became a word or | |
| 3 | * a glyph beside the figure they explain, or moved behind one `?` (a closed `<details>`), the cut | |
| 4 | * /agents got in #11078. A note is any element, whatever its tag, that carries a note class token | |
| 5 | * (muted, small, note, hint, why) and says more than four words; the count is of what the page | |
| 6 | * renders outside its disclosures, and each page's ceiling is half of what main rendered. | |
| 7 | */ | |
| 8 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 9 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 10 | import { tmpdir } from "node:os"; | |
| 11 | import { join } from "node:path"; | |
| 12 | import { createApp, type App } from "../src/app.ts"; | |
| 13 | import { openIssue } from "../src/actions/issues.ts"; | |
| 14 | import { createRepo } from "../src/actions/repos.ts"; | |
| 15 | import { createToken } from "../src/core/tokens.ts"; | |
| 16 | import { createUser } from "../src/core/users.ts"; | |
| 17 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 18 | import { quietLogs } from "./helpers/globals.ts"; | |
| 19 | ||
| 20 | quietLogs(); | |
| 21 | ||
| 22 | const NOTE = ["muted", "small", "note", "hint", "why"]; | |
| 23 | ||
| 24 | /** The note elements of a page outside its closed disclosures: any tag, a note class, over four words. */ | |
| 25 | function notesOf(page: string): string[] { | |
| 26 | const main = page.slice(page.indexOf(`<div class="page-head`)).replace(/<details\b[\s\S]*?<\/details>/g, ""); | |
| 27 | const out: string[] = []; | |
| 28 | for (const m of main.matchAll(/(?=<([a-z0-9]+)\b[^>]*\bclass="([^"]*)"[^>]*>([\s\S]*?)<\/\1>)/g)) { | |
| 29 | const classes = m[2]!.split(/\s+/); | |
| 30 | // Data is not a note: a monospace reading, or the label of the buttons it sits beside. | |
| 31 | if (!classes.some((c) => NOTE.includes(c)) || classes.includes("mono") || m[3]!.includes("<button")) continue; | |
| 32 | const words = m[3]!.replace(/<[^>]*>/g, " ").split(/\s+/).filter(Boolean); | |
| 33 | if (words.length > 4) out.push(words.join(" ")); | |
| 34 | } | |
| 35 | return out; | |
| 36 | } | |
| 37 | ||
| 38 | /** What main rendered for the same fixtures, counted before the cut. */ | |
| 39 | const MAIN_SYSTEM = 12; | |
| 40 | const MAIN_MAP = 2; | |
| 41 | ||
| 42 | /** Every explanation the cut moved is still on the page, closed behind one `?` each. */ | |
| 43 | const HELP = { "/system": "sys-more", "/alice/plan/map": "map-more" } as const; | |
| 44 | ||
| 45 | let dir: string; | |
| 46 | let app: App; | |
| 47 | let servers: Servers; | |
| 48 | let token: string; | |
| 49 | ||
| 50 | beforeAll(async () => { | |
| 51 | dir = await mkdtemp(join(tmpdir(), "forge-first-sight-")); | |
| 52 | app = createApp({ dataDir: dir, port: 0 }); | |
| 53 | servers = await serve(app); | |
| 54 | const u = await createUser(app.db, { | |
| 55 | origin: "seed", trusted: true, username: "alice", email: "[email protected]", | |
| 56 | password: "a-good-passphrase", adminIfFirst: true, | |
| 57 | }); | |
| 58 | if (!u.ok) throw new Error(u.error); | |
| 59 | token = createToken(app.db, u.value.id, "t", "admin").token; | |
| 60 | const made = await createRepo(app, u.value, { name: "plan", visibility: "public" }); | |
| 61 | if (!made.ok) throw new Error(String(made.error)); | |
| 62 | if (!openIssue(app, u.value, made.value, { title: "Alpha work" }).ok) throw new Error("issue"); | |
| 63 | }, 30_000); | |
| 64 | afterAll(async () => { | |
| 65 | await servers.stop(); | |
| 66 | await rm(dir, { recursive: true, force: true }); | |
| 67 | }); | |
| 68 | ||
| 69 | const get = async (path: string): Promise<string> => | |
| 70 | (await fetch(servers.url + path, { headers: { authorization: `Bearer ${token}` } })).text(); | |
| 71 | ||
| 72 | describe("first sight", () => { | |
| 73 | test("each page keeps its how-to behind one `?` disclosure, and the action links beside it", async () => { | |
| 74 | for (const [path, id] of Object.entries(HELP)) { | |
| 75 | const page = await get(path); | |
| 76 | expect(page).toContain(`<details id="${id}" class="disclosure">`); | |
| 77 | expect(page).toMatch(/<summary>\? How to read this<\/summary>/); | |
| 78 | } | |
| 79 | const system = await get("/system"); | |
| 80 | expect(system).toMatch(/id="sys-more"[\s\S]*id="cell-caps" data-by="(kernel|watch)"/); | |
| 81 | // A control keeps its word beside it: pause is in the head, not behind the `?`. | |
| 82 | expect(system.slice(system.indexOf('class="page-head"'), system.indexOf('id="sys-more"'))).toContain('href="/system?pause"'); | |
| 83 | expect(system).toMatch(/id="cap-words"[^>]*>\s*<span class="pill"[^>]*>cell memory: (kernel-capped|watched)/); | |
| 84 | // What each cache holds is text inside the `?`, not a hover-only title. | |
| 85 | expect(system.slice(system.indexOf('id="sys-more"'))).toMatch(/<li><strong>kept pages<\/strong>: a stranger's/); | |
| 86 | expect(system).not.toContain('title="a stranger'); | |
| 87 | const map = await get("/alice/plan/map"); | |
| 88 | expect(map.slice(map.indexOf('id="map-more"'), map.indexOf('id="map-live"'))).not.toContain("/milestones"); | |
| 89 | expect(map.slice(map.indexOf('class="page-head'), map.indexOf('id="map-more"'))).toContain("/alice/plan/next"); | |
| 90 | }); | |
| 91 | test("/system renders at most half the notes main did", async () => { | |
| 92 | const notes = notesOf(await get("/system")); | |
| 93 | expect(notes.length).toBeLessThanOrEqual(Math.floor(MAIN_SYSTEM / 2)); | |
outdated
Stray whitespace-only line; remove it. Also |
||
| 94 | }); | |
| 95 | test("/map renders at most half the notes main did", async () => { | |
| 96 | const notes = notesOf(await get("/alice/plan/map")); | |
| 97 | expect(notes.length).toBeLessThanOrEqual(Math.floor(MAIN_MAP / 2)); | |
| 98 | }); | |
| 99 | }); |
test/spawn-self-use.test.ts +5 −3
| @@ -25,7 +25,7 @@ import { | ||
| 25 | 25 | type SelfSource, |
| 26 | 26 | } from "../src/spawn/usage.ts"; |
| 27 | 27 | import { childLines, selfLines } from "../src/tui/screens/system.ts"; |
| 28 | import { childrenSection, selfSection } from "../src/web/pages/system.ts"; | |
| 28 | import { childrenSection, selfHelp, selfSection } from "../src/web/pages/system.ts"; | |
| 29 | 29 | import { quietLogs } from "./helpers/globals.ts"; |
| 30 | 30 | import { settleApp } from "./helpers/tui.ts"; |
| 31 | 31 | import { createForgeApp, forgeCommands } from "../src/tui/forge.ts"; |
| @@ -117,7 +117,9 @@ describe("selfUse", () => { | ||
| 117 | 117 | expect(page).toContain("1.5 of 2 cores"); |
| 118 | 118 | expect(page).toContain("512 MiB of 10 GiB"); |
| 119 | 119 | expect(page).toContain("This forge's size: medium</h2>"); |
| 120 | expect(page).toContain('<p class="small muted">Set by the forge that made this one. Disk is measured every 30 seconds.</p>'); | |
| 120 | // The note moved behind the page's `?` (#11111): the section is the figures alone. | |
| 121 | expect(page).not.toContain("Disk is measured"); | |
| 122 | expect(selfHelp(second)!.text).toContain("Size is set by the forge that made this one. Disk is measured every 30 seconds."); | |
| 121 | 123 | expect(page).not.toContain("memory.current"); |
| 122 | 124 | }); |
| 123 | 125 | |
| @@ -128,7 +130,7 @@ describe("selfUse", () => { | ||
| 128 | 130 | // 2 s of CPU over 4 s = 0.5 cores. |
| 129 | 131 | expect(s.use.cores).toBe(0.5); |
| 130 | 132 | expect(selfLines(s)[1]).toBe(" memory 300 MiB of 2 GiB (this process alone)"); |
| 131 | expect(selfSection(s).text).toContain("Memory and CPU count this process alone."); | |
| 133 | expect(selfHelp(s)!.text).toContain("Memory and CPU count this process alone."); | |
| 132 | 134 | }); |
| 133 | 135 | }); |
| 134 | 136 |
test/system-memory.test.ts +1 −2
| @@ -122,7 +122,6 @@ describe("code page caches (#9514)", () => { | ||
| 122 | 122 | expect(caps).toBe(128 * MiB); |
| 123 | 123 | const html = String(cachesSection([{ name: "kept pages", holds: "<b>", capBytes: 64 * MiB, entries: 3, bytes: 2 * MiB, hits: 3, misses: 1 }])); |
| 124 | 124 | expect(html).toContain('data-cache="kept pages"'); |
| 125 | expect(html).toContain("<b>"); | |
| 126 | 125 | expect(html).toContain("2.0 MiB"); |
| 127 | 126 | expect(html).toContain("64.0 MiB"); |
| 128 | 127 | expect(html).toContain("75.0%"); |
| @@ -130,7 +129,7 @@ describe("code page caches (#9514)", () => { | ||
| 130 | 129 | |
| 131 | 130 | test("the owner's /system shows them", async () => { |
| 132 | 131 | const body = await (await get("alice")).text(); |
| 133 | expect(body).toContain("<h2>Code page caches</h2>"); | |
| 132 | expect(body).toContain("<h2>Code page caches, since start</h2>"); | |
| 134 | 133 | for (const name of ["kept pages", "encoded bodies", "page columns", "folders and refs"]) expect(body).toContain(`data-cache="${name}"`); |
| 135 | 134 | }); |
| 136 | 135 | }); |
test/system-parts.test.ts +3 −3
| @@ -54,7 +54,7 @@ test("one <h2> per part, each in a settings-block, and no card", async () => { | ||
| 54 | 54 | "Processes", |
| 55 | 55 | "Main-thread stalls, last hour", |
| 56 | 56 | "Telemetry readers", |
| 57 | "Code page caches", | |
| 57 | "Code page caches, since start", | |
| 58 | 58 | "Forge memory", |
| 59 | 59 | ]); |
| 60 | 60 | expect(main.match(/<section class="settings-block"/g)?.length).toBe(heads.length); |
| @@ -95,8 +95,8 @@ test("no stalls says so rather than drawing a header over nothing", () => { | ||
| 95 | 95 | expect(stallSection([{ route: "/x", ms: 300, count: 1 }]).text).toContain("<table"); |
| 96 | 96 | }); |
| 97 | 97 | |
| 98 | test("the caches section keeps one short sentence", () => { | |
| 99 | expect(cachesSection([]).text.match(/<p /g)?.length).toBe(1); | |
| 98 | test("the caches section keeps no note: the counts' span is in its heading, what each holds is in the `?`", () => { | |
| 99 | expect(cachesSection([]).text).not.toContain("<p "); | |
| 100 | 100 | }); |
| 101 | 101 | |
| 102 | 102 | test("no memory samples yet says so", () => { |
Three links in a bare
span.mutedseparated by whitespace: at 360px they run together with no spacing and tap targets are tiny. Use the named row/links component with real gaps.