docs(01-11): amendment A — pivot to extension-internal harness page
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>
This commit is contained in:
@@ -0,0 +1,140 @@
|
|||||||
|
# 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)
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
amended_at: "2026-05-18"
|
||||||
|
amendment: "A"
|
||||||
|
amendment_summary: "Wave 1 T2 + Wave 2 T3 + Wave 3 method guidance superseded. MV3 SW blocks dynamic import (verified empirically); SW-side test hooks DROPPED. Replaced with extension-internal harness page architecture (proven by c647f61 prototype). See 01-11-PLAN-AMENDMENT-A.md."
|
||||||
phase: 01-stabilize-video-pipeline
|
phase: 01-stabilize-video-pipeline
|
||||||
plan: 11
|
plan: 11
|
||||||
type: tdd
|
type: tdd
|
||||||
|
|||||||
Reference in New Issue
Block a user