Files
improvise/web/tests/render_surface.test.js
T
Edward Langley 9863d96eef fix(harness): restore the close-routing guard PR #61 dropped, on a faked clock
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>
2026-09-20 13:59:06 -07:00

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`);
}
});