1b6635a1
test: the page's badge vocabulary and the attach it gates
a73x 2026-08-26 12:13
Commit message
web/verify.js
| Old | New | ||
|---|---|---|---|
| @@ -2552,6 +2552,76 @@ async function verifySelectionShell(shell, html) { | |||
| 2552 | check('failed destructive reset also clears orphaned scroll request', failedResetPaint.tile.scrollRequest, null); | 2552 | check('failed destructive reset also clears orphaned scroll request', failedResetPaint.tile.scrollRequest, null); |
| 2553 | } | 2553 | } |
| 2554 | 2554 | ||
| 2555 | // The terminal-status region: which badge stops a tile re-attaching, and | ||
| 2556 | // the two words the hub and the page agree on. Nothing on the wire can | ||
| 2557 | // see any of it — wsclient attaches 0x0 and renders no badge — so this | ||
| 2558 | // shell is the gate, and a real browser is the gate for pixels and keys. | ||
| 2559 | async function verifyStatusShell(shell) { | ||
| 2560 | const h = browserShell(shell); | ||
| 2561 | const wall = h.document.createElement('div'); | ||
| 2562 | |||
| 2563 | // ENV_FRAME/ENV_CONTROL and the exit_status opcode, spelled as the | ||
| 2564 | // browser receives them: mux.js reads these bytes off a WebSocket, so a | ||
| 2565 | // fixture that called its constants instead would pin nothing. | ||
| 2566 | const exitStatus = (code) => Uint8Array.from([0x00, 0x82, 1, 0, 0, 0, code]); | ||
| 2567 | const control = (state) => | ||
| 2568 | Uint8Array.from([0x01, ...new TextEncoder().encode(JSON.stringify({ state }))]); | ||
| 2569 | |||
| 2570 | const makeTile = () => { | ||
| 2571 | const tile = new h.Tile(9, 'status fixture', wall, ''); | ||
| 2572 | tile.core = { | ||
| 2573 | memory: { buffer: new ArrayBuffer(64) }, | ||
| 2574 | mux_attach_payload: () => 20, | ||
| 2575 | mux_output_ptr: () => 0, | ||
| 2576 | mux_output_len: () => 20, | ||
| 2577 | }; | ||
| 2578 | const sent = []; | ||
| 2579 | tile.sendFrame = (type) => { sent.push(type); return true; }; | ||
| 2580 | tile.reflow = () => {}; | ||
| 2581 | return { tile, sent }; | ||
| 2582 | }; | ||
| 2583 | |||
| 2584 | // --- the badge vocabulary, which webhub.zig's restore rule reads back --- | ||
| 2585 | |||
| 2586 | const refusal = makeTile(); | ||
| 2587 | refusal.tile.onMessage(exitStatus(0)); | ||
| 2588 | check('a refusal before any grid badges refused', refusal.tile.status, 'refused'); | ||
| 2589 | check('the refusal badge names no reason the daemon never gave', refusal.tile.statusText, 'refused'); | ||
| 2590 | |||
| 2591 | const ending = makeTile(); | ||
| 2592 | ending.tile.gotState = true; | ||
| 2593 | ending.tile.onMessage(exitStatus(3)); | ||
| 2594 | check('an exit after a grid badges exited', ending.tile.status, 'exited'); | ||
| 2595 | check('the exit badge carries the code', ending.tile.statusText, 'exited 3'); | ||
| 2596 | |||
| 2597 | // --- what each badge does to the next attach --- | ||
| 2598 | |||
| 2599 | const exited = makeTile(); | ||
| 2600 | exited.tile.gotState = true; | ||
| 2601 | exited.tile.onMessage(exitStatus(0)); | ||
| 2602 | exited.tile.zoomed = true; // a sized attach CREATES: the dangerous one | ||
| 2603 | exited.tile.sendAttach(false); | ||
| 2604 | check('a session the user ended is not re-attached', exited.sent.length, 0); | ||
| 2605 | |||
| 2606 | // The restore heal IS a refusal followed by a re-attach: the hub births | ||
| 2607 | // the session and re-dials, and this attach is what lands on it. Gate | ||
| 2608 | // the whole TERMINAL set and the browser wall stays dead after a daemon | ||
| 2609 | // restart. | ||
| 2610 | const refused = makeTile(); | ||
| 2611 | refused.tile.onMessage(exitStatus(0)); | ||
| 2612 | refused.tile.sendAttach(false); | ||
| 2613 | check('a refused tile keeps re-attaching', refused.sent.length, 1); | ||
| 2614 | |||
| 2615 | // --- the precedence the gate rests on --- | ||
| 2616 | |||
| 2617 | const narrated = makeTile(); | ||
| 2618 | narrated.tile.gotState = true; | ||
| 2619 | narrated.tile.onMessage(exitStatus(0)); | ||
| 2620 | narrated.tile.onMessage(control('up')); | ||
| 2621 | check('link news cannot clear exited', narrated.tile.status, 'exited'); | ||
| 2622 | check('and the up it rode in on sends no attach', narrated.sent.length, 0); | ||
| 2623 | } | ||
| 2624 | |||
| 2555 | // --- wire builders (layouts golden-pinned in protocol.zig) --- | 2625 | // --- wire builders (layouts golden-pinned in protocol.zig) --- |
| 2556 | function snapshotPayload({ seq, history, cols, rows, epoch }, state) { | 2626 | function snapshotPayload({ seq, history, cols, rows, epoch }, state) { |
| 2557 | const stateBytes = Buffer.from(state, 'utf8'); | 2627 | const stateBytes = Buffer.from(state, 'utf8'); |
| @@ -3249,6 +3319,7 @@ async function main() { | |||
| 3249 | await verifyClipboardShell(shell, html); | 3319 | await verifyClipboardShell(shell, html); |
| 3250 | await verifySelectionShell(shell, html); | 3320 | await verifySelectionShell(shell, html); |
| 3251 | await verifySettingsShell(shell, html); | 3321 | await verifySettingsShell(shell, html); |
| 3322 | await verifyStatusShell(shell); | ||
| 3252 | 3323 | ||
| 3253 | // Mutation fixture for the source checks below. A correct-looking route | 3324 | // Mutation fixture for the source checks below. A correct-looking route |
| 3254 | // in either kind of comment must be invisible, while live literal | 3325 | // in either kind of comment must be invisible, while live literal |