Merge remote-tracking branch 'gh/main' into fix/web-panel-clicks

This commit is contained in:
Edward Langley
2026-09-20 14:36:19 -07:00
6 changed files with 261 additions and 27 deletions
+4 -1
View File
@@ -4,6 +4,7 @@
import init, { Client } from "./pkg/improvise_wasm_client.js";
import {
COL_PX,
HANDSHAKE_TIMEOUT_MS,
ROW_PX,
createShell,
extentOf,
@@ -40,13 +41,15 @@ const client = new Client();
// A peer that never answers the handshake, and the two facts `#status` shows.
// Both are decided in shell.js; this is the wire to the element.
const HANDSHAKE_TIMEOUT_MS = 8000;
const shell = createShell({
onStatus: (text) => {
el.status.textContent = text;
},
timeoutMs: HANDSHAKE_TIMEOUT_MS,
});
// The harness asks whether the watchdog is ARMED, because a negative assertion
// about it is vacuous where it is not (clients.md). Read by tests, nothing else.
window.__improviseShell = shell;
// What the grid's own frame costs, in whole cells, as of the last repaint —
// measured off the painted model, never predicted. context/code/clients.md.
+10
View File
@@ -27,6 +27,11 @@ export function indicator(collapsed) {
export const ROW_PX = 26;
export const COL_PX = 110;
/** How long a peer has to answer the handshake. Exported because the harness
* must jump *past* it: a spec with its own copy can understate it silently
* and every watchdog assertion then passes vacuously (clients.md). */
export const HANDSHAKE_TIMEOUT_MS = 8000;
/** The extent a box of this size can show, in whole DATA cells: the frame is
* charged for first, and at least one cell is always asked for. */
export function extentOf(box, frameCost, metrics = { rowPx: ROW_PX, colPx: COL_PX }) {
@@ -107,6 +112,7 @@ export function createShell({ onStatus, timeoutMs, setTimer = setTimeout, clearT
let shell = "";
let model = "";
let watchdog = null;
let arms = 0;
const paint = () => onStatus(composeStatus(shell, model));
return {
@@ -121,6 +127,7 @@ export function createShell({ onStatus, timeoutMs, setTimer = setTimeout, clearT
/** A peer that never answers the handshake. Deliberately about the *first
* frame*, not either peer's own failure modes — clients.md. */
arm(message) {
arms += 1;
clearTimer(watchdog);
watchdog = setTimer(() => {
watchdog = null;
@@ -140,5 +147,8 @@ export function createShell({ onStatus, timeoutMs, setTimer = setTimeout, clearT
this.setShell(text);
},
armed: () => watchdog !== null,
/** How many times the watchdog has been armed. A test denying its message
* needs this to be nonzero, or it is denying an unreachable event. */
timesArmed: () => arms,
};
}
+8 -1
View File
@@ -8,6 +8,13 @@ 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
@@ -32,7 +39,7 @@ function renderModelFields() {
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 = read("../app.js") + read("../shell.js");
const page = code("../app.js") + code("../shell.js");
const unpainted = renderModelFields().filter((field) => !page.includes(`model.${field}`));
assert.deepEqual(
unpainted.sort(),