diff --git a/.planning/phases/01-stabilize-video-pipeline/01-11-PLAN-AMENDMENT-A.md b/.planning/phases/01-stabilize-video-pipeline/01-11-PLAN-AMENDMENT-A.md new file mode 100644 index 0000000..ce3f57d --- /dev/null +++ b/.planning/phases/01-stabilize-video-pipeline/01-11-PLAN-AMENDMENT-A.md @@ -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:///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) diff --git a/.planning/phases/01-stabilize-video-pipeline/01-11-PLAN.md b/.planning/phases/01-stabilize-video-pipeline/01-11-PLAN.md index 5769f8f..0c40191 100644 --- a/.planning/phases/01-stabilize-video-pipeline/01-11-PLAN.md +++ b/.planning/phases/01-stabilize-video-pipeline/01-11-PLAN.md @@ -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 plan: 11 type: tdd