Plan 01-11 orchestrator commissioned a research+prototype investigation
into whether full MV3 UAT automation is feasible with the architecture:
extension-internal test page + chrome.runtime.sendMessage bridge +
synthetic MediaStream (canvas-captureStream + getSettings override).
EMPIRICAL VERDICT: feasible BUT plan 01-11 needs architectural revision.
Architectural findings (with proof):
1. DYNAMIC IMPORT BLOCKED IN MV3 SW. Top-of-module
`await import('../test-hooks/sw-hooks')` in src/background/index.ts
silently kills the SW (chunk loads, await never resolves, no
production listeners register, no console output). This is by design
per Chromium docs (es_modules.md) + w3c/webextensions#212. The Plan
01-11 RESEARCH §6 architecture was wrong for the SW side.
Workaround in this prototype: REMOVE the SW-side gated dynamic
import. SW-side test hooks need a different design (see verdict).
2. OFFSCREEN-SIDE DYNAMIC IMPORT WORKS. Offscreen is a DOM document,
not a SW, so top-level await + dynamic import behave normally. The
offscreen-hooks.ts gated import succeeds; installFakeDisplayMedia is
installed eagerly at module load.
3. EXTENSION-INTERNAL PAGE HAS FULL chrome.* SURFACE. Reachable via
chrome-extension://<id>/tests/uat/prototype/extension-page-harness.html
(added as rollup input in vite.test.config.ts). The page can call
chrome.action.getBadgeText, chrome.action.getPopup, chrome.offscreen
.createDocument, chrome.notifications.getAll, chrome.runtime
.sendMessage — everything needed for A6.
4. NO 'tabs' PERMISSION → tab.url IS UNDEFINED. Production
startVideoCapture's `chrome.tabs.query({active:true})` check
(`if (!tab.id || !tab.url) throw`) fails because the manifest lacks
the 'tabs' permission. Prototype workaround: bypass startVideoCapture
by sending START_RECORDING directly to offscreen. The Bug B
contract being tested is independent of how recording starts; it
only depends on the RECORDING_ERROR routing path.
5. SYNTHETIC MEDIASTREAM WORKS. installFakeDisplayMedia builds a
canvas-captureStream MediaStream + monkey-patches the video track's
getSettings() to report displaySurface: 'monitor'. Production code's
post-grant validation passes. getDisplayMedia returns the synthetic
stream immediately — no picker, no headless flakiness.
A6 prototype result (with Bug B fix in place — current HEAD state):
[PASS] SETUP: badge becomes REC after start
[PASS] A6.1: badge text is '' (NOT 'ERR') after user-stop
[PASS] A6.2: popup is '' (NOT manifest default) after user-stop
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
A6 prototype result (with Bug B fix rewound to `if (false)`):
[PASS] SETUP: badge becomes REC after start
[FAIL] A6.1: badge text is '' (got "ERR")
[FAIL] A6.2: popup is '' (got chrome-extension://.../popup/index.html)
[FAIL] A6.3: notif delta = 0 (got 1)
[PASS] A6.4: isRecording=false ← false-positive (badge='ERR' not 'REC')
The Bug B regression rewind cycle proves the harness CAN catch regression:
4/5 checks turn RED on rewind, 5/5 turn GREEN with the fix restored.
Files in this commit:
- tests/uat/prototype/extension-page-harness.{html,ts} — the harness
page (chrome-extension URL, exposes window.__mokoshHarness.assertA6)
- tests/uat/prototype/a6.test.ts — Puppeteer driver (~270 lines)
- tests/uat/prototype/probe_*.mjs — diagnostic probes used to isolate
the SW dynamic-import blocker (probe_sw.mjs is the key one)
- src/test-hooks/offscreen-hooks.ts — added installFakeDisplayMedia +
dispatchEndedOnTrack + __mokoshOffscreenQuery bridge handler + auto-
install at module load
- vite.test.config.ts — added prototype harness page as rollup input;
added modulePreload.polyfill=false (red herring; harmless)
- src/background/index.ts — removed the broken SW-side gated dynamic
import (this is the BLOCKER unblocker — production 01-11 plan needs
to redesign SW-side test hooks before re-spawning)
Bundle hygiene: prototype runs against dist-test/; production dist/
remains hook-free (Tier-1 grep gate still GREEN, verified via
no-test-hooks-in-prod-bundle.test.ts in the unit test suite).
Vitest baseline: 89/89 GREEN preserved.
Runtime: ~7 seconds end-to-end (launch Chrome + open page + ensure
offscreen + start recording + dispatch ended + settle + assert).
See: research return for VERDICT + recommended next step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
4.5 KiB
TypeScript
89 lines
4.5 KiB
TypeScript
// vite.test.config.ts — Plan 01-11 two-bundle separation.
|
|
//
|
|
// Extends the production `./vite.config.ts` with the following delta knobs:
|
|
// 1. `mode: 'test'` — Vite statically replaces `import.meta.env.MODE`
|
|
// everywhere in the input source with the string literal `'test'`.
|
|
// 2. `define: { __MOKOSH_UAT__: 'true' }` — the dedicated build-time
|
|
// token gating the test-hook dynamic imports in
|
|
// src/background/index.ts + src/offscreen/recorder.ts (Plan 01-11
|
|
// Task 2). With this set to `true` the `if (__MOKOSH_UAT__)` branch
|
|
// becomes a live branch and Rollup KEEPS the dynamic imports;
|
|
// production builds (vite.config.ts sets it `false`) tree-shake
|
|
// them away (verified by the Tier-1 grep gate
|
|
// `tests/background/no-test-hooks-in-prod-bundle.test.ts`).
|
|
// 3. `build.outDir: 'dist-test'` + `emptyOutDir: true` — emit to a
|
|
// SEPARATE directory so a `npm run build` immediately after this
|
|
// build does not clobber. Puppeteer harness consumes this path via
|
|
// `puppeteer.launch({ enableExtensions: [<abs-path-to-dist-test>] })`.
|
|
// 4. `build.modulePreload: { polyfill: false }` — CRITICAL SW FIX.
|
|
// Vite's default module-preload polyfill calls
|
|
// `document.getElementsByTagName` + `document.querySelector` at
|
|
// module init in EVERY chunk that contains a dynamic import. The
|
|
// production bundle has no dynamic imports (the test-hook gate is
|
|
// dead code; tree-shaken). The test bundle HAS the dynamic
|
|
// `await import('../test-hooks/sw-hooks')` — so the preload
|
|
// polyfill gets included in the SW chunk. SWs have no DOM —
|
|
// `document` is undefined — and the polyfill throws on the very
|
|
// first await, killing the SW module init silently (no console
|
|
// output, just a dead worker). Disabling the polyfill removes the
|
|
// `document.*` references; modern Chrome (and our MV3 target ≥88)
|
|
// supports native dynamic import without the polyfill.
|
|
// Empirically verified: with the polyfill enabled, the test
|
|
// bundle's SW never reaches `Service Worker initializing` log;
|
|
// with it disabled, the SW initializes and chrome.runtime.onMessage
|
|
// handlers respond. See Plan 01-11 PROTOTYPE research session.
|
|
//
|
|
// PROTOTYPE addition: the prototype harness page at
|
|
// `tests/uat/prototype/extension-page-harness.html` is added as a
|
|
// Rollup input so the test build emits it. Production builds do NOT
|
|
// include the prototype page (vite.config.ts has no such input).
|
|
//
|
|
// References:
|
|
// - Vite mergeConfig: https://vite.dev/guide/api-javascript.html#mergeconfig
|
|
// - Vite environment variables: https://vite.dev/guide/env-and-mode.html
|
|
// - Vite build.modulePreload: https://vite.dev/config/build-options.html#build-modulepreload
|
|
// - Rollup multi-entry inputs: https://rollupjs.org/configuration-options/#input
|
|
|
|
import { defineConfig, mergeConfig, type UserConfigExport } from 'vite';
|
|
import baseConfig from './vite.config';
|
|
|
|
const baseAsExport: UserConfigExport = baseConfig;
|
|
|
|
export default defineConfig(({ command, mode }) =>
|
|
mergeConfig(
|
|
typeof baseAsExport === 'function'
|
|
? baseAsExport({ command, mode, isPreview: false, isSsrBuild: false })
|
|
: baseAsExport,
|
|
{
|
|
mode: 'test',
|
|
// `define` performs a static text replacement at build time. We use a
|
|
// dedicated `__MOKOSH_UAT__` token (NOT `import.meta.env.MODE`) for the
|
|
// hook-import gate because vitest defaults its mode to 'test' too —
|
|
// gating on MODE would activate the hooks during unit tests and
|
|
// overwrite their vi.fn() mocks for chrome.notifications.create etc.
|
|
// The dedicated token is `false` everywhere except in this test bundle
|
|
// (Plan 01-11 RESEARCH §6 augmented — keeps Vite tree-shake semantics
|
|
// intact while sidestepping the vitest cross-contamination).
|
|
// Reference: https://vite.dev/config/shared-options.html#define
|
|
define: {
|
|
__MOKOSH_UAT__: 'true',
|
|
},
|
|
build: {
|
|
outDir: 'dist-test',
|
|
emptyOutDir: true,
|
|
// CRITICAL: see file header comment §4 — disables the
|
|
// document.*-using module preload polyfill that crashes SW init.
|
|
modulePreload: { polyfill: false },
|
|
rollupOptions: {
|
|
input: {
|
|
// Add the prototype harness page so it lands in dist-test/
|
|
// and becomes reachable as
|
|
// chrome-extension://<id>/tests/uat/prototype/extension-page-harness.html
|
|
prototype_harness: 'tests/uat/prototype/extension-page-harness.html',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
),
|
|
);
|