9863d96eef
Adversarial review of PR #61 found one real coverage loss, and it is the defect class that PR's own body names: an assertion that survives the deletion of the thing it was about. `shell.spec.js` lost "a socket that opened and then dropped reads as disconnected, and stays that way" and both MutationObserver negative assertions. Reverting PR #21's routing half in `web/app.js` — the socket `error` and `close` arms calling `setShell` instead of `fail` — left the node suite 26/26 and Playwright 13/13 green. `shell.test.js` cannot cover it, and why is the whole boundary argument: "a loud failure DISARMS the watchdog" tests `createShell` in isolation and cannot see whether `app.js` ROUTES the close event through `fail()`. That routing is the half gotcha 48 says keeps getting lost; this was the third time, after two earlier mis-confirmations that simulated the `opened` flag and not the routing. Both tests are back under their original names, so the two `####` headings in context/test-evidence/clients.md name live tests again. They cost ~75ms each rather than ~9s, because `page.clock` fakes the PAGE's timers: the page, the socket, the listeners and the `setTimeout` call are all real and only the passage of time is simulated. That is not a weakening, and the injections are the proof in both directions — if `runFor` did not advance the page's timers, a broken routing could not surface the watchdog's message and they would pass: - both arms reverted to `setShell` -> "…and stays that way" fails - close arm only -> "…and stays that way" fails - close arm drops the `opened` flag -> "…the watchdog stays out of it" fails Three injections, three outcomes, none of them the unit test. The helper guards its own premise, because it turned out not to: `runFor` without `clock.install()` does nothing AND throws nothing, so a test that forgot the install asserted "the watchdog never spoke" against a watchdog that never ran — 6/6 green. `outlastTheWatchdog` now requires the page's own `Date.now()` to have moved, and that omission fails by name. Also: `render_surface.test.js` matched `model.<field>` over source INCLUDING comments, so un-painting a field and mentioning it in a comment passed — demonstrated against the pre-fix version, then fixed and re-demonstrated. And the PR's speed claim is corrected where it is documented. The eleven deleted per-widget tests cost 3.3s between them against 3.4s for the three contract tests replacing them: they were free. Essentially all of 36.9s -> 15.5s was two real-timer waits, one of them this guard. The wait was the cost, not the test. Filed, not fixed: improvise-igxp (the help overlay's rows are asserted as present, never as content — inherited verbatim from the test contract.spec.js replaced) and improvise-4ktt (the js job's bundle-freedom is a shell-glob convention, not a gate). The `####`-vs-`###` blind spot in --check-evidence, which is why the dangling heading survived the gate, is a comment on improvise-r0qa. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
60 lines
2.6 KiB
JavaScript
60 lines
2.6 KiB
JavaScript
// The RenderModel→DOM boundary, statically: a projected field that nothing
|
|
// paints is the bug four widgets shared. See context/code/clients.md.
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const read = (rel) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
|
|
|
|
/// Comments name model fields routinely, so a field mentioned only in one
|
|
/// would read as painted. Strip them before asking what the code touches.
|
|
const code = (rel) =>
|
|
read(rel)
|
|
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
.replace(/(^|[^:"'`])\/\/.*$/gm, "$1");
|
|
|
|
/// Fields the page deliberately does not read, each with the reason. A field
|
|
/// belongs here only with an argument; the list is the debt, written down.
|
|
// improvise-z754 put the title bar's facts in one shared cache and taught
|
|
// `src/draw.rs::draw_title` to fold it; this page never learned to. That is
|
|
// improvise-5jna, open, and this list is what stops it being forgotten.
|
|
const UNPAINTED = {
|
|
doc_name: "improvise-5jna: the title bar is drawn by the TUI and not here",
|
|
doc_dirty: "improvise-5jna",
|
|
doc_stored_as: "improvise-5jna",
|
|
};
|
|
|
|
/// The `pub` fields of `RenderModel`, read off the Rust that defines it.
|
|
function renderModelFields() {
|
|
const src = read("../../crates/improvise-wasm-client/src/render.rs");
|
|
const start = src.indexOf("pub struct RenderModel {");
|
|
assert.ok(start > 0, "RenderModel moved — this test reads the struct itself");
|
|
const body = src.slice(start, src.indexOf("\n}", start));
|
|
const fields = [...body.matchAll(/\n {4}pub ([a-z_0-9]+):/g)].map((m) => m[1]);
|
|
assert.ok(fields.length > 10, `found only ${fields.length} fields`);
|
|
return fields;
|
|
}
|
|
|
|
test("every field the render model projects is painted, or named as debt", () => {
|
|
// Both halves of the page: `app.js` paints, `shell.js` plans the grid.
|
|
const page = code("../app.js") + code("../shell.js");
|
|
const unpainted = renderModelFields().filter((field) => !page.includes(`model.${field}`));
|
|
assert.deepEqual(
|
|
unpainted.sort(),
|
|
Object.keys(UNPAINTED).sort(),
|
|
"a projected field reaches no element — paint it, or add it to UNPAINTED with the reason",
|
|
);
|
|
});
|
|
|
|
test("every element the page looks up exists in the page", () => {
|
|
const app = read("../app.js");
|
|
const html = read("../index.html");
|
|
const ids = [...app.matchAll(/getElementById\("([^"]+)"\)/g)].map((m) => m[1]);
|
|
assert.ok(ids.length >= 8, `found only ${ids.length} element lookups`);
|
|
for (const id of ids) {
|
|
assert.ok(html.includes(`id="${id}"`), `app.js looks up #${id}, which index.html has not got`);
|
|
}
|
|
});
|