test(debug-01-08): complete SW-bundle-import gate — mock chrome.* surface
The Tier-1 SW-bundle-loadability gate (c75854c) stripped
Buffer/process/window/document from the spawned Node isolate
but did not mock chrome.*. A correctly-bundled SW that reaches
addListener calls at module init would (correctly) progress to
chrome.runtime.onMessage.addListener(...) and throw
ReferenceError because chrome was undefined — a false-positive
RED.
This commit adds a minimal Proxy-based chrome.* stub that
no-ops any chrome.<api>.<method>(...) chain. The gate now
verifies what its file-header comment claims: "bundled artifact
reaches module-init completion under SW-simulated globals."
RED->GREEN: the gate now correctly passes against the post-fix
bundle and would catch any future regression in SW
bundle-loadability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,19 @@
|
||||
// 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.<anything>.<anything>(...)` 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.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
@@ -39,6 +52,8 @@
|
||||
// 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
|
||||
|
||||
import { execFile } from 'node:child_process';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
@@ -115,10 +130,54 @@ function resolveBuiltSwChunkUrl(): string {
|
||||
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.<api>.<method>(...)` 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 and then dynamically
|
||||
* imports the SW chunk, reporting back via stdout sentinels.
|
||||
* 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.
|
||||
@@ -130,8 +189,10 @@ function buildChildSource(chunkUrl: string): string {
|
||||
const stripLines = SW_FORBIDDEN_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)});`,
|
||||
|
||||
Reference in New Issue
Block a user