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

8.6 KiB

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

    • w3c/webextensions#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 — 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 missingchrome.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