Check the mouth interior by its own tone, and stop teardown failing the run

Two bugs in the browser suite, both dating from step 7 adding eyes, brows and
teeth to the stage.

The shut-mouth check asked for exactly one tone on the whole canvas. That was
true when the stage held nothing but a mouth; since step 7 every frame carries
five or six tones whatever the mouth is doing, so the check failed on a correct
picture. Ask instead whether :mouth-dark is on the stage at all: nothing else
carries that tone, so it answers "is the interior drawn" without needing to know
where the mouth is. freeze_test already got this same rescoping; its browser twin
did not.

The failure went unnoticed because close() raced Chrome's own profile writes and
threw ENOTEMPTY out of main's `finally`, past the summary line and the
process.exit that reports the failure count. The suite therefore exited 2 and
printed no verdict whether it passed or failed. Teardown is now allowed to fail
out loud without taking the exit code with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B87NVmiU36qQmN9gmFYnJ9
This commit is contained in:
Olive Vaughn 2026-09-27 22:44:16 -04:00
parent 663b7c367a
commit ccca93e233

View file

@ -37,6 +37,14 @@ const CHROME = process.env.CHROME ??
// nothing drawn, and every check below is a count of pixels that are not it.
const BG = [0x12, 0x14, 0x1c];
// :mouth-dark, from domain/palette, packed the way PROBE packs a tone. It is the
// mouth INTERIOR's colour and nothing else on the stage carries it, so its
// presence is the whole of "the mouth is open" without needing to know where the
// mouth is. A whole-canvas tone COUNT cannot answer that any more: step 7 put
// eyes, brows, an iris, a pupil and teeth on the same stage, so every frame has
// several tones whatever the mouth is doing.
const MOUTH_DARK = (0x24 << 16) | (0x16 << 8) | 0x1a;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let failures = 0;
@ -122,7 +130,22 @@ async function connect() {
const r = await this.send('Page.captureScreenshot', { format: 'png' });
writeFileSync(join(OUT, `${name}.png`), Buffer.from(r.data, 'base64'));
},
close() { ws.close(); chrome.kill(); rmSync(profile, { recursive: true, force: true }); },
// Teardown must not be able to fail the run. `chrome.kill()` returns as soon
// as the signal is sent, so Chrome is often still writing its profile when
// rmSync walks it, and macOS then throws ENOTEMPTY on a directory that
// reappeared mid-delete. That throw used to propagate out of main's `finally`
// and past the summary line, so the suite exited 2 and printed no verdict
// whether it had passed or failed — which is how a stale assertion sat here
// unnoticed. A leaked temp profile is worth less than a truthful exit code.
close() {
ws.close();
chrome.kill();
try {
rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
} catch (e) {
console.log(` note could not remove the temp profile — ${e.code ?? e.message}`);
}
},
};
}
@ -154,7 +177,7 @@ const PROBE = `(() => {
let h = 2166136261;
for (let i = 0; i < d.length; i += 4) { h ^= d[i] + d[i+1] * 31 + d[i+2] * 131; h = Math.imul(h, 16777619); }
return {
w: c.width, h: c.height, drawn, tones: [...tones].length,
w: c.width, h: c.height, drawn, tones: [...tones].length, toneSet: [...tones],
cx: drawn ? cx / drawn : null, cy: drawn ? cy / drawn : null,
hash: h >>> 0,
frame: document.querySelector('.readout span')?.textContent ?? '',
@ -212,21 +235,24 @@ async function main() {
// --- it is a mouth: two tones, one inside the other ---
//
// The three-layer structure is what makes a flat shape read as an opening
// rather than a blob, so the interior being a SECOND tone is the check that
// this is a mouth and not one polygon.
// rather than a blob, so the interior being its OWN tone is the check that
// this is a mouth and not one polygon. Asked of :mouth-dark specifically
// rather than of the tone count: the count is now dominated by the eyes and
// brows, which are on the stage whatever the mouth is doing.
await page.eval(SEEK(10)); // beat 1: wide open
await sleep(120);
const open = await page.eval(PROBE);
await page.shot('take-open');
check(open.tones >= 2, 'an open mouth draws an outline and an interior',
`${open.tones} tones`);
check(open.toneSet.includes(MOUTH_DARK),
'an open mouth draws an outline and an interior',
`${open.tones} tones, interior ${open.toneSet.includes(MOUTH_DARK) ? 'present' : 'MISSING'}`);
await page.eval(SEEK(28)); // beat 3: shut
await sleep(120);
const shut = await page.eval(PROBE);
await page.shot('take-shut');
check(shut.tones === 1, 'a shut mouth draws the outline alone',
`${shut.tones} tones`);
check(!shut.toneSet.includes(MOUTH_DARK), 'a shut mouth draws the outline alone',
`${shut.tones} tones, interior ${shut.toneSet.includes(MOUTH_DARK) ? 'STILL DRAWN' : 'gone'}`);
check(open.hash !== shut.hash, 'the open and the shut mouth are different pictures');
check(open.drawn > shut.drawn, 'the open mouth covers more of the stage',
`${open.drawn} vs ${shut.drawn} px`);