Architectural pivot triggered by feasibility research prototype (commitc647f61, 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>
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:
-
MV3 service workers BLOCK dynamic import.
await import('../test-hooks/sw-hooks')insrc/background/index.tssilently kills the SW — the chunk loads, the await never resolves, no listeners register. Verified by feasibility-research subagent against Chrome 148. Citations: Chromiumes_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__).
- w3c/webextensions#212.
The Wave 1 (
-
Puppeteer's
WebWorker.evaluateagainst MV3 SW only exposeschrome.{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 forevaluate. Plus Puppeteer #9995 — callingtarget.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 fromsrc/background/index.tsintroduced bycb1a729 - DELETE
src/test-hooks/sw-hooks.tsentirely if it exists - KEEP
src/test-hooks/offscreen-hooks.tsand EXTEND withinstallFakeDisplayMedia()(eager-init pattern at module load, gated by__MOKOSH_UAT__) - Production grep gate (A0) still asserts
__mokoshTestabsent from proddist/
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 exposingwindow.__mokoshHarness.assertN()per assertionvite.test.config.tsdeclares the harness page as a rollup input (so it builds intodist-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 eachassertN()
Reference files (working pattern from c647f61 prototype):
tests/uat/prototype/extension-page-harness.tstests/uat/prototype/extension-page-harness.htmltests/uat/prototype/a6.test.tssrc/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
getDisplayMediato return synthetic stream withgetSettings().displaySurface='monitor'; harness asserts via offscreen-bridge query - A4 (popup during recording) —
page.bringToFronton popup target aftertriggerExtensionActionduring 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'}viachrome.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 validiconUrl - 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)
tabspermission missing →chrome.tabs.query({active:true})returns tabs without.url→ productionstartVideoCapturethrows "No active tab found". Workaround: bypassstartVideoCapture; sendSTART_RECORDINGdirectly to offscreen viachrome.runtime.sendMessage. (For production, this is a separate Plan 5 hardening item — manifest may wanttabspermission added.)- Puppeteer #9995:
target.worker()can put SW into dead state. Avoidworker()entirely — page-driven architecture doesn't need it. - vite
modulePreload.polyfill: red herring. Harmless in SW (only fires DOM code ifn.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)