// 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. // Empty since improvise-5jna painted the title bar. Note what this check CANNOT // see, and why the harness asserts the painted text too: it is a substring // search, so a field named by a function nothing calls reads as painted. const UNPAINTED = {}; /// 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", ); }); /// One level up, and the direction that would have caught improvise-p7lh: /// the check above is closed under what `RenderModel` chose to expose, so a /// cache field that never reaches it is invisible to it. Reason per entry. const UNREAD_CACHE = { "grid.col_widths": "the browser lets the table size its own columns; improvise-pcje is the parity residue", }; /// `ClientState`'s cache fields, and the `pub` fields of each cache type. function cacheFields() { const client = read("../../crates/improvise-protocol/src/client.rs"); const render = read("../../crates/improvise-protocol/src/render.rs"); const state = client.slice( client.indexOf("pub struct ClientState {"), client.indexOf("\n}", client.indexOf("pub struct ClientState {")), ); assert.ok(state.length > 0, "ClientState moved — this test reads the struct itself"); const caches = [...state.matchAll(/\n {4}pub ([a-z_0-9]+): ([A-Za-z]*Cache)\b/g)]; assert.ok(caches.length >= 5, `found only ${caches.length} widget caches`); return caches.flatMap(([, field, type]) => { const start = render.indexOf(`pub struct ${type} {`); if (start < 0) return []; // Defined elsewhere (BasisCache): not a drawn widget. const body = render.slice(start, render.indexOf("\n}", start)); return [...body.matchAll(/\n {4}pub ([a-z_0-9]+):/g)].map((m) => `${field}.${m[1]}`); }); } // improvise-p7lh: `GridCache::totals` and `page_coords` were projected, folded // and correctly maintained, and no front end read either for months. test("every field of every widget cache reaches the browser, or is named as debt", () => { // The browser's whole decision surface: what it renders, what it resolves a // key against, and the fold. A field none of these names is drawn by nothing. // Whitespace-stripped, because rustfmt breaks `state.grid.totals` across // three lines and a substring search would then read it as unread. const client = [ "../../crates/improvise-wasm-client/src/render.rs", "../../crates/improvise-wasm-client/src/lib.rs", "../../crates/improvise-wasm-client/src/socket.rs", "../../crates/improvise-protocol/src/resolve.rs", ] .map((rel) => code(rel).replace(/\s+/g, "")) .join("\n"); const unread = cacheFields().filter((path) => { const [cache, field] = path.split("."); // Either the field itself is read, or the WHOLE cache is handed to a // shared derivation — which is a real hole, named in the evidence file. if (client.includes(`state.${cache}.${field}`)) return false; if (field === "cells" && client.includes(`state.${cache}.cell(`)) return false; // `&state.grid.row_frame` CONTAINS `&state.grid`, so a plain substring // test here makes every GridCache field read as handed over. Demonstrated. return !new RegExp(`&state\\.${cache}(?![.\\w])`).test(client); }); assert.deepEqual( unread.sort(), Object.keys(UNREAD_CACHE).sort(), "a maintained cache field reaches no front end — read it, or add it to UNREAD_CACHE 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`); } });