Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_wasmpanic.mjs

5.2 KiB, 1 run

created by r2519314175:801, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_wasmpanic.mjs — a Rust panic must say where it happened.
2//
3// WHY. There was no panic hook in this app at all, and a wasm panic without one
4// is the most opaque failure it can produce:
5//
6// - the browser reports a bare `"Script error."` with no file, line or message
7// - the `Promise` the call was made through NEVER SETTLES, so the caller's
8// `.then` and `.catch` both stay silent for ever
9// - the module is poisoned, so everything after it fails differently
10//
11// An iPhone looped on exactly that for four sessions. The durable trail showed
12// `unlocked`, then `Script error.`, then a step that had started and reported
13// neither success nor failure, and then the tab dying. Four diagnoses were made
14// from reading code and all four were wrong, because the one component that
15// knew what had happened had nowhere to say it.
16//
17// So: a hook, and this file, which requires the hook to be REAL rather than
18// present. A diagnostic nobody has watched work is a diagnostic whose silence
19// means nothing.
20//
21// WHAT IS LOCKED DOWN.
22//
23// A. A panic reaches the durable trail, with the FILE AND LINE in it.
24// B. It also reaches the console, for anyone who has one.
25// C. It survives a reload, because a panicking tab is a tab about to go away
26// and a message that dies with it is no message at all.
27// D. The hook is installed before the app can run anything — it is the first
28// thing after `init()`, not somewhere later that a panic could beat.
29// E. Installing twice is harmless.
30//
31// node dev/verify_wasmpanic.mjs
32//
33// Needs dev/serve.mjs. No gateway, no model.
34
35import fs from 'node:fs';
36import path from 'node:path';
37import { fileURLToPath } from 'node:url';
38import { open, scratch } from './harness.mjs';
39
40const HERE = path.dirname(fileURLToPath(import.meta.url));
41const ROOT = path.join(HERE, '..');
42
43const out = [];
44let bad = 0;
45const check = (ok, what, detail) => {
46 out.push(`${ok ? 'PASS' : 'FAIL'} ${what}${detail != null ? ' — ' + detail : ''}`);
47 if (!ok) bad++;
48 return ok;
49};
50
51// ── D. Installed FIRST, read from the source ────────────────────────
52//
53// Ordering is the whole value of a panic hook: one installed after the thing
54// that panics reports nothing. Checked in the file rather than at runtime,
55// because a runtime check can only see the order that happened to occur.
56{
57 const src = fs.readFileSync(path.join(ROOT, 'www', 'js', 'daimond.js'), 'utf8');
58 const at = src.indexOf('install_panic_hook()');
59 const init = src.indexOf('await init();');
60 const ready = src.indexOf('__DAIMOND_READY = true');
61 check(at > 0 && init > 0 && at > init && at < ready,
62 'the hook is installed immediately after the wasm module, before anything uses it',
63 `init@${init} hook@${at} ready@${ready}`);
64}
65
66const PROFILE = scratch('pw', 'wasmpanic-' + process.pid);
67fs.rmSync(PROFILE, { recursive: true, force: true });
68const s = await open({ name: 'wasmpanic', connect: false, profile: PROFILE });
69const p = s.page;
70
71// ── A + B. It reports, with a location ──────────────────────────────
72const r = await p.evaluate(async () => {
73 const m = await import('/pkg/oxedyne_daimond.js');
74 // Twice, which also covers E: the second install must not throw and the
75 // second panic must still report.
76 try { m.install_panic_hook(); } catch (e) { /* already */ }
77 try { m.panic_on_purpose(); } catch (e) { /* the wasm trap, expected */ }
78 await new Promise((r) => setTimeout(r, 300));
79 return {
80 rows: (window.DaimondTrail.rows() || []).filter((x) => /panic/i.test(x.w)),
81 text: window.DaimondTrail.text(),
82 };
83});
84
85check(r.rows.length > 0, 'a panic reaches the durable trail', `${r.rows.length} row(s)`);
86const said = (r.rows[0] || {}).d || '';
87check(/entry\.rs:\d+/.test(said),
88 'and NAMES THE FILE AND THE LINE — which is the whole point',
89 JSON.stringify(said.slice(0, 90)));
90check(/panic_on_purpose/.test(said),
91 'and carries the panic message, not just its location');
92
93const consoled = s.logs.some((l) => /panic_on_purpose/.test(l));
94check(consoled, 'and the console gets it too, for anyone who has one');
95
96// ── C. It survives the reload ───────────────────────────────────────
97{
98 await p.reload({ waitUntil: 'domcontentloaded' });
99 await p.waitForTimeout(1200);
100 const after = await p.evaluate(() => {
101 try { return (window.DaimondTrail.rows() || []).filter((x) => /panic/i.test(x.w)).length; }
102 catch (e) { return -1; }
103 });
104 check(after > 0,
105 'and it is still there after a reload — a panicking tab is one about to go away',
106 String(after));
107}
108
109// ── The trail says nothing it should not ────────────────────────────
110check(!/[A-Za-z0-9_-]{32,}/.test(r.text),
111 'the trail carries nothing that looks like a key or a token',
112 JSON.stringify((r.text.match(/[A-Za-z0-9_-]{32,}/) || [])[0] || 'none'));
113
114await s.close();
115try { fs.rmSync(PROFILE, { recursive: true, force: true }); } catch (e) { /* gone */ }
116
117console.log(out.join('\n'));
118const total = out.filter((l) => /^(PASS|FAIL)/.test(l)).length;
119console.log(bad === 0 ? `\nALL ${total} CHECKS PASSED` : `\n${bad} of ${total} FAILED`);
120process.exit(bad === 0 ? 0 : 1);