2026-07-19 15:59:21 +02:00
|
|
|
import * as fs from 'fs';
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
import type { Page } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Install a REAL `globalThis.ngspiceService` provider into a harness page —
|
|
|
|
|
* the same worker-backed ngspice_service boot the standalone app does
|
|
|
|
|
* (web/standalone/src/wasm/ngspice-service.ts), minus the CDN manifest
|
|
|
|
|
* resolution: the harness serves ngspice_service.{js,wasm} same-origin next
|
|
|
|
|
* to the tool page (tests/scripts/setup-kicad-wasm.sh copies them from
|
|
|
|
|
* output/).
|
|
|
|
|
*
|
|
|
|
|
* The worker-side wrapper is the SHARED source of truth
|
|
|
|
|
* (web/standalone/src/wasm/ngspice-worker.js — the standalone imports it via
|
|
|
|
|
* vite `?raw`; the harness reads it off disk and injects it verbatim), so the
|
|
|
|
|
* boot logic cannot drift between app and tests.
|
|
|
|
|
*
|
|
|
|
|
* Additions for assertability:
|
|
|
|
|
* - every `{ evt }` frame is appended to window.__ngspiceEvents
|
|
|
|
|
* ({ kind, lines?, finished?, status?, t: ms-since-install }) BEFORE being
|
|
|
|
|
* forwarded to globalThis.__ngspiceOnEvent (the editor client stub's
|
|
|
|
|
* dispatcher, when integrated) — specs assert live streaming by comparing
|
|
|
|
|
* event timestamps against run boundaries;
|
|
|
|
|
* - request/response summaries are appended to window.__ngspiceLog.
|
|
|
|
|
*
|
|
|
|
|
* The worker fetches ngspice_service.js lazily on the FIRST request — specs
|
|
|
|
|
* assert the lazy-load boundary by watching network requests.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const NGSPICE_WORKER_SRC = fs.readFileSync(
|
|
|
|
|
path.resolve(__dirname, '..', '..', '..',
|
|
|
|
|
'web', 'standalone', 'src', 'wasm', 'ngspice-worker.js'),
|
|
|
|
|
'utf8');
|
|
|
|
|
|
|
|
|
|
export async function installNgspiceServiceStub(page: Page): Promise<void> {
|
|
|
|
|
await page.addInitScript((workerSrc: string) => {
|
|
|
|
|
if ((globalThis as any).ngspiceService) return;
|
|
|
|
|
|
|
|
|
|
const t0 = Date.now();
|
|
|
|
|
(window as any).__ngspiceEvents = [];
|
|
|
|
|
(window as any).__ngspiceLog = [];
|
|
|
|
|
|
|
|
|
|
let workerP: Promise<Worker> | null = null;
|
|
|
|
|
const pending = new Map<number, (res: any) => void>();
|
|
|
|
|
let nextId = 1;
|
|
|
|
|
|
|
|
|
|
const evtQueue: any[] = [];
|
|
|
|
|
const dispatchEvt = (evt: any) => {
|
|
|
|
|
(window as any).__ngspiceEvents.push({ ...evt, t: Date.now() - t0 });
|
|
|
|
|
const handler = (globalThis as any).__ngspiceOnEvent;
|
|
|
|
|
if (handler) {
|
|
|
|
|
while (evtQueue.length) handler(evtQueue.shift());
|
|
|
|
|
handler(evt);
|
|
|
|
|
} else {
|
|
|
|
|
evtQueue.push(evt);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const failAllPending = (why: string) => {
|
|
|
|
|
for (const [, resolve] of pending) resolve({ error: why });
|
|
|
|
|
pending.clear();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ensureWorker = (): Promise<Worker> => {
|
|
|
|
|
if (!workerP) {
|
|
|
|
|
workerP = (async () => {
|
|
|
|
|
const glue = new URL('ngspice_service.js', window.location.href).href;
|
|
|
|
|
console.log(`[TEST-NGSPICE] booting ngspice_service from ${glue}`);
|
|
|
|
|
const worker = new Worker(URL.createObjectURL(new Blob(
|
|
|
|
|
[`self.NGSPICE_GLUE_URL = ${JSON.stringify(glue)};\n`, workerSrc],
|
|
|
|
|
{ type: 'text/javascript' })));
|
|
|
|
|
worker.onmessage = (e) => {
|
|
|
|
|
const data = e.data ?? {};
|
|
|
|
|
if (data.evt) { dispatchEvt(data.evt); return; }
|
|
|
|
|
if (typeof data.id !== 'number') return;
|
|
|
|
|
const resolve = pending.get(data.id);
|
|
|
|
|
if (resolve) { pending.delete(data.id); resolve(data.res); }
|
|
|
|
|
};
|
|
|
|
|
worker.onerror = (e) => {
|
|
|
|
|
console.log(`[TEST-NGSPICE] worker error: ${e.message} — resetting service`);
|
|
|
|
|
failAllPending(`ngspice_service crashed: ${e.message}`);
|
|
|
|
|
workerP = null;
|
|
|
|
|
try { worker.terminate(); } catch { /* already gone */ }
|
|
|
|
|
};
|
jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).
Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
upstream; pthread children re-run the wrapper blob, so an em-pthread
realm now importScripts the glue and gets out of the way (before:
recursive service boots, pool never fills, silent 180s boot hangs —
every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
targets ported to -sJSPI (the JSPI-only libcontext crashed at first
yield under them); mainloop/gl repro pages drive their tick through a
promising export (emscripten_set_main_loop callbacks cannot suspend);
retired inject-dyncall-shims lines removed (targets were unbuildable
since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
(plain throws on strict-JSPI Firefox; emscripten::async() re-executes
its invoker on settle) — kept sync for manual Chromium probing, spec
coverage moved to the jspi-coroutine harness (18 cases).
Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
gate over boot / board load / chooser open / cancel (deterministically
red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
beacon regexes, footprint-chooser-close liveness -> wx parking-timer
heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
contract), quarantine never yanks SP from a live window.
Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
|
|
|
// Legible boot: bound the handshake and surface worker
|
|
|
|
|
// death — the bare version hung to the spec timeout with
|
|
|
|
|
// zero evidence (occ-service.ts has the same guard).
|
2026-07-19 15:59:21 +02:00
|
|
|
await new Promise<void>((resolve, reject) => {
|
jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).
Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
upstream; pthread children re-run the wrapper blob, so an em-pthread
realm now importScripts the glue and gets out of the way (before:
recursive service boots, pool never fills, silent 180s boot hangs —
every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
targets ported to -sJSPI (the JSPI-only libcontext crashed at first
yield under them); mainloop/gl repro pages drive their tick through a
promising export (emscripten_set_main_loop callbacks cannot suspend);
retired inject-dyncall-shims lines removed (targets were unbuildable
since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
(plain throws on strict-JSPI Firefox; emscripten::async() re-executes
its invoker on settle) — kept sync for manual Chromium probing, spec
coverage moved to the jspi-coroutine harness (18 cases).
Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
gate over boot / board load / chooser open / cancel (deterministically
red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
beacon regexes, footprint-chooser-close liveness -> wx parking-timer
heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
contract), quarantine never yanks SP from a live window.
Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
|
|
|
const fail = (msg: string) => {
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
reject(new Error(msg));
|
|
|
|
|
};
|
|
|
|
|
const timer = setTimeout(
|
|
|
|
|
() => fail('[TEST-NGSPICE] ngspice_service boot timed out after '
|
|
|
|
|
+ '60s (no ready/bootError from the worker)'), 60000);
|
2026-07-19 15:59:21 +02:00
|
|
|
const onFirst = (e: MessageEvent) => {
|
jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).
Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
upstream; pthread children re-run the wrapper blob, so an em-pthread
realm now importScripts the glue and gets out of the way (before:
recursive service boots, pool never fills, silent 180s boot hangs —
every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
targets ported to -sJSPI (the JSPI-only libcontext crashed at first
yield under them); mainloop/gl repro pages drive their tick through a
promising export (emscripten_set_main_loop callbacks cannot suspend);
retired inject-dyncall-shims lines removed (targets were unbuildable
since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
(plain throws on strict-JSPI Firefox; emscripten::async() re-executes
its invoker on settle) — kept sync for manual Chromium probing, spec
coverage moved to the jspi-coroutine harness (18 cases).
Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
gate over boot / board load / chooser open / cancel (deterministically
red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
beacon regexes, footprint-chooser-close liveness -> wx parking-timer
heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
contract), quarantine never yanks SP from a live window.
Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
|
|
|
if (e.data?.ready) {
|
|
|
|
|
worker.removeEventListener('message', onFirst);
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
resolve();
|
|
|
|
|
} else if (e.data?.bootError) {
|
|
|
|
|
fail(`[TEST-NGSPICE] ngspice_service bootError: ${e.data.bootError}`);
|
|
|
|
|
}
|
2026-07-19 15:59:21 +02:00
|
|
|
};
|
|
|
|
|
worker.addEventListener('message', onFirst);
|
jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).
Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
upstream; pthread children re-run the wrapper blob, so an em-pthread
realm now importScripts the glue and gets out of the way (before:
recursive service boots, pool never fills, silent 180s boot hangs —
every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
targets ported to -sJSPI (the JSPI-only libcontext crashed at first
yield under them); mainloop/gl repro pages drive their tick through a
promising export (emscripten_set_main_loop callbacks cannot suspend);
retired inject-dyncall-shims lines removed (targets were unbuildable
since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
(plain throws on strict-JSPI Firefox; emscripten::async() re-executes
its invoker on settle) — kept sync for manual Chromium probing, spec
coverage moved to the jspi-coroutine harness (18 cases).
Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
gate over boot / board load / chooser open / cancel (deterministically
red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
beacon regexes, footprint-chooser-close liveness -> wx parking-timer
heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
contract), quarantine never yanks SP from a live window.
Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
|
|
|
worker.addEventListener('error', (e: any) => fail(
|
|
|
|
|
`[TEST-NGSPICE] ngspice_service worker error: ${e?.message ?? e} `
|
|
|
|
|
+ `(${e?.filename ?? '?'}:${e?.lineno ?? '?'})`));
|
|
|
|
|
worker.addEventListener('messageerror', () => fail(
|
|
|
|
|
'[TEST-NGSPICE] ngspice_service worker messageerror (structured clone failed)'));
|
2026-07-19 15:59:21 +02:00
|
|
|
});
|
|
|
|
|
console.log('[TEST-NGSPICE] ngspice_service ready');
|
|
|
|
|
return worker;
|
|
|
|
|
})().catch((e) => { workerP = null; throw e; });
|
|
|
|
|
}
|
|
|
|
|
return workerP;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const request = async (req: any) => {
|
|
|
|
|
let worker: Worker;
|
|
|
|
|
try {
|
|
|
|
|
worker = await ensureWorker();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return { error: `ngspice_service unavailable: ${e}` };
|
|
|
|
|
}
|
|
|
|
|
const id = nextId++;
|
|
|
|
|
const res: any = await new Promise((resolve) => {
|
|
|
|
|
pending.set(id, resolve);
|
|
|
|
|
worker.postMessage({ id, req });
|
|
|
|
|
});
|
|
|
|
|
(window as any).__ngspiceLog.push({
|
|
|
|
|
kind: req.kind,
|
|
|
|
|
cmd: req.cmd,
|
|
|
|
|
name: req.name,
|
|
|
|
|
ret: res?.ret,
|
|
|
|
|
error: res?.error,
|
|
|
|
|
length: res?.length,
|
|
|
|
|
t: Date.now() - t0,
|
|
|
|
|
});
|
|
|
|
|
return res;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(globalThis as any).ngspiceService = { request };
|
|
|
|
|
}, NGSPICE_WORKER_SRC);
|
|
|
|
|
}
|