// tests/background/sw-bundle-import.test.ts // // Tier-1 SW-bundle-loadability gate (Debug session 01-08-sw-incompatibility). // // This test closes the orchestrator-side gap that produced the 01-08 init // crash: prior to this gate, the only SW-compat assertion was // `tests/background/webm-remux-deps.test.ts`, which loaded the SOURCE // packages (`ts-ebml`, `webm-muxer`) under default Node globals. That test // passed because the raw packages import fine in Node — but it never // loaded the BUNDLED output emitted by `vite build`, and Vite's CJS-interop // pipeline tree-shook the transitive `ebml` package out of the bundle while // leaving a dangling destructure reference (`{tools:f}=Pc` against an empty // placeholder `Pc`). The result: SW dies at top-level module init with // `TypeError: Cannot read properties of undefined (reading 'readVint')` // before any handler can register, and the operator's chrome://extensions // "service worker" link goes inaccessible. // // The gate ships in TWO layers: // // Layer 1 ("built SW chunk imports without throwing...") — verifies the // BUNDLED artifact's module-init reaches completion under SW-simulated // globals (Buffer/process/window/document all stripped). This is the // gate that catches Vite/Rollup CJS-interop bugs like the `Pc={}`- // placeholder tree-shake from 01-08. Buffer is stripped here because // `vite-plugin-node-polyfills` rewrites bundled `Buffer` references into // imports of the polyfill chunk's exported `Buffer` (no dependency on // `globalThis.Buffer`); if a future bundler change re-introduces a // `globalThis.Buffer` dependency, this strip catches it. // // Layer 2 ("source remuxSegments invocation does not throw an SW- // incompatible error...") — verifies that the SOURCE `remuxSegments` // code path is REACHABLE under SW-simulated globals. Layer 1 only // proves module-init; if ts-ebml or webm-muxer reaches a code path // only when `remuxSegments` is INVOKED (which only happens at // archive-save time, deep inside `chrome.runtime.onMessage` for // `SAVE_ARCHIVE`), Layer 1 will pass while the real SW could still // crash mid-archive. Layer 2 catches that. // // IMPORTANT — Layer 2 polyfill semantics. Layer 2 imports SOURCE // webm-remux.ts directly via Node's TS transform; the polyfill is a // BUNDLER-LEVEL rewrite (Buffer → imported polyfill chunk) that does // NOT apply when source is loaded outside Vite. To faithfully model // what the real SW sees at runtime (the bundle with polyfilled Buffer), // Layer 2 leaves `Buffer` AVAILABLE in the child env — exactly mirroring // what the polyfilled bundle provides. Stripping Buffer in Layer 2 // would simulate a more-restrictive runtime than what the polyfilled // bundle actually faces, producing false-positive failures. `process`, // `window`, `document` remain stripped because the polyfill is // configured with `globals.process: false` and the SW genuinely has no // DOM. // // Implementation note: the strip+import happens in a SPAWNED Node child // process, not in-process. Vitest's own RPC layer references both `Buffer` // (`node_modules/vitest/dist/chunks/rpc.*.js`) and `process.nextTick`, so // stripping those on the test runner's globalThis crashes vitest itself. // A child process gives us a fresh V8 isolate where we can strip cleanly. // // chrome.* mock: the SW bundle's top-level module init calls // `chrome.runtime.onMessage.addListener(...)`, `chrome.action.onClicked // .addListener(...)`, etc. In the real Service Worker runtime, Chrome // provides the `chrome.*` global. In our Node-simulated isolate, we must // provide a no-op stub or those calls will throw `ReferenceError: chrome // is not defined` — which is a TEST-ENVIRONMENT incompleteness, NOT a // bundle bug. The mock is a recursive Proxy that returns callable no-ops // for any `chrome..(...)` chain. It does NOT exercise // any chrome.* behavior — it only proves that the bundle's top-level // module evaluation completes without throwing. Verifying actual // chrome.* behavior is the responsibility of the offscreen-handshake // and end-to-end smoke tests, not this gate. // // Layer 2 source-import: Node 24 ships native TypeScript transform via // `--experimental-transform-types` (no `tsx` / `ts-node` install needed). // `webm-remux.ts` imports `'../shared/logger'` without a `.ts` extension — // vite/rollup add the extension at bundle time, but the bare Node ESM // resolver does NOT. To bridge that, the child registers a tiny resolution // hook that retries failed `./...`/`../...` lookups with a `.ts` suffix. // The hook is inlined as a `data:` URL so the test stays self-contained. // // Layer 2 invocation: we pass `remuxSegments` a single synthetic Blob // carrying a 4-byte EBML magic + minimal "DocType=webm" header — enough // to push the call past entry-point validation and into ts-ebml's // decoder. Domain errors (invalid-EBML, missing-Tracks, etc.) are // ACCEPTABLE outcomes; they prove the runtime path is structurally // reachable under SW-simulated globals. Only `process` / CSP-eval errors // (and a `Buffer` ReferenceError, which would mean the polyfill itself // regressed) indicate genuine SW-incompat that would crash the SW mid- // archive in Chrome. The classifier in the child encodes that policy. // // Pre-flight contract: callers must `npm run build` first. The test fails // fast with a clear "run npm run build" message if `dist/` is missing. // // Per `feedback-pre-checkpoint-bundle-gates.md` (auto-loaded memory): any // future plan executor whose work surfaces a SW must run this test before // any operator-empirical checkpoint. // // Reference for SW-restricted globals: // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope // Reference for Node ESM dynamic-import accepting file:// URLs: // https://nodejs.org/api/esm.html#import-expressions // Reference for `child_process.execFile`: // https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback // Reference for Proxy traps: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy // Reference for Node 24 native TS transform: // https://nodejs.org/api/typescript.html#type-stripping // Reference for Node ESM resolution hooks (`register`): // https://nodejs.org/api/module.html#moduleregisterspecifier-parenturl-options // Reference for vite-plugin-node-polyfills (Buffer rewrite policy): // https://github.com/davidmyersdev/vite-plugin-node-polyfills import { execFile } from 'node:child_process'; import { existsSync, readFileSync } from 'node:fs'; import { resolve as resolvePath } from 'node:path'; import { pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; import { describe, expect, it } from 'vitest'; const execFileAsync = promisify(execFile); // Layer 1 strip list — applied to the spawned child BEFORE it imports the // BUILT SW chunk. Service Workers expose ServiceWorkerGlobalScope which // has NO `window`, NO `document`, NO `Buffer`, and NO `process`. They DO // have standard browser-ish globals (fetch, Blob, Crypto, etc.) which // Node provides natively in modern versions, so we don't strip those. // // `Buffer` is stripped here EVEN THOUGH `vite-plugin-node-polyfills` // resolves Buffer at bundle-time: the bundler rewrites every `Buffer` // reference into an import of the polyfill chunk's exported `Buffer`, // so the bundle itself never reads `globalThis.Buffer`. Stripping Buffer // from the child's globalThis is therefore an additional contract: "the // bundle must not depend on globalThis.Buffer." A regression where the // polyfill plugin gets removed or misconfigured would re-introduce a // raw `Buffer.alloc(...)` somewhere in the bundle, and Layer 1 would // catch it. const SW_BUNDLE_STRIP_GLOBALS: ReadonlyArray = [ 'window', 'document', 'Buffer', 'process', ]; // Layer 2 strip list — applied to the spawned child BEFORE it dynamically // imports the SOURCE `src/background/webm-remux.ts`. The polyfill is a // BUNDLER-LEVEL rewrite that DOES NOT apply when ts-ebml's source is // loaded outside Vite (via Node's TS transform). So in Layer 2, `Buffer` // is left available in the child env — exactly mirroring what the // polyfilled BUNDLE provides at SW runtime. Stripping it here would // simulate a more-restrictive environment than the polyfilled bundle // actually faces in Chrome, producing false-positive failures. // // `process`, `window`, `document` remain stripped: the polyfill is // configured with `globals.process: false` (not provided), and the SW // genuinely has no DOM regardless of polyfills. const SW_SOURCE_STRIP_GLOBALS: ReadonlyArray = [ 'window', 'document', 'process', ]; // Sentinel strings emitted by the child process; checked by the parent // to distinguish success from failure without resorting to exit codes // (the child may exit 0 on a caught throw). const CHILD_OK_SENTINEL = '__SW_BUNDLE_IMPORT_OK__'; const CHILD_FAIL_SENTINEL = '__SW_BUNDLE_IMPORT_FAILED__'; // Layer 2 sentinels. The runtime invocation distinguishes three outcomes: // success (returned a Blob), domain-error (threw but the error is the // expected shape of a parse failure on our synthetic input — acceptable), // and SW-incompat (threw a process/CSP error — RED; or a Buffer // ReferenceError, which would mean the polyfill itself regressed). const CHILD_REMUX_OK_SENTINEL = '__SW_REMUX_OK__'; const CHILD_REMUX_DOMAIN_ERROR_SENTINEL = '__SW_REMUX_DOMAIN_ERROR__'; const CHILD_REMUX_SW_INCOMPAT_SENTINEL = '__SW_REMUX_SW_INCOMPAT__'; // Cap how long the child has to import. Real SW init takes < 1 s; if the // import takes longer than this, treat it as a hang (functionally the // same as a top-level throw from the operator's perspective). const CHILD_TIMEOUT_MS = 10_000; // Source path for Layer 2. Absolute to avoid `process.cwd()` quirks inside // the spawned child (whose CWD may diverge from the test runner's). const WEBM_REMUX_SOURCE_PATH = resolvePath( process.cwd(), 'src/background/webm-remux.ts', ); interface ChildImportResult { readonly ok: boolean; readonly errorMessage: string; readonly errorStackFirstLine: string; } /** Layer 2 outcome categories — see file header for semantics. */ type RemuxOutcome = 'ok' | 'domain_error' | 'sw_incompat'; interface ChildRemuxResult { readonly outcome: RemuxOutcome; readonly errorName: string; readonly errorMessage: string; readonly errorStack: string; } /** * Resolve the file:// URL of the built SW chunk by parsing the * `dist/service-worker-loader.js` shim that crxjs emits. The shim is a * one-liner of the form `import './assets/index.ts-.js';` whose hash * changes per build; parsing it avoids hard-coding a content hash that * would break on every rebuild. * * @returns Absolute file:// URL of the SW chunk, ready for dynamic import. * @throws If `dist/` is missing or the loader shim cannot be parsed. */ function resolveBuiltSwChunkUrl(): string { const distDir = resolvePath(process.cwd(), 'dist'); const loaderPath = resolvePath(distDir, 'service-worker-loader.js'); if (!existsSync(loaderPath)) { throw new Error( `dist/service-worker-loader.js not found at ${loaderPath}. ` + `Run \`npm run build\` before running this test.`, ); } const loaderSource = readFileSync(loaderPath, 'utf8'); // crxjs emits exactly: `import './assets/index.ts-.js';\n` const importMatch = loaderSource.match(/^\s*import\s+['"](.+?)['"]\s*;?\s*$/m); if (importMatch === null) { throw new Error( `Could not parse SW import path from service-worker-loader.js. ` + `Loader content was: ${JSON.stringify(loaderSource)}`, ); } const chunkRelativePath = importMatch[1]; const chunkAbsolutePath = resolvePath(distDir, chunkRelativePath); return pathToFileURL(chunkAbsolutePath).href; } /** * Build the source for the recursive `chrome.*` Proxy mock the child * installs on its own `globalThis` before importing the SW bundle. * * The mock proves a minimal contract: any `chrome..(...)` chain * evaluated at module-init time returns a callable no-op (or no-op object * with `addListener`/`removeListener` shims) rather than throwing * `ReferenceError: chrome is not defined` or `TypeError: ... is not a * function`. It does NOT exercise any chrome.* behavior — this is purely * a gate that the bundle's top-level evaluation reaches completion. * * Kept as a string-emitting helper so the mock is in scope inside the * spawned child's ESM body, where it must execute BEFORE the dynamic * import of the SW chunk. * * @returns A line of ESM source that installs `globalThis.chrome` as a * recursive Proxy returning no-op callables. */ function buildChromeMockSource(): string { // The Proxy's `get` trap returns another Proxy whose target is a // no-op function, so the returned value is both callable (`chrome.api()`) // and indexable (`chrome.api.subprop`). The inner `get` shims well-known // event-listener methods to silent no-ops; everything else recurses. // // Symbol.toPrimitive is short-circuited to `undefined` so the JS engine // doesn't try to coerce the Proxy itself when, e.g., string-concatenating // a chrome.* reference — it would throw "Cannot convert object to // primitive value" without this guard. return [ `const chromeNoop = () => undefined;`, `const makeProxy = () => new Proxy(chromeNoop, {`, ` get(_, p) {`, ` if (p === Symbol.toPrimitive) return undefined;`, ` if (p === 'then') return undefined;`, ` if (p === 'addListener' || p === 'removeListener' || p === 'hasListener') return () => {};`, ` return makeProxy();`, ` },`, ` apply() { return undefined; },`, `});`, `globalThis.chrome = makeProxy();`, ].join(' '); } /** * Build the inline ESM source that the spawned child will execute. The * child strips the listed globals on its own isolate, installs a no-op * `chrome.*` mock, then dynamically imports the SW chunk, reporting back * via stdout sentinels. * * Kept as a function (not a template literal at module top) so the * sentinel/global lists stay synced with the constants above. * * @param chunkUrl - file:// URL the child will import. * @returns ESM source the child runs under `node --input-type=module`. */ function buildChildSource(chunkUrl: string): string { const stripLines = SW_BUNDLE_STRIP_GLOBALS.map( (key) => `delete globalThis['${key}'];`, ).join(' '); const chromeMockLine = buildChromeMockSource(); return [ stripLines, chromeMockLine, `try {`, ` await import(${JSON.stringify(chunkUrl)});`, ` console.log(${JSON.stringify(CHILD_OK_SENTINEL)});`, `} catch (e) {`, ` const msg = e && e.message ? String(e.message) : String(e);`, ` const stack = e && e.stack ? String(e.stack).split('\\n')[0] : '(no stack)';`, ` console.log(${JSON.stringify(CHILD_FAIL_SENTINEL)});`, ` console.log('MSG:' + msg);`, ` console.log('STK:' + stack);`, `}`, ].join(' '); } /** * Build the inline ESM source for Layer 2: dynamic-import the SOURCE * `webm-remux.ts` under stripped globals and invoke `remuxSegments` * against a synthetic single-segment input. The child registers a * resolution hook so that `import '../shared/logger'` (no `.ts` * extension) resolves via the `.ts` source, mimicking what * vite/rollup does at bundle time. * * The synthetic segment is the minimum EBML doctype header * (`1A 45 DF A3` magic + `42 82 84 'webm'` DocType element). ts-ebml * may accept this and emit zero frames, or throw on missing * Segment/Cluster — both are domain-correct outcomes. Only `process`, * `Buffer` (which would indicate polyfill regression), or eval-related * errors prove SW-incompat. * * @param remuxSourceUrl - file:// URL of `src/background/webm-remux.ts`. * @returns ESM source the child runs under `node --experimental-transform-types`. */ function buildRemuxRuntimeChildSource(remuxSourceUrl: string): string { // Loader hook: when a relative specifier (./ or ../) has NO extension, // try the default resolver first; if it returns ERR_MODULE_NOT_FOUND, // retry with `.ts` appended. This mirrors vite/rollup's extension // resolution policy for the project's TS source tree. const loaderSource = [ `export async function resolve(specifier, context, next) {`, ` const isRelative = specifier.startsWith('./') || specifier.startsWith('../');`, ` const hasKnownExt = specifier.endsWith('.ts') || specifier.endsWith('.js')`, ` || specifier.endsWith('.mjs') || specifier.endsWith('.cjs')`, ` || specifier.endsWith('.json');`, ` if (isRelative && !hasKnownExt) {`, ` try { return await next(specifier, context); }`, ` catch (err) {`, ` if (err && err.code === 'ERR_MODULE_NOT_FOUND') {`, ` return await next(specifier + '.ts', context);`, ` }`, ` throw err;`, ` }`, ` }`, ` return next(specifier, context);`, `}`, ].join(' '); // Encode the loader as a base64 data: URL so the child does not need any // on-disk loader file. Buffer is available HERE (parent context) because // we only call this helper before the strip happens in the child. const loaderDataUrl = 'data:text/javascript;base64,' + Buffer.from(loaderSource, 'utf8').toString('base64'); const chromeMockLine = buildChromeMockSource(); const stripLines = SW_SOURCE_STRIP_GLOBALS.map( (key) => `delete globalThis['${key}'];`, ).join(' '); // Classifier for the runtime outcome. The "SW-incompat" set is precisely // the failure modes that would crash the real SW in Chrome at // archive-save time, GIVEN the current bundler config (polyfilled Buffer): // - ReferenceError: Buffer is not defined (would mean the polyfill // itself has regressed; we still flag this as SW-incompat because // it would crash the real SW if the polyfill were removed) // - ReferenceError: process is not defined (polyfill is configured // `globals.process: false`, so process truly is missing at SW // runtime) // - EvalError / CSP errors (would-be `new Function(...)` in encoder // blocked by MV3 SW CSP) // - SyntaxError from eval (same CSP class) // Everything else (parse failures, missing Tracks, etc.) is a domain // error — the runtime path is structurally reachable, the SW would // surface a clean error to the operator instead of crashing the worker. // // NOTE: Layer 2 leaves `Buffer` AVAILABLE in the child env (see // SW_SOURCE_STRIP_GLOBALS), mirroring the polyfilled bundle. A // `Buffer is not defined` here would only happen if the child Node // environment itself lost Buffer (essentially impossible) or if a // future test refactor accidentally re-stripped it — in both cases // we want to surface that as SW-incompat-class noise rather than // mask it as a domain error. const classifyExprParts = [ `(err) => {`, ` const name = err && err.name ? String(err.name) : '';`, ` const msg = err && err.message ? String(err.message) : String(err);`, ` if (name === 'ReferenceError' && /\\b(Buffer|process)\\b/.test(msg)) return 'sw_incompat';`, ` if (name === 'EvalError') return 'sw_incompat';`, ` if (/Refused to evaluate|unsafe-eval/.test(msg)) return 'sw_incompat';`, ` return 'domain_error';`, `}`, ]; const classifyExpr = classifyExprParts.join(' '); return [ // STEP 1: register the resolution hook FIRST, so subsequent imports // (including the dynamic import of webm-remux.ts) go through it. `import { register } from 'node:module';`, `import { pathToFileURL } from 'node:url';`, `register(${JSON.stringify(loaderDataUrl)}, pathToFileURL('./'));`, // STEP 2: strip SW-forbidden globals. Must happen AFTER register() // because register() touches `process` internally. Note that // SW_SOURCE_STRIP_GLOBALS does NOT include 'Buffer' — see classifier // comment above for rationale. stripLines, // STEP 3: install chrome.* mock. webm-remux.ts itself does not touch // chrome.*, but its `Logger` import chain might transitively, so we // keep symmetry with Layer 1. chromeMockLine, // STEP 4: import and invoke. Wrap everything in try/catch and emit // sentinels via console.log. `const classify = ${classifyExpr};`, `try {`, ` const mod = await import(${JSON.stringify(remuxSourceUrl)});`, ` if (typeof mod.remuxSegments !== 'function') {`, ` console.log(${JSON.stringify(CHILD_REMUX_SW_INCOMPAT_SENTINEL)});`, ` console.log('NAME:Error');`, ` console.log('MSG:remuxSegments export missing from source module');`, ` console.log('STK:(no stack)');`, ` } else {`, // Synthetic single-segment payload: EBML magic + DocType=webm. ` const segBytes = new Uint8Array([0x1a, 0x45, 0xdf, 0xa3, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6d]);`, ` const segBlob = new Blob([segBytes], { type: 'video/webm' });`, ` const segments = [{ data: segBlob, timestamp: 1 }];`, ` try {`, ` const out = await mod.remuxSegments(segments);`, ` console.log(${JSON.stringify(CHILD_REMUX_OK_SENTINEL)});`, ` console.log('SIZE:' + String(out && typeof out.size === 'number' ? out.size : -1));`, ` } catch (innerErr) {`, ` const outcome = classify(innerErr);`, ` const sentinel = outcome === 'sw_incompat'`, ` ? ${JSON.stringify(CHILD_REMUX_SW_INCOMPAT_SENTINEL)}`, ` : ${JSON.stringify(CHILD_REMUX_DOMAIN_ERROR_SENTINEL)};`, ` console.log(sentinel);`, ` console.log('NAME:' + (innerErr && innerErr.name ? String(innerErr.name) : 'Error'));`, ` console.log('MSG:' + (innerErr && innerErr.message ? String(innerErr.message) : String(innerErr)));`, ` console.log('STK:' + (innerErr && innerErr.stack ? String(innerErr.stack).split('\\n').slice(0, 5).join(' | ') : '(no stack)'));`, ` }`, ` }`, `} catch (importErr) {`, // An error here means the SOURCE module failed to LOAD under stripped // globals — classify it the same way (process/CSP class at module- // init level in webm-remux.ts's transitive deps would be a real // SW-incompat). ` const outcome = classify(importErr);`, ` const sentinel = outcome === 'sw_incompat'`, ` ? ${JSON.stringify(CHILD_REMUX_SW_INCOMPAT_SENTINEL)}`, ` : ${JSON.stringify(CHILD_REMUX_DOMAIN_ERROR_SENTINEL)};`, ` console.log(sentinel);`, ` console.log('NAME:' + (importErr && importErr.name ? String(importErr.name) : 'Error'));`, ` console.log('MSG:' + (importErr && importErr.message ? String(importErr.message) : String(importErr)));`, ` console.log('STK:' + (importErr && importErr.stack ? String(importErr.stack).split('\\n').slice(0, 5).join(' | ') : '(no stack)'));`, `}`, ].join(' '); } /** * Spawn a Node child process that imports the SW chunk under stripped * globals and returns the result. Resolves rather than rejects on import * failure — the failure is the data the test asserts on. * * @param chunkUrl - file:// URL the child will import. * @returns Structured result; `ok: false` means the bundle threw. */ async function runSwBundleImportInChild(chunkUrl: string): Promise { const source = buildChildSource(chunkUrl); const { stdout } = await execFileAsync( process.execPath, ['--input-type=module', '-e', source], { timeout: CHILD_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, }, ); if (stdout.includes(CHILD_OK_SENTINEL)) { return { ok: true, errorMessage: '', errorStackFirstLine: '' }; } if (stdout.includes(CHILD_FAIL_SENTINEL)) { const msgMatch = stdout.match(/^MSG:(.*)$/m); const stkMatch = stdout.match(/^STK:(.*)$/m); return { ok: false, errorMessage: msgMatch?.[1] ?? '(no message)', errorStackFirstLine: stkMatch?.[1] ?? '(no stack)', }; } throw new Error( `Child process produced neither OK nor FAIL sentinel. ` + `stdout was: ${JSON.stringify(stdout)}`, ); } /** * Spawn a Node child process that source-imports `src/background/ * webm-remux.ts` under SW-simulated globals and invokes `remuxSegments` * against a synthetic single-segment payload. Resolves with a structured * outcome — `sw_incompat` is the test failure mode. * * @param remuxSourceUrl - file:// URL of the webm-remux.ts source. * @returns Structured outcome with name/message/stack on errors. */ async function runRemuxRuntimeInChild(remuxSourceUrl: string): Promise { const source = buildRemuxRuntimeChildSource(remuxSourceUrl); const { stdout } = await execFileAsync( process.execPath, // `--experimental-transform-types` enables Node 24's native TS support // (strip types + handle enums/namespaces + auto-resolve type-only // imports). No tsx/ts-node install required. ['--experimental-transform-types', '--input-type=module-typescript', '-e', source], { timeout: CHILD_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, // Suppress the ExperimentalWarning noise on stderr; the sentinel // protocol only inspects stdout, but a clean stderr keeps the // vitest output readable when this test fails. NODE_NO_WARNINGS: '1', }, }, ); const okHit = stdout.includes(CHILD_REMUX_OK_SENTINEL); const domainHit = stdout.includes(CHILD_REMUX_DOMAIN_ERROR_SENTINEL); const swIncompatHit = stdout.includes(CHILD_REMUX_SW_INCOMPAT_SENTINEL); // Exactly one outcome sentinel should appear. SW-incompat takes // precedence in the (impossible) event of multiple — it's the failure // mode we want to surface. if (swIncompatHit) { const nameMatch = stdout.match(/^NAME:(.*)$/m); const msgMatch = stdout.match(/^MSG:(.*)$/m); const stkMatch = stdout.match(/^STK:(.*)$/m); return { outcome: 'sw_incompat', errorName: nameMatch?.[1] ?? '(no name)', errorMessage: msgMatch?.[1] ?? '(no message)', errorStack: stkMatch?.[1] ?? '(no stack)', }; } if (domainHit) { const nameMatch = stdout.match(/^NAME:(.*)$/m); const msgMatch = stdout.match(/^MSG:(.*)$/m); const stkMatch = stdout.match(/^STK:(.*)$/m); return { outcome: 'domain_error', errorName: nameMatch?.[1] ?? '(no name)', errorMessage: msgMatch?.[1] ?? '(no message)', errorStack: stkMatch?.[1] ?? '(no stack)', }; } if (okHit) { return { outcome: 'ok', errorName: '', errorMessage: '', errorStack: '' }; } throw new Error( `Child produced no outcome sentinel. stdout was: ${JSON.stringify(stdout)}`, ); } describe('SW bundle loadability (Tier-1 gate — closes the 01-08 orchestrator gap)', () => { // Resolved at module-level (BEFORE the spawn) so `process.cwd()` and // `process.execPath` are still available. const swChunkUrl = resolveBuiltSwChunkUrl(); const remuxSourceUrl = pathToFileURL(WEBM_REMUX_SOURCE_PATH).href; it('built SW chunk imports without throwing under SW-simulated globals', async () => { const result = await runSwBundleImportInChild(swChunkUrl); expect( result.ok, result.ok ? 'unreachable' : `Built SW bundle throws at top-level module init under ` + `SW-simulated globals (${SW_BUNDLE_STRIP_GLOBALS.join(', ')} ` + `stripped). This is exactly what kills the SW in Chrome and ` + `makes chrome://extensions "service worker" inaccessible.\n\n` + `First line of stack: ${result.errorStackFirstLine}\n` + `Full message: ${result.errorMessage}\n\n` + `Bundle URL: ${swChunkUrl}`, ).toBe(true); }); it('source remuxSegments invocation does not throw an SW-incompatible error', async () => { if (!existsSync(WEBM_REMUX_SOURCE_PATH)) { throw new Error( `webm-remux source not found at ${WEBM_REMUX_SOURCE_PATH}. ` + `The Tier-1 gate's runtime layer requires the SOURCE file ` + `(not just the built bundle).`, ); } const result = await runRemuxRuntimeInChild(remuxSourceUrl); // Domain errors (invalid-EBML parse, missing-Tracks, etc.) on our // intentionally-synthetic input are ACCEPTABLE — they prove the // runtime path is structurally reachable. Success is also fine. // Only `sw_incompat` (process/CSP/eval — or a Buffer error, which // would mean the polyfill itself regressed) is a real failure. expect( result.outcome !== 'sw_incompat', result.outcome === 'sw_incompat' ? `remuxSegments throws an SW-INCOMPATIBLE error when invoked ` + `under SW-simulated globals. This means the SW would crash ` + `mid-archive-save in real Chrome — exactly the kind of bug ` + `Layer 1 (module-init only) cannot catch.\n\n` + `Error name: ${result.errorName}\n` + `Error message: ${result.errorMessage}\n` + `Stack (first 5 frames, ' | '-separated):\n ${result.errorStack}\n\n` + `Source URL: ${remuxSourceUrl}` : 'unreachable', ).toBe(true); }); });