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.
|
// 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.
|
// 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
|
// 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.
|
||||||
//
|
//
|
||||||
@@ -39,6 +52,8 @@
|
|||||||
// https://nodejs.org/api/esm.html#import-expressions
|
// https://nodejs.org/api/esm.html#import-expressions
|
||||||
// Reference for `child_process.execFile`:
|
// Reference for `child_process.execFile`:
|
||||||
// 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:
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
|
||||||
|
|
||||||
import { execFile } from 'node:child_process';
|
import { execFile } from 'node:child_process';
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
@@ -115,10 +130,54 @@ function resolveBuiltSwChunkUrl(): string {
|
|||||||
return pathToFileURL(chunkAbsolutePath).href;
|
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
|
* Build the inline ESM source that the spawned child will execute. The
|
||||||
* child strips the listed globals on its own isolate and then dynamically
|
* child strips the listed globals on its own isolate, installs a no-op
|
||||||
* imports the SW chunk, reporting back via stdout sentinels.
|
* `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
|
* Kept as a function (not a template literal at module top) so the
|
||||||
* sentinel/global lists stay synced with the constants above.
|
* sentinel/global lists stay synced with the constants above.
|
||||||
@@ -130,8 +189,10 @@ function buildChildSource(chunkUrl: string): string {
|
|||||||
const stripLines = SW_FORBIDDEN_GLOBALS.map(
|
const stripLines = SW_FORBIDDEN_GLOBALS.map(
|
||||||
(key) => `delete globalThis['${key}'];`,
|
(key) => `delete globalThis['${key}'];`,
|
||||||
).join(' ');
|
).join(' ');
|
||||||
|
const chromeMockLine = buildChromeMockSource();
|
||||||
return [
|
return [
|
||||||
stripLines,
|
stripLines,
|
||||||
|
chromeMockLine,
|
||||||
`try {`,
|
`try {`,
|
||||||
` await import(${JSON.stringify(chunkUrl)});`,
|
` await import(${JSON.stringify(chunkUrl)});`,
|
||||||
` console.log(${JSON.stringify(CHILD_OK_SENTINEL)});`,
|
` console.log(${JSON.stringify(CHILD_OK_SENTINEL)});`,
|
||||||
|
|||||||
Reference in New Issue
Block a user