Files
mokosh/.planning/phases/01-stabilize-video-pipeline/01-11-PLAN-AMENDMENT-A.md
Mark 565f8fa44c docs(01-11): amendment A — pivot to extension-internal harness page
Architectural pivot triggered by feasibility research prototype
(commit c647f61, A6 PASS 5/5 + Bug-B regression rewind verified).

Two empirical findings invalidate original architecture:
1. MV3 service workers BLOCK dynamic import. await import('test-hooks/
   sw-hooks') in src/background/index.ts silently kills the SW —
   chunk loads, await never resolves, no listeners register. Cited
   Chromium es_modules.md + w3c/webextensions#212.
2. Puppeteer WebWorker.evaluate against MV3 SW only exposes chrome.
   {loadTimes,csi} — not the extension chrome.* API surface.

The Wave 1 (cb1a729) SW-side hooks are fundamentally broken in test
builds (production unaffected — gated by __MOKOSH_UAT__ which is
false in prod). Executor must DELETE the SW-side dynamic import +
sw-hooks.ts entirely; offscreen-side hooks stay (offscreen IS a DOM
document; dynamic import works there).

Replacement (Verdict-A architecture, proven by prototype):
- Extension-internal harness page at chrome-extension://<id>/tests/
  uat/extension-page-harness.html — privileged extension context
  with FULL chrome.* API access
- Puppeteer drives the page via page.goto + page.evaluate
- For SW state: page calls chrome.runtime.sendMessage; SW responds
  via production messaging
- For getDisplayMedia: offscreen-side installFakeDisplayMedia() patches
  navigator.mediaDevices.getDisplayMedia → Canvas captureStream
  synthetic MediaStream

A6 (Bug B regression catch) PROVEN. Industry-standard pattern (MetaMask,
eyeo, Chrome MV3 official testing docs all converge).

Effort remaining: ~7-10h subagent budget (Wave 0 + bonus debug-import
commits keep; Wave 1 hooks rewire 30min; Wave 2 scaffolding 1-2h;
Wave 3 13 more assertions 4-6h; Wave 4 closure 1h).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 12:10:52 +02:00

141 lines
8.6 KiB
Markdown

# 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://<id>/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)