# Plan 01-11 — Amendment A (2026-05-18) Architectural pivot triggered by Feasibility Research Prototype (commit `c647f61`). Supersedes Wave 1 Task 2 + Wave 2 Task 3 + Wave 3 method guidance in `01-11-PLAN.md` (commit 66e6c4a). Other waves unchanged. ## Why this amendment exists The original Plan 01-11 (66e6c4a) + RESEARCH (969afba) specified driving assertions via `Puppeteer sw.evaluate(chrome.action.getBadgeText)` against the MV3 service worker. **This architecture is empirically infeasible** for two reasons the original research didn't probe: 1. **MV3 service workers BLOCK dynamic import.** `await import('../test-hooks/sw-hooks')` in `src/background/index.ts` silently kills the SW — the chunk loads, the await never resolves, no listeners register. Verified by feasibility-research subagent against Chrome 148. Citations: [Chromium `es_modules.md`](https://chromium.googlesource.com/chromium/src/+/HEAD/content/browser/service_worker/es_modules.md) + [w3c/webextensions#212](https://github.com/w3c/webextensions/issues/212). The Wave 1 (cb1a729) SW-side hooks are therefore **fundamentally broken in test builds**. Production unaffected (gated by `__MOKOSH_UAT__`). 2. **Puppeteer's `WebWorker.evaluate` against MV3 SW only exposes `chrome.{loadTimes, csi}`** — not the extension chrome.* API. The CDP-level abstraction treats MV3 SW as a generic service worker, dropping extension privileges in the isolated world used for `evaluate`. Plus [Puppeteer #9995](https://github.com/puppeteer/puppeteer/issues/9995) — calling `target.worker()` can put SW into a dead state. ## What replaces it (Verdict-A architecture) Drive assertions FROM INSIDE the extension via an internal harness page — the pattern MetaMask, eyeo, and Chrome's official MV3 testing docs converge on: ``` chrome-extension:///tests/uat/extension-page-harness.html ↓ Puppeteer page.goto() drives this PAGE ↓ page calls chrome.* directly (privileged extension page) ↓ for SW state: chrome.runtime.sendMessage round-trip SW background context (production code, untouched) ↓ response harness page → reads result, asserts, exposes verdict via window.__mokoshHarness ↓ Puppeteer reads via page.evaluate ``` The harness page is an extension page — privileged context with FULL chrome.* API access. Puppeteer drives it like any normal page. ## Section overrides ### Wave 1 Task 2 (test hooks) — REPLACED **OLD:** gated SW + offscreen hooks at `src/test-hooks/`; production bundle hook-free. **NEW:** gated OFFSCREEN-ONLY hooks at `src/test-hooks/offscreen-hooks.ts`. **DO NOT add SW-side hooks** (dynamic import blocked in MV3 SW). SW observability moves to extension-internal harness page using only production `chrome.*` APIs. **Action items:** - DELETE the `await import('../test-hooks/sw-hooks')` block from `src/background/index.ts` introduced by cb1a729 - DELETE `src/test-hooks/sw-hooks.ts` entirely if it exists - KEEP `src/test-hooks/offscreen-hooks.ts` and EXTEND with `installFakeDisplayMedia()` (eager-init pattern at module load, gated by `__MOKOSH_UAT__`) - Production grep gate (A0) still asserts `__mokoshTest` absent from prod `dist/` ### Wave 2 Task 3 (harness scaffolding) — REPLACED **OLD:** `tests/uat/lib/*` + `harness.test.ts` skeleton; popup-bridge architecture. **NEW:** Extension-internal harness page architecture: - `tests/uat/extension-page-harness.html` — page skeleton (loaded by Puppeteer) - `tests/uat/extension-page-harness.ts` — controller exposing `window.__mokoshHarness.assertN()` per assertion - `vite.test.config.ts` declares the harness page as a rollup input (so it builds into `dist-test/tests/uat/`) - `tests/uat/lib/driver.ts` — Puppeteer driver utilities (launches Chrome, loads extension, opens harness page, runs assertions) - `tests/uat/harness.test.ts` — top-level test runner invoking each `assertN()` **Reference files (working pattern from c647f61 prototype):** - `tests/uat/prototype/extension-page-harness.ts` - `tests/uat/prototype/extension-page-harness.html` - `tests/uat/prototype/a6.test.ts` - `src/test-hooks/offscreen-hooks.ts` (synthetic stream impl) - `vite.test.config.ts` (rollup input wiring) Executor extends prototype files; promotes from `tests/uat/prototype/` to `tests/uat/` proper. ### Wave 3 Tasks 4-7 (assertions) — METHOD UPDATED Each assertion implemented as `__mokoshHarness.assertN()` on the page, not via Puppeteer `sw.evaluate`. Specific guidance per assertion: - **A0 (hook leak grep)** — keep existing Tier-1 vitest unit-test gate (no change) - **A1 (SW bootstrap)** — harness page reads `chrome.action.getBadgeText({})` + `getPopup({})` after extension load - **A2 (toolbar click → REC)** — `page.triggerExtensionAction(ext)` (Puppeteer 25 native); harness page polls badge state - **A3 (displaySurface=monitor)** — offscreen hook patches `getDisplayMedia` to return synthetic stream with `getSettings().displaySurface='monitor'`; harness asserts via offscreen-bridge query - **A4 (popup during recording)** — `page.bringToFront` on popup target after `triggerExtensionAction` during REC state - **A5 (SAVE_ARCHIVE → download)** — harness sends SAVE_ARCHIVE via `chrome.runtime.sendMessage`; Puppeteer polls headless Downloads dir - **A6 (Bug B regression catch)** — ✅ PROVEN by prototype (5/5 GREEN). Extend pattern verbatim. - **A7 (genuine error → ERR + recovery notif)** — harness sends synthetic `RECORDING_ERROR{error:'codec-unsupported'}` via `chrome.runtime.sendMessage`; asserts badge='ERR' + `chrome.notifications.getAll()` shows recovery notif - **A8 (Bug A onStartup notification)** — harness manually invokes onStartup listener via offscreen-side bridge → trigger SW listener call via test message; asserts `chrome.notifications.getAll()` succeeds with valid `iconUrl` - **A9 (icon file sizes)** — harness fetches `chrome.runtime.getURL('icons/icon{16,48,128}.png')`, checks Content-Length ≥ floors - **A10 (manifest shape)** — harness reads `chrome.runtime.getManifest()` directly - **A11 (35s buffer → ≥3 segments)** — offscreen hook records 35s via synthetic stream; harness queries segment count via offscreen-bridge - **A12 (ffprobe-clean WebM)** — harness produces zip via SAVE_ARCHIVE; Puppeteer extracts; ffprobe via Bash from driver - **A13 (zip structure)** — same as A12; harness/driver inspects zip contents ### Additional gotchas (researcher empirically discovered) - **`tabs` permission missing** → `chrome.tabs.query({active:true})` returns tabs without `.url` → production `startVideoCapture` throws "No active tab found". **Workaround:** bypass `startVideoCapture`; send `START_RECORDING` directly to offscreen via `chrome.runtime.sendMessage`. (For production, this is a separate Plan 5 hardening item — manifest may want `tabs` permission added.) - **Puppeteer #9995**: `target.worker()` can put SW into dead state. **Avoid `worker()` entirely** — page-driven architecture doesn't need it. - **vite `modulePreload.polyfill`**: red herring. Harmless in SW (only fires DOM code if `n.length > 0`). Left enabled in prototype config. ## Effort estimate (revised) | Wave | Status from f44ca3a | Action | Estimate | |---|---|---|---| | 0 (T1) | COMPLETE (96fa8e8, 0cd50fd) | Keep | 0 h | | 1 (T2) | PARTIAL (cb1a729) — SW hooks broken | DELETE SW-side; keep offscreen-side; extend with `installFakeDisplayMedia` | 30 min | | 2 (T3) | PARTIAL (dbd977c) — popup-bridge wrong arch | REPLACE with extension-page architecture per prototype | 1-2 h | | 3 (T4-T7) | NOT STARTED | Extend prototype to 14 assertions | 4-6 h | | 4 (T8-T9) | NOT STARTED | Closure ceremony + operator brand checkpoint | 1 h | **Total remaining:** ~7-10 hours subagent budget. ## Acceptance unchanged Per original PLAN.md §"Success criteria": - All 14 assertions GREEN (A0 already GREEN from Wave 0) - Production bundle hook-free (Tier-1 grep gate enforces) - Existing vitest baseline preserved (89/89 at amendment time) - Plan 01-09 closure via harness PASS (Task 9 `checkpoint:human-verify`) ## Sources - [eyeo's MV3 SW testing journey](https://developer.chrome.com/blog/eyeos-journey-to-testing-mv3-service%20worker-suspension) - [Chrome MV3 E2E testing official guide](https://developer.chrome.com/docs/extensions/mv3/end-to-end-testing/) - [Chromium ES Modules in Service Workers](https://chromium.googlesource.com/chromium/src/+/HEAD/content/browser/service_worker/es_modules.md) - [w3c/webextensions#212 — dynamic import in background SW](https://github.com/w3c/webextensions/issues/212) - [Puppeteer #9995](https://github.com/puppeteer/puppeteer/issues/9995) - [MetaMask extension driver.js](https://github.com/MetaMask/metamask-extension/blob/develop/test/e2e/webdriver/driver.js)