Milestone v1 (v2.0.0): Mokosh — Session Capture #1

Merged
strategy155 merged 297 commits from gsd/phase-04-harden-clean-up-optional into main 2026-05-31 15:34:17 +00:00
Showing only changes of commit 761dfc0388 - Show all commits

View File

@@ -15,10 +15,39 @@
// before any handler can register, and the operator's chrome://extensions // before any handler can register, and the operator's chrome://extensions
// "service worker" link goes inaccessible. // "service worker" link goes inaccessible.
// //
// This test exercises the actual built artifact under SW-simulated globals // The gate ships in TWO layers:
// (Buffer/process/document/window stripped). Any throw at top-level module //
// evaluation surfaces as a clean test failure with the exact stack the // Layer 1 ("built SW chunk imports without throwing...") — verifies the
// operator would see in Chrome. // 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 // Implementation note: the strip+import happens in a SPAWNED Node child
// process, not in-process. Vitest's own RPC layer references both `Buffer` // process, not in-process. Vitest's own RPC layer references both `Buffer`
@@ -39,6 +68,24 @@
// chrome.* behavior is the responsibility of the offscreen-handshake // chrome.* behavior is the responsibility of the offscreen-handshake
// and end-to-end smoke tests, not this gate. // 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 // 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. // fast with a clear "run npm run build" message if `dist/` is missing.
// //
@@ -54,6 +101,12 @@
// https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback // https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback
// Reference for Proxy traps: // Reference for Proxy traps:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy // 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 { execFile } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs'; import { existsSync, readFileSync } from 'node:fs';
@@ -65,35 +118,89 @@ import { describe, expect, it } from 'vitest';
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
// Globals stripped to simulate the MV3 Service Worker runtime. Service // Layer 1 strip list — applied to the spawned child BEFORE it imports the
// Workers expose ServiceWorkerGlobalScope which has NO `window`, NO // BUILT SW chunk. Service Workers expose ServiceWorkerGlobalScope which
// `document`, NO `Buffer`, and NO `process`. They DO have standard // has NO `window`, NO `document`, NO `Buffer`, and NO `process`. They DO
// browser-ish globals (fetch, Blob, Crypto, etc.) which Node provides // have standard browser-ish globals (fetch, Blob, Crypto, etc.) which
// natively in modern versions, so we don't strip those. // Node provides natively in modern versions, so we don't strip those.
const SW_FORBIDDEN_GLOBALS: ReadonlyArray<string> = [ //
// `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<string> = [
'window', 'window',
'document', 'document',
'Buffer', 'Buffer',
'process', '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<string> = [
'window',
'document',
'process',
];
// Sentinel strings emitted by the child process; checked by the parent // Sentinel strings emitted by the child process; checked by the parent
// to distinguish success from failure without resorting to exit codes // to distinguish success from failure without resorting to exit codes
// (the child may exit 0 on a caught throw). // (the child may exit 0 on a caught throw).
const CHILD_OK_SENTINEL = '__SW_BUNDLE_IMPORT_OK__'; const CHILD_OK_SENTINEL = '__SW_BUNDLE_IMPORT_OK__';
const CHILD_FAIL_SENTINEL = '__SW_BUNDLE_IMPORT_FAILED__'; 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 // 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 // import takes longer than this, treat it as a hang (functionally the
// same as a top-level throw from the operator's perspective). // same as a top-level throw from the operator's perspective).
const CHILD_TIMEOUT_MS = 10_000; 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 { interface ChildImportResult {
readonly ok: boolean; readonly ok: boolean;
readonly errorMessage: string; readonly errorMessage: string;
readonly errorStackFirstLine: 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 * 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 * `dist/service-worker-loader.js` shim that crxjs emits. The shim is a
@@ -186,7 +293,7 @@ function buildChromeMockSource(): string {
* @returns ESM source the child runs under `node --input-type=module`. * @returns ESM source the child runs under `node --input-type=module`.
*/ */
function buildChildSource(chunkUrl: string): string { function buildChildSource(chunkUrl: string): string {
const stripLines = SW_FORBIDDEN_GLOBALS.map( const stripLines = SW_BUNDLE_STRIP_GLOBALS.map(
(key) => `delete globalThis['${key}'];`, (key) => `delete globalThis['${key}'];`,
).join(' '); ).join(' ');
const chromeMockLine = buildChromeMockSource(); const chromeMockLine = buildChromeMockSource();
@@ -206,6 +313,160 @@ function buildChildSource(chunkUrl: string): string {
].join(' '); ].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 * Spawn a Node child process that imports the SW chunk under stripped
* globals and returns the result. Resolves rather than rejects on import * globals and returns the result. Resolves rather than rejects on import
@@ -245,10 +506,81 @@ async function runSwBundleImportInChild(chunkUrl: string): Promise<ChildImportRe
); );
} }
/**
* 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<ChildRemuxResult> {
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)', () => { describe('SW bundle loadability (Tier-1 gate — closes the 01-08 orchestrator gap)', () => {
// Resolved at module-level (BEFORE the spawn) so `process.cwd()` and // Resolved at module-level (BEFORE the spawn) so `process.cwd()` and
// `process.execPath` are still available. // `process.execPath` are still available.
const swChunkUrl = resolveBuiltSwChunkUrl(); const swChunkUrl = resolveBuiltSwChunkUrl();
const remuxSourceUrl = pathToFileURL(WEBM_REMUX_SOURCE_PATH).href;
it('built SW chunk imports without throwing under SW-simulated globals', async () => { it('built SW chunk imports without throwing under SW-simulated globals', async () => {
const result = await runSwBundleImportInChild(swChunkUrl); const result = await runSwBundleImportInChild(swChunkUrl);
@@ -258,7 +590,7 @@ describe('SW bundle loadability (Tier-1 gate — closes the 01-08 orchestrator g
result.ok result.ok
? 'unreachable' ? 'unreachable'
: `Built SW bundle throws at top-level module init under ` + : `Built SW bundle throws at top-level module init under ` +
`SW-simulated globals (${SW_FORBIDDEN_GLOBALS.join(', ')} ` + `SW-simulated globals (${SW_BUNDLE_STRIP_GLOBALS.join(', ')} ` +
`stripped). This is exactly what kills the SW in Chrome and ` + `stripped). This is exactly what kills the SW in Chrome and ` +
`makes chrome://extensions "service worker" inaccessible.\n\n` + `makes chrome://extensions "service worker" inaccessible.\n\n` +
`First line of stack: ${result.errorStackFirstLine}\n` + `First line of stack: ${result.errorStackFirstLine}\n` +
@@ -266,4 +598,35 @@ describe('SW bundle loadability (Tier-1 gate — closes the 01-08 orchestrator g
`Bundle URL: ${swChunkUrl}`, `Bundle URL: ${swChunkUrl}`,
).toBe(true); ).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);
});
}); });