Cross-check el() ids against index.html in the selftest

A knob wired in app.js but missing from the markup throws during wiring, aborts
the module and leaves a blank page - a symptom pointing nowhere near its cause.
It has now happened twice, so it gets a check rather than vigilance: the
selftest fetches both files and compares the id sets in each direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-24 15:25:39 -04:00
parent 9a11eeabc1
commit a146d271b6
3 changed files with 38 additions and 2 deletions

View file

@ -148,7 +148,11 @@ chromium --headless --virtual-time-budget=8000 --dump-dom \
http://127.0.0.1:8777/selftest.html | grep -oE '(PASS|FAIL) [0-9/]+'
```
Or open `selftest.html`. 29 assertions over the stages below detection.
Or open `selftest.html`. 41 assertions over the stages below detection, plus a
wiring cross-check: every `el('id')` in `app.js` must exist in `index.html`. A
knob wired in one but not the other throws during wiring, which aborts the rest
of the module and leaves a blank page — a symptom that points nowhere near its
cause, and which has happened twice.
The ring-simplicity check is the load-bearing one. Because `hold` parts *cut*
between poses instead of interpolating, a ring whose vertex order is wrong

View file

@ -45,6 +45,37 @@ const spreadX = (frames, slot) => {
/* ---- the tests ---- */
// Cross-check every el('id') in app.js against the ids in index.html.
//
// This bug class has bitten twice: a knob wired in app.js but absent from the
// markup throws during wiring, which aborts the rest of the module and leaves a
// blank page. The symptom ("nothing happens") points nowhere near the cause, so
// it is worth an automated check rather than vigilance.
export async function runWiring() {
const out = [];
try {
const [app, html] = await Promise.all([
fetch('./js/app.js').then((r) => r.text()),
fetch('./index.html').then((r) => r.text()),
]);
const ids = new Set([...app.matchAll(/\bel\(\s*['"]([\w-]+)['"]\s*\)/g)].map((m) => m[1]));
const list = app.match(/for \(const id of \[([\s\S]*?)\]\)/);
if (list) {
for (const m of list[1].matchAll(/'([\w]+)'/g)) { ids.add(m[1]); ids.add(m[1] + 'v'); }
}
const have = new Set([...html.matchAll(/id="([\w-]+)"/g)].map((m) => m[1]));
const missing = [...ids].filter((i) => !have.has(i));
out.push({ name: `every el() id exists in index.html (${ids.size} checked)`,
pass: missing.length === 0, detail: missing.join(', ') });
const unused = [...have].filter((i) => !ids.has(i));
out.push({ name: 'no orphaned ids in index.html', pass: unused.length === 0,
detail: unused.join(', ') });
} catch (e) {
out.push({ name: 'wiring check ran', pass: false, detail: e.message });
}
return out;
}
export function run() {
results.length = 0;

View file

@ -7,10 +7,11 @@
</style></head><body>
<h1 id="head">running…</h1><ul id="out"></ul>
<script type="module">
import { run } from './js/selftest.js';
import { run, runWiring } from './js/selftest.js';
let res;
try { res = run(); }
catch (e) { res = [{ name: 'harness threw: ' + e.message, pass: false, detail: String(e.stack).split('\n')[1] || '' }]; }
res = res.concat(await runWiring());
const pass = res.filter(r => r.pass).length;
const fail = res.length - pass;
document.getElementById('head').textContent = `${fail === 0 ? 'PASS' : 'FAIL'} — ${pass}/${res.length} assertions`;