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
174 lines
8.8 KiB
TypeScript
174 lines
8.8 KiB
TypeScript
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import type { Page } from '@playwright/test';
|
||
|
||
/**
|
||
* Install a REAL `globalThis.occService` provider into a harness page — the
|
||
* same worker-backed occ_service boot the standalone app does, minus the CDN
|
||
* manifest resolution: the harness serves occ_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/occ-worker.js — the standalone imports it via vite
|
||
* `?raw`; the harness reads it off disk and injects it verbatim), so the
|
||
* trap-prone boot logic (blob worker + locateFile absolutization + pthread
|
||
* mainScriptUrlOrBlob) cannot drift between app and tests.
|
||
*
|
||
* Differences from the app provider, for assertability:
|
||
* - export results are captured into window.__occExports (name, size, magic
|
||
* prefix, the exporter's report text, and a PRODUCT-entity count for STEP
|
||
* bodies — the per-component geometry signal) instead of triggering a
|
||
* browser download;
|
||
* - the app's export model prefetch (occ-service.ts → models-bridge
|
||
* collectBoardModelFiles) is mirrored against the page's `kicadLibs`
|
||
* provider: lib refs scanned from the board text are ensured (kind
|
||
* "model3d"), read back from the editor MEMFS, and shipped as the
|
||
* request's `models` array. Specs without a kicadLibs stub ship none —
|
||
* the pre-delivery behavior.
|
||
* - installed as an init script (kicad fixtures do this for every page), so
|
||
* it exists from document start on every navigation — standalone parity,
|
||
* where boot.ts installs the provider whenever the editor bundle boots.
|
||
*
|
||
* The worker fetches occ_service.js lazily on the FIRST request — specs can
|
||
* assert the lazy-load boundary by watching network requests.
|
||
*/
|
||
|
||
const OCC_WORKER_SRC = fs.readFileSync(
|
||
path.resolve(__dirname, '..', '..', '..',
|
||
'web', 'standalone', 'src', 'wasm', 'occ-worker.js'),
|
||
'utf8');
|
||
|
||
export async function installOccServiceStub(page: Page): Promise<void> {
|
||
await page.addInitScript((workerSrc: string) => {
|
||
if ((globalThis as any).occService) return;
|
||
|
||
(window as any).__occExports = [];
|
||
|
||
let workerP: Promise<Worker> | null = null;
|
||
const pending = new Map<number, (res: any) => void>();
|
||
let nextId = 1;
|
||
|
||
const ensureWorker = (): Promise<Worker> => {
|
||
if (!workerP) {
|
||
workerP = (async () => {
|
||
const glue = new URL('occ_service.js', window.location.href).href;
|
||
console.log(`[TEST-OCC] booting occ_service from ${glue}`);
|
||
const worker = new Worker(URL.createObjectURL(new Blob(
|
||
[`self.OCC_GLUE_URL = ${JSON.stringify(glue)};\n`, workerSrc],
|
||
{ type: 'text/javascript' })));
|
||
worker.onmessage = (e) => {
|
||
const { id, res } = e.data ?? {};
|
||
if (typeof id !== 'number') return;
|
||
const resolve = pending.get(id);
|
||
if (resolve) { pending.delete(id); resolve(res); }
|
||
};
|
||
// Legible boot: the old handshake could never reject on a
|
||
// worker DEATH (importScripts throw, pthread spawn wedge,
|
||
// OOM-kill) — the promise just hung until the spec's 180s
|
||
// timeout with zero evidence. Surface worker errors and
|
||
// bound the boot.
|
||
await new Promise<void>((resolve, reject) => {
|
||
const fail = (msg: string) => {
|
||
clearTimeout(timer);
|
||
reject(new Error(msg));
|
||
};
|
||
const timer = setTimeout(
|
||
() => fail('[TEST-OCC] occ_service boot timed out after 60s '
|
||
+ '(no ready/bootError from the worker)'), 60000);
|
||
const onFirst = (e: MessageEvent) => {
|
||
if (e.data?.ready) {
|
||
worker.removeEventListener('message', onFirst);
|
||
clearTimeout(timer);
|
||
resolve();
|
||
} else if (e.data?.bootError) {
|
||
fail(`[TEST-OCC] occ_service bootError: ${e.data.bootError}`);
|
||
}
|
||
};
|
||
worker.addEventListener('message', onFirst);
|
||
worker.addEventListener('error', (e: any) => fail(
|
||
`[TEST-OCC] occ_service worker error: ${e?.message ?? e} `
|
||
+ `(${e?.filename ?? '?'}:${e?.lineno ?? '?'})`));
|
||
worker.addEventListener('messageerror', () => fail(
|
||
'[TEST-OCC] occ_service worker messageerror (structured clone failed)'));
|
||
});
|
||
console.log('[TEST-OCC] occ_service ready');
|
||
return worker;
|
||
})().catch((e) => { workerP = null; throw e; });
|
||
}
|
||
return workerP;
|
||
};
|
||
|
||
// Mirror of the app's collectBoardModelFiles, against the page's
|
||
// kicadLibs provider (the specs' model stub): scan lib refs, ensure
|
||
// each into the editor MEMFS, read the staged bytes back.
|
||
const collectModels = async (boardText: string) => {
|
||
const hook = (globalThis as any).kicadLibs;
|
||
const FS = (window as any).FS;
|
||
if (!hook?.request || !FS) return [];
|
||
const ROOT = '/pcbjam/3dmodels';
|
||
const refs = new Set<string>();
|
||
const re = /\(\s*model\s+"((?:[^"\\]|\\.)*)"/g;
|
||
for (let m = re.exec(boardText); m; m = re.exec(boardText)) {
|
||
const raw = m[1].replace(/\\(.)/g, '$1');
|
||
const lib = raw.match(/^\$[{(](?:[^})]*3DMODEL_DIR|KISYS3DMOD)[})][/\\]+(.+)$/);
|
||
if (lib) refs.add(lib[1]);
|
||
}
|
||
const models: Array<{ path: string; bytes: Uint8Array }> = [];
|
||
const seen = new Set<string>();
|
||
for (const ref of refs) {
|
||
const abs = await hook.request('ensure', '', ref, 'model3d');
|
||
if (typeof abs !== 'string' || !abs.startsWith(`${ROOT}/`) || seen.has(abs)) continue;
|
||
seen.add(abs);
|
||
models.push({ path: abs.slice(ROOT.length + 1), bytes: FS.readFile(abs) });
|
||
}
|
||
console.log(`[TEST-OCC] shipping ${models.length} board model(s) with the export`);
|
||
return models;
|
||
};
|
||
|
||
const request = async (req: any) => {
|
||
if (req.kind === 'export')
|
||
req.models = await collectModels(new TextDecoder().decode(req.board));
|
||
let worker: Worker;
|
||
try {
|
||
worker = await ensureWorker();
|
||
} catch (e) {
|
||
return { ok: false, report: `occ_service unavailable: ${e}` };
|
||
}
|
||
const id = nextId++;
|
||
const transfer = req.kind === 'export'
|
||
? [req.board.buffer, ...(req.models ?? []).map((m: any) => m.bytes.buffer)]
|
||
: [req.bytes.buffer];
|
||
const res: any = await new Promise((resolve) => {
|
||
pending.set(id, resolve);
|
||
worker.postMessage({ id, req }, transfer);
|
||
});
|
||
if (req.kind === 'export') {
|
||
if (res.ok && res.bytes?.length) {
|
||
const magic = new TextDecoder().decode(res.bytes.slice(0, 16));
|
||
// STEP is a text format: `#n=PRODUCT('name',…)` entities count the
|
||
// distinct model bodies in the assembly (a bare board exports 1–2;
|
||
// component models add one each). The anchored `=PRODUCT(` match
|
||
// excludes PRODUCT_DEFINITION/PRODUCT_CONTEXT relatives.
|
||
let productCount = -1;
|
||
if (magic.startsWith('ISO-10303-21')) {
|
||
const text = new TextDecoder().decode(res.bytes);
|
||
productCount = (text.match(/=\s*PRODUCT\s*\(/g) ?? []).length;
|
||
}
|
||
(window as any).__occExports.push({
|
||
name: req.fileName || res.fileName,
|
||
size: res.bytes.length,
|
||
magic,
|
||
report: String(res.report ?? ''),
|
||
productCount,
|
||
});
|
||
console.log(`[TEST-OCC] export captured: ${req.fileName} ${res.bytes.length}B "${magic}" products=${productCount}`);
|
||
}
|
||
return { ok: res.ok, report: res.report, fileName: res.fileName };
|
||
}
|
||
return res;
|
||
};
|
||
|
||
(globalThis as any).occService = { request };
|
||
}, OCC_WORKER_SRC);
|
||
}
|