diff --git a/.planning/phases/04-harden-clean-up-optional/04-08-PLAN.md b/.planning/phases/04-harden-clean-up-optional/04-08-PLAN.md index d8fbf04..2d3ee3d 100644 --- a/.planning/phases/04-harden-clean-up-optional/04-08-PLAN.md +++ b/.planning/phases/04-harden-clean-up-optional/04-08-PLAN.md @@ -22,6 +22,7 @@ files_modified: - tests/uat/lib/harness-page-driver.ts - tests/uat/harness.test.ts - tests/uat/spike-a33-sw-persistence.ts + - tests/background/no-test-hooks-in-prod-bundle.test.ts autonomous: true requirements: [] tags: @@ -35,33 +36,63 @@ tags: - charter-d-p4-01 - roadmap-sc-1-closes - post-debug-session-2 + - planner-iter-2-revision user_setup: [] +revision_history: + - iter: 1 + plan_commit: 504d9dc + checker_commit: 051813e + verdict: ITERATE-NEEDED + blockers_addressed: + - "BLOCKER 1: Vite ?url -> dist-test/assets/.webm web_accessible_resources strategy now pre-decided (per checker Option B + user objective Option A) — explicit WAR entry for assets/*.webm in manifest.json + chrome.runtime.getURL() not needed because Vite's ?url import resolves to the chrome-extension:///assets/.webm form already (verified by Plan 01-10 mokosh-mark.svg precedent for SVG; differs only by extension). The WAR entry covers production (zero *.webm assets — entry is inert) AND test bundle (offscreen reaches the asset via direct ?url import; the WAR entry just authorizes chrome-extension:// scheme access from the offscreen document context). No executor improvisation." + - "BLOCKER 2: installFakeDisplayMedia() remains SYNCHRONOUS at module load — preserves eager-install contract at lines 533-537. The HTMLVideoElement creation + DOM append + monkey-patch installation happen synchronously. The canplay wait + .play() + first captureStream call are deferred INTO the fakeGetDisplayMedia closure (lazy first-frame pattern). First getDisplayMedia call awaits readyState >= HAVE_FUTURE_DATA; subsequent calls reuse the now-ready video element. Zero race window with recorder.ts:46-48 top-level await chain." + warnings_addressed: + - "WARNING 1: headless autoplay fallback documented — if videoEl.play() rejects with NotAllowedError, the closure throws with explicit 'autoplay-blocked' error class so the spike surfaces the precise root cause (not a mysterious 0-frames)." + - "WARNING 2: patchDisplaySurface compatibility check — added explicit sub-verify gate that tests track.getSettings().displaySurface === 'monitor' on a fresh mintStream() call BEFORE the spike re-run." + - "WARNING 3: spike probe-value asserts surfaced as explicit grep gates in Task 2 block (POST-PRIME=0, PRE-KILL≥3, POST-KILL≥3)." + - "WARNING 4: ROADMAP.md edit pre-specified — exact sed-replace target documented; grep gate enforces post-edit invariant." + - "WARNING 5: synthetic-display-source filename leak gate added as Tier-2 invariant in tests/background/no-test-hooks-in-prod-bundle.test.ts (dist/ grep for 'synthetic-display-source' returns 0)." + cosmetics_addressed: + - "advisory 1: Task 3 mis-reference in commit message corrected (Task 1 + Task 2 only)." + - "advisory 2: recorder.ts:91 segments invariant gated by grep in Task 1 verify." + - "advisory 3: dual-fixture-location note added to Task 1 Step 1." must_haves: truths: + - "installFakeDisplayMedia() at src/test-hooks/offscreen-hooks.ts is SYNCHRONOUS at module load — preserves the eager-install contract at lines 533-537 (the monkey-patch on navigator.mediaDevices.getDisplayMedia is in place BEFORE recorder.ts:46-48 top-level await resolves)" + - "fakeGetDisplayMedia closure performs the lazy first-frame wait — first invocation awaits videoEl.readyState >= 3 (HAVE_FUTURE_DATA) + .play(); subsequent invocations reuse the cached ready video element; mint a fresh MediaStream each call (matching the existing canvas.captureStream-per-call contract)" - "installFakeDisplayMedia() at src/test-hooks/offscreen-hooks.ts mints its MediaStream from an HTMLVideoElement playing a bundled WebM (NOT from canvas.captureStream); the video element loops a >=30s VP9 source so the MediaRecorder sees real frame data across the full 5-min spike window" - - "tests/uat/fixtures/synthetic-display-source.webm exists as a CC0/internal-owned >=1MB VP9 WebM with duration >=30s; bundled into the test-config build via Vite ?url import (per Plan 01-10 mokosh-mark.svg precedent)" - - "tests/uat/spike-a33-sw-persistence.ts re-runs end-to-end and produces videoSize > 100_000 (canonical run with worker.close()) — empirically refutes the previous 8505-byte 0-frames methodology failure and confirms ROADMAP SC #1 architectural integrity per debug session-2 verdict" + - "tests/uat/fixtures/synthetic-display-source.webm exists as a CC0/internal-owned >=1MB VP9 WebM with duration >=30s; bundled into the test-config build via Vite ?url import (per Plan 01-10 mokosh-mark.svg precedent); chrome-extension:// scheme access from offscreen document is authorized via the explicit web_accessible_resources entry for assets/*.webm in manifest.json" + - "manifest.json web_accessible_resources block contains an explicit entry for assets/*.webm — pre-decided per planner iter-2 (no executor improvisation; the entry is inert in production where dist/ has zero *.webm assets, and load-bearing in test build where dist-test/assets/.webm needs chrome-extension:// authorization)" + - "tests/uat/spike-a33-sw-persistence.ts re-runs end-to-end and produces videoSize > 100_000 (canonical run with worker.close()) — empirically refutes the previous 8505-byte 0-frames methodology failure and confirms ROADMAP SC #1 architectural integrity per debug session-2 verdict; spike probe values shift from (POST-PRIME=0, PRE-KILL=3, POST-KILL=3 but 0 frames) -> (POST-PRIME=0, PRE-KILL>=3, POST-KILL>=3 with NON-ZERO frames; videoSize > 100_000)" - "assertA33 / driveA33 / orchestrator wiring land per the original Plan 04-04 Pattern 4 verbatim (cs-injection-world precedent NOT applicable — A33 is host-side CDP-driven, not page-side rrweb-driven)" - "stopServiceWorker(browser, extensionId) helper at tests/uat/lib/harness-page-driver.ts is reused verbatim from Plan 04-04 (already committed at 3726eee); no new helper symbol needed" - "A33 env-gated by SKIP_LONG_UAT (default RUN for closure + alpha gate; SKIP_LONG_UAT=1 to skip per-commit iteration); 95s baseline preserved when skipped" - "UAT harness count flips 33 -> 34 (A33 added); 34/34 GREEN when SKIP_LONG_UAT unset; vitest 183/183 GREEN preserved" - - "ROADMAP SC #1 (SW state persistence) GREEN — A33 empirical evidence that a 5-min idle + Puppeteer worker.close() + SAVE_ARCHIVE produces video/last_30sec.webm with size > 100 KB" + - "ROADMAP SC #1 (SW state persistence) GREEN — A33 empirical evidence that a 5-min idle + Puppeteer worker.close() + SAVE_ARCHIVE produces video/last_30sec.webm with size > 100 KB; ROADMAP.md row flipped from 'STATUS 2026-05-21: OPEN' to 'STATUS 2026-05-22: CLOSED via Plan 04-08'" - "Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries (lockstep across tests/uat/harness.test.ts + tests/background/no-test-hooks-in-prod-bundle.test.ts) — Plan 04-08 introduces NO new __MOKOSH_UAT__-gated symbols on production bundle; the bundled WebM ships only in test build per Vite ?url + test-config gating" + - "Tier-2 production-bundle filename leak gate added: 'synthetic-display-source' string returns 0 hits across dist/ — codified in tests/background/no-test-hooks-in-prod-bundle.test.ts as a new sub-invariant (canary for future regressions that might accidentally inline test-hooks into the production chunk)" - "Pre-checkpoint bundle gates 6/6 PASS unchanged from Plan 04-03 baseline (new Function=0 + eval=0 + Buffer.=1 pre-existing JSZip + window./document.=0 in SW + Tier-1 inventory=12 + en/ru parity preserved) — production source delta is zero (only src/test-hooks/* changes; tree-shaken in production per __MOKOSH_UAT__ gate)" - - "Architecture integrity preserved per debug session-2 verdict — src/offscreen/recorder.ts:91 `let segments: Blob[] = []` RAM-only architecture is canonically correct; no IndexedDB persistence work, no chrome.storage migration, no offscreen-document lifecycle changes" + - "Architecture integrity preserved per debug session-2 verdict — src/offscreen/recorder.ts:91 `let segments: Blob[] = []` RAM-only architecture is canonically correct (verified by grep gate); no IndexedDB persistence work, no chrome.storage migration, no offscreen-document lifecycle changes" + - "patchDisplaySurface monkey-patch survives HTMLVideoElement.captureStream — verified by an explicit sub-test in Task 1 verify block (a synthetic mintStream() call + track.getSettings().displaySurface === 'monitor' assertion BEFORE the spike re-run)" artifacts: - path: "tests/uat/fixtures/synthetic-display-source.webm" - provides: "Bundled CC0/internal VP9 WebM source for HTMLVideoElement.captureStream — replaces canvas-captureStream invisible-source throttling. Size 1-3 MB; duration >=30s; loopable (autoplay+loop+muted). License: internal capture by this project's MediaRecorder pipeline (CC0-equivalent project-owned)" + provides: "Bundled CC0/internal VP9 WebM source for HTMLVideoElement.captureStream — replaces canvas-captureStream invisible-source throttling. Size 1-3 MB; duration >=30s; loopable (autoplay+loop+muted). License: internal capture by this project's MediaRecorder pipeline (CC0-equivalent project-owned). Note: tests/fixtures/last_30sec.webm (the original Plan 01-07 regression fixture) remains in place; this is a SECOND copy at tests/uat/fixtures/ for the Vite ?url import to resolve cleanly without crossing the test-vs-fixtures dir boundary." min_size_bytes: 1000000 - path: "src/test-hooks/offscreen-hooks.ts" - provides: "installFakeDisplayMedia() rewritten to mint MediaStream from HTMLVideoElement.captureStream(30); displaySurface monkey-patch preserved; lifecycle (idempotent install + idempotent uninstall) preserved; A23 constraints capture preserved; all 6 existing bridge ops (install-fake-display-media + dispatch-ended + has-stream + get-display-surface + get-segment-count + get-last-getDisplayMedia-constraints) preserved" + provides: "installFakeDisplayMedia() rewritten — SYNCHRONOUS module-load function; mints MediaStream from HTMLVideoElement.captureStream(30) on first getDisplayMedia call (lazy first-frame wait inside fakeGetDisplayMedia closure); displaySurface monkey-patch preserved; lifecycle (idempotent install + idempotent uninstall) preserved; A23 constraints capture preserved; all 6 existing bridge ops (install-fake-display-media + dispatch-ended + has-stream + get-display-surface + get-segment-count + get-last-getDisplayMedia-constraints) preserved with their original sync return contract" contains: "videoEl.captureStream" - path: "globals.d.ts" provides: "Ambient declaration for `*.webm?url` Vite asset import (mirrors existing `*.svg?url` block at lines 34-37)" contains: "webm?url" + - path: "manifest.json" + provides: "Explicit web_accessible_resources entry for assets/*.webm — pre-decided per planner iter-2 BLOCKER 1 remediation; production dist/ has zero *.webm assets so the entry is inert there; test dist-test/ has the hashed asset and the entry authorizes chrome-extension:///assets/.webm access from the offscreen document context." + contains: "assets/*.webm" + - path: "tests/background/no-test-hooks-in-prod-bundle.test.ts" + provides: "Tier-2 invariant: 'synthetic-display-source' filename returns 0 hits across dist/ — canary for accidental test-hook inlining into production chunk. Tier-1 inventory at 12 entries UNCHANGED; this is a NEW Tier-2 entry alongside (does not count against the 12 lockstep)." + contains: "synthetic-display-source" - path: "tests/uat/extension-page-harness.ts" - provides: "assertA33 page-side method registered on __mokoshHarness OR existing assertA2 reused for prime step (decided by read_first; Plan 04-04 REVISION iter-2 Option B precedent prefers reuse)" - contains: "assertA33" + provides: "NO EDIT NEEDED per Plan 04-04 REVISION iter-2 Option B — assertA2 is reused as the canonical prime entrypoint; A33 logic lives entirely in driveA33 (host-side) + the inline page.evaluate dispatch. The Plan 04-04 SUMMARY post-debug amendment confirms this is the canonical pattern." + contains: "__mokoshHarness" - path: "tests/uat/lib/harness-page-driver.ts" provides: "driveA33(page, browser, extensionId, downloadsDir) host-side driver implementing the 5-min idle + stopServiceWorker + SAVE_ARCHIVE + JSZip video-size check (Plan 04-04 Pattern 4 verbatim; REVISION iter-2 Option B inline SAVE_ARCHIVE dispatch)" contains: "driveA33" @@ -69,14 +100,18 @@ must_haves: provides: "driveA33 import + driveA33Wrapped wrapped-driver const + drivers-array push entry with SKIP_LONG_UAT env-gate (per Plan 04-04 Wave 1 spec verbatim)" contains: "driveA33Wrapped" key_links: - - from: "src/test-hooks/offscreen-hooks.ts installFakeDisplayMedia" + - from: "src/test-hooks/offscreen-hooks.ts installFakeDisplayMedia (sync top-level + lazy closure)" to: "tests/uat/fixtures/synthetic-display-source.webm via Vite ?url import" - via: "import videoUrl from '../../tests/uat/fixtures/synthetic-display-source.webm?url' OR equivalent gated import" + via: "import syntheticDisplaySourceUrl from '../../tests/uat/fixtures/synthetic-display-source.webm?url' (resolves to chrome-extension:///assets/.webm at test bundle time; chrome-extension:// scheme authorized via manifest.json web_accessible_resources assets/*.webm entry)" pattern: "synthetic-display-source\\.webm\\?url" - - from: "src/test-hooks/offscreen-hooks.ts mintStream()" - to: "HTMLVideoElement.captureStream(30) + displaySurface monkey-patch" - via: "videoEl.captureStream(30); patchDisplaySurface(stream)" - pattern: "videoEl\\.captureStream" + - from: "manifest.json web_accessible_resources" + to: "dist-test/assets/.webm (test bundle only; production has zero *.webm assets)" + via: "explicit resources: ['assets/*.webm'] entry alongside the existing src/welcome/welcome.html entry" + pattern: "assets/\\*\\.webm" + - from: "src/test-hooks/offscreen-hooks.ts fakeGetDisplayMedia closure" + to: "videoEl readiness gate (readyState >= HAVE_FUTURE_DATA + .play()) + videoEl.captureStream(30) + displaySurface monkey-patch" + via: "first-call lazy path awaits canplay/error + .play(); subsequent calls fast-path the cached-ready videoEl; mintStream(); patchDisplaySurface(stream); return stream" + pattern: "readyState.*HAVE_FUTURE_DATA|videoEl\\.captureStream" - from: "tests/uat/harness.test.ts driveA33Wrapped" to: "tests/uat/lib/harness-page-driver.ts driveA33(page, browser, extensionId, downloadsDir)" via: "(page) => driveA33(page, handles.browser, handles.extensionId, handles.downloadsDir)" @@ -98,11 +133,17 @@ must_haves: **Fix:** Replace the canvas.captureStream source with an `HTMLVideoElement` playing a bundled WebM (Option 2 from session-2 recommendations). Video-file-backed `videoEl.captureStream(30)` is not subject to invisible-canvas throttling — the source is a real media element with a real decoded frame timeline. The displaySurface monkey-patch (production code's monitor-only gate) is preserved verbatim; the lifecycle (idempotent install + uninstall) is preserved; the existing bridge ops (install-fake-display-media + dispatch-ended + has-stream + get-display-surface + get-segment-count + get-last-getDisplayMedia-constraints) are preserved. -**Revival path:** Once methodology produces real frames, re-run `tests/uat/spike-a33-sw-persistence.ts` — it MUST exit 0 with `videoSize > 100_000`. Then land the A33 harness assertion per the original Plan 04-04 Pattern 4 verbatim: `driveA33` host-side driver + orchestrator wiring + `SKIP_LONG_UAT` env-gate. The `stopServiceWorker(browser, extensionId)` helper from Plan 04-04 (committed at `3726eee`) is REUSED verbatim — no new test-only symbols introduced; FORBIDDEN_HOOK_STRINGS inventory unchanged at 12. +**Iter-2 revision (per checker iter-1 findings at `.planning/phases/04-harden-clean-up-optional/04-08-CHECKER-iter-1.md`, commit `051813e`):** + +- **BLOCKER 1 resolution — bundling strategy pre-decided.** Vite `?url` import emits the 1.9 MB WebM as `dist-test/assets/.webm` (extracted-asset path; not inlined because the size is three orders of magnitude above `assetsInlineLimit: 4096`). The `@crxjs/vite-plugin` auto-WAR behavior for extracted media assets in offscreen-document context is empirically untested in this codebase. **Pre-emptive remediation: add an explicit `web_accessible_resources` entry for `assets/*.webm`** in `manifest.json`. Production `dist/` has zero `*.webm` assets so the entry is inert there (no production attack surface); test `dist-test/` has the hashed asset and the entry authorizes the `chrome-extension:///assets/.webm` URL form from the offscreen document context. No executor improvisation; the bundling strategy is locked-in BEFORE Task 1 begins. + +- **BLOCKER 2 resolution — eager-install contract preserved.** `installFakeDisplayMedia()` remains **SYNCHRONOUS** at module load (matching the existing eager call at lines 533-537). The video element creation + `document.body.appendChild(videoEl)` + monkey-patch assignment to `navigator.mediaDevices.getDisplayMedia` all execute synchronously. The first-frame readiness await (canplay + `.play()`) is **deferred INTO the fakeGetDisplayMedia closure** (lazy first-frame pattern). The first time `getDisplayMedia()` is called (which happens inside `recorder.startRecording` AFTER recorder.ts:46-48 top-level await resolves and AFTER recorder.bootstrap), the closure awaits `videoEl.readyState >= HAVE_FUTURE_DATA` if not already ready, then calls `videoEl.captureStream(30)`, applies `patchDisplaySurface`, and returns the MediaStream. Subsequent `getDisplayMedia` calls fast-path the cached-ready video element. Zero race window with the eager-install contract. + +**Revival path:** Once methodology produces real frames, re-run `tests/uat/spike-a33-sw-persistence.ts` — it MUST exit 0 with `videoSize > 100_000`. Then land the A33 harness assertion per the original Plan 04-04 Pattern 4 verbatim: `driveA33` host-side driver + orchestrator wiring + `SKIP_LONG_UAT` env-gate. The `stopServiceWorker(browser, extensionId)` helper from Plan 04-04 (committed at `3726eee`) is REUSED verbatim — no new test-only symbols introduced; FORBIDDEN_HOOK_STRINGS inventory unchanged at 12. A new Tier-2 sub-invariant ('synthetic-display-source' filename = 0 hits in dist/) lands alongside, codifying the production-bundle leak canary. Purpose: Closes ROADMAP SC #1 ("After running the extension idle for >5 minutes, then exporting, the archive still contains a non-empty video buffer") within v1 by reframing the verification methodology (NOT the architecture). Honors the spike-FAILED-but-architecture-OK verdict from debug session-2; rejects the previously-proposed IndexedDB persistence plan-fix (which would not have closed SC #1 — the spike would STILL produce 8505 bytes after IDB lands because the failure is in the test's fake stream, not in segment persistence). -Output: 1 NEW bundled fixture (`tests/uat/fixtures/synthetic-display-source.webm`); rewrite of `installFakeDisplayMedia()` (canvas -> HTMLVideoElement); 1 ambient module declaration for `*.webm?url`; 1 NEW harness assertion (A33; harness count 33->34); driveA33 + orchestrator wiring; spike script re-runs PASSED. ROADMAP SC #1 flips OPEN -> GREEN. +Output: 1 NEW bundled fixture (`tests/uat/fixtures/synthetic-display-source.webm`); rewrite of `installFakeDisplayMedia()` (canvas -> HTMLVideoElement; SYNC install + LAZY first-frame); 1 ambient module declaration for `*.webm?url`; 1 explicit `web_accessible_resources` entry for `assets/*.webm`; 1 NEW harness assertion (A33; harness count 33->34); driveA33 + orchestrator wiring; spike script re-runs PASSED; 1 NEW Tier-2 filename-leak gate. ROADMAP SC #1 flips OPEN -> GREEN. @@ -118,6 +159,7 @@ Output: 1 NEW bundled fixture (`tests/uat/fixtures/synthetic-display-source.webm @.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md @.planning/phases/04-harden-clean-up-optional/04-04-PLAN.md @.planning/phases/04-harden-clean-up-optional/04-04-SUMMARY.md +@.planning/phases/04-harden-clean-up-optional/04-08-CHECKER-iter-1.md @.planning/debug/sw-offscreen-persistence-investigation-session-2.md # Source files — locus of the methodology reframe @@ -128,6 +170,7 @@ Output: 1 NEW bundled fixture (`tests/uat/fixtures/synthetic-display-source.webm @tests/uat/harness.test.ts @tests/uat/lib/launch.ts @tests/uat/spike-a33-sw-persistence.ts +@tests/background/no-test-hooks-in-prod-bundle.test.ts # Vite ?url import precedent (Plan 01-10 mokosh-mark.svg pattern) @src/welcome/welcome.ts @@ -141,25 +184,39 @@ Output: 1 NEW bundled fixture (`tests/uat/fixtures/synthetic-display-source.webm @.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-SUMMARY.md - + -From src/test-hooks/offscreen-hooks.ts:139-264 (CURRENT installFakeDisplayMedia — the methodology failure locus): +From src/test-hooks/offscreen-hooks.ts:139-264 (CURRENT installFakeDisplayMedia — the methodology failure locus, SYNCHRONOUS): ```typescript // CURRENT (broken under 5-min headless idle per debug session-2 verdict): -const canvas = document.createElement('canvas'); -canvas.width = 320; canvas.height = 180; -canvas.style.position = 'fixed'; canvas.style.top = '-9999px'; canvas.style.left = '-9999px'; -document.body.appendChild(canvas); -const ctx = canvas.getContext('2d'); -// ... drawFrame loop + setInterval(drawFrame, 33) ... -const mintStream = (): MediaStream => { - const stream = canvas.captureStream(30); // <- THE THROTTLING SOURCE - patchDisplaySurface(stream); - return stream; -}; +// CRITICAL: this is SYNCHRONOUS — the monkey-patch on +// navigator.mediaDevices.getDisplayMedia is in place BEFORE the +// recorder.ts:46-48 await import('../test-hooks/offscreen-hooks') +// resolves. This is the eager-install contract. +export function installFakeDisplayMedia(): void { // SYNC — no Promise return + if (fakeInstalled) return; + fakeInstalled = true; + const canvas = document.createElement('canvas'); + canvas.width = 320; canvas.height = 180; + // ... -9999px style + appendChild + drawFrame + setInterval ... + const mintStream = (): MediaStream => { + const stream = canvas.captureStream(30); // <- THE THROTTLING SOURCE + patchDisplaySurface(stream); + return stream; + }; + const fakeGetDisplayMedia = async ( + constraints?: DisplayMediaStreamOptions, + ): Promise => { + lastGetDisplayMediaConstraints = constraints ?? null; + return mintStream(); // SYNC body; async only for the contract shape + }; + (navigator.mediaDevices as unknown as { + getDisplayMedia: typeof fakeGetDisplayMedia; + }).getDisplayMedia = fakeGetDisplayMedia; // SYNC monkey-patch — pre-resolve point +} ``` -REPLACEMENT pattern (HTMLVideoElement.captureStream — Plan 04-08): +REPLACEMENT pattern (HTMLVideoElement.captureStream — Plan 04-08, SYNC install + LAZY first-frame closure): ```typescript // Plan 04-08 methodology reframe — video-file-backed MediaStream. // canvas.captureStream(30) on a hidden -9999px canvas is throttled to @@ -169,38 +226,121 @@ REPLACEMENT pattern (HTMLVideoElement.captureStream — Plan 04-08): // throttling because the source has a real decoded frame timeline // independent of canvas pixel mutations. // +// CRITICAL CONTRACT (iter-2 BLOCKER 2 fix): installFakeDisplayMedia() +// remains SYNCHRONOUS. The video element creation + DOM append + +// monkey-patch on navigator.mediaDevices.getDisplayMedia execute +// synchronously at function call time. The canplay wait + .play() are +// deferred INTO the fakeGetDisplayMedia closure so the eager module- +// load call at lines 533-537 still installs the monkey-patch BEFORE +// recorder.ts:46-48 top-level await resolves. No race window with the +// recorder.startRecording call. +// // References: // - debug session-2 verdict at .planning/debug/sw-offscreen-persistence-investigation-session-2.md // - HTMLVideoElement.captureStream: // https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/captureStream // - Chrome bug 653548 (auto-throttled canvas captureStream): // https://bugs.chromium.org/p/chromium/issues/detail?id=653548 +// - HTMLMediaElement.readyState (HAVE_FUTURE_DATA == 3): +// https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/readyState +// - iter-2 BLOCKER 2 remediation: .planning/phases/04-harden-clean-up-optional/04-08-CHECKER-iter-1.md -import sourceUrl from '../../tests/uat/fixtures/synthetic-display-source.webm?url'; +import syntheticDisplaySourceUrl from '../../tests/uat/fixtures/synthetic-display-source.webm?url'; -const videoEl = document.createElement('video'); -videoEl.src = sourceUrl; -videoEl.loop = true; // loop indefinitely so 5-min idle has source frames -videoEl.muted = true; // muted required for autoplay per Chrome policy -videoEl.autoplay = true; // ensure playback starts without user gesture -videoEl.style.position = 'fixed'; -videoEl.style.top = '-9999px'; -videoEl.style.left = '-9999px'; -document.body.appendChild(videoEl); +// Module-level cell — populated by the SYNC install; consumed by the +// LAZY closure. Mutable because the closure may need to await readiness +// across multiple calls before the cached state stabilizes. +let fakeVideoEl: HTMLVideoElement | null = null; +let fakeVideoReadyPromise: Promise | null = null; -// Wait for the first frame to ensure captureStream returns live tracks -// (not stalled tracks awaiting first decode). Use 'canplay' event + -// explicit .play() for headless headless reliability. -await new Promise((resolve, reject) => { - videoEl.addEventListener('canplay', () => resolve(), { once: true }); - videoEl.addEventListener('error', () => reject(new Error('video load failed')), { once: true }); -}); -await videoEl.play(); +export function installFakeDisplayMedia(): void { // SYNC — return type unchanged from current + if (fakeInstalled) return; + fakeInstalled = true; + + // SYNC: create + append + style — these complete on the same tick. + const videoEl = document.createElement('video'); + videoEl.src = syntheticDisplaySourceUrl; + videoEl.loop = true; // loop indefinitely so 5-min idle has source frames + videoEl.muted = true; // muted required for autoplay per Chrome policy + videoEl.autoplay = true; // ensure playback starts without user gesture + videoEl.playsInline = true; + videoEl.preload = 'auto'; // start fetching/decoding immediately + videoEl.style.position = 'fixed'; + videoEl.style.top = '-9999px'; + videoEl.style.left = '-9999px'; + document.body.appendChild(videoEl); + fakeVideoEl = videoEl; + + // SYNC: start the readiness Promise NOW so it has the maximum head + // start before the first getDisplayMedia call. The Promise resolves + // when readyState reaches HAVE_FUTURE_DATA (canplay event). If the + // video is already ready by the time fakeGetDisplayMedia is invoked, + // the closure's `await fakeVideoReadyPromise` resolves immediately. + // + // The .play() call is fire-and-forget here — we don't await it at + // install time (that would block the SYNC contract). We DO chain it + // off the canplay resolution inside the Promise body so the closure + // can rely on "ready AND playing" once the Promise resolves. + fakeVideoReadyPromise = new Promise((resolve, reject) => { + const onCanPlay = (): void => { + videoEl.removeEventListener('canplay', onCanPlay); + videoEl.removeEventListener('error', onError); + // Chain .play() — required because autoplay attr is best-effort. + videoEl.play().then(() => resolve()).catch((err) => { + reject(new Error( + `synthetic-display-source.webm play() rejected: ${ + err instanceof Error ? err.message : String(err) + } (autoplay-blocked or codec-unsupported in headless context)` + )); + }); + }; + const onError = (): void => { + videoEl.removeEventListener('canplay', onCanPlay); + videoEl.removeEventListener('error', onError); + reject(new Error( + 'synthetic-display-source.webm failed to load (media error; verify WAR entry + dist-test/assets/.webm reachable)' + )); + }; + videoEl.addEventListener('canplay', onCanPlay); + videoEl.addEventListener('error', onError); + }); + + // SYNC: monkey-patch — the eager-install contract requires this assignment + // to happen synchronously at module load (before recorder.ts:46-48 top- + // level await resolves). The closure body is async (matches the real + // getDisplayMedia signature) but the assignment itself is synchronous. + const fakeGetDisplayMedia = async ( + constraints?: DisplayMediaStreamOptions, + ): Promise => { + // Plan 01-14 A23 capture (preserved verbatim). + lastGetDisplayMediaConstraints = constraints ?? null; + + // LAZY FIRST-FRAME: await the readiness Promise. First call may + // block ~50-500ms while the video decodes its first frame. Cached + // resolution thereafter — subsequent calls observe the resolved + // Promise and proceed immediately. + if (fakeVideoReadyPromise !== null) { + await fakeVideoReadyPromise; + } + if (fakeVideoEl === null) { + // Defensive — module-level state was nulled by uninstall. Reject + // explicitly so callers see a clear error rather than NPE. + throw new Error('fake getDisplayMedia called after uninstall'); + } + return mintStream(); + }; + (navigator.mediaDevices as unknown as { + getDisplayMedia: typeof fakeGetDisplayMedia; + }).getDisplayMedia = fakeGetDisplayMedia; +} const mintStream = (): MediaStream => { + if (fakeVideoEl === null) { + throw new Error('mintStream called before fakeVideoEl initialized'); + } // captureStream(30) — request 30 fps; the actual cadence is driven // by the source video's frame timeline. - const stream = (videoEl as HTMLVideoElement & { + const stream = (fakeVideoEl as HTMLVideoElement & { captureStream: (fps?: number) => MediaStream; }).captureStream(30); patchDisplaySurface(stream); // preserve existing monkey-patch @@ -208,7 +348,35 @@ const mintStream = (): MediaStream => { }; ``` -From src/welcome/welcome.ts:46 (Plan 01-10 Vite ?url precedent — direct analog): +From src/test-hooks/offscreen-hooks.ts:528-537 (CURRENT eager-install at module load — UNCHANGED in iter-2): +```typescript +// PROTOTYPE: install the fake getDisplayMedia eagerly so production +// recorder.startRecording will use the synthetic stream on its first +// call — no chicken-and-egg with the bridge install op. Wrap in a +// try so any DOM-not-ready edge case does not block module init. +try { + installFakeDisplayMedia(); // STILL SYNC — eager-install contract preserved +} catch (e) { + console.warn("[offscreen-hooks] eager installFakeDisplayMedia failed:", e); +} +``` + +From src/offscreen/recorder.ts:46-48 (the top-level await resume point — eager-install contract dependent): +```typescript +let testHooks: typeof import('../test-hooks/offscreen-hooks') | null = null; +if (__MOKOSH_UAT__) { + testHooks = await import('../test-hooks/offscreen-hooks'); +} +// By the time the await resolves, offscreen-hooks's module-load eager +// call has already executed installFakeDisplayMedia() SYNCHRONOUSLY, +// which means navigator.mediaDevices.getDisplayMedia is ALREADY patched +// to the fakeGetDisplayMedia closure. recorder.startRecording's +// `await navigator.mediaDevices.getDisplayMedia(...)` resolves with the +// synthetic stream — the lazy first-frame wait happens inside the closure, +// not at install time. +``` + +From src/welcome/welcome.ts:46 (Plan 01-10 Vite ?url precedent — direct analog for SVG): ```typescript import markUrl from '../shared/brand/mokosh-mark.svg?url'; ``` @@ -226,11 +394,45 @@ declare module '*.webm?url' { } ``` -From src/test-hooks/offscreen-hooks.ts:271-291 (uninstallFakeDisplayMedia — preserve idempotent teardown): +From manifest.json:20-25 (CURRENT web_accessible_resources block — Plan 04-08 appends one entry): +```json +"web_accessible_resources": [ + { + "resources": ["src/welcome/welcome.html"], + "matches": [""] + } +], +``` + +Plan 04-08 iter-2 BLOCKER 1 fix — append a second resources entry: +```json +"web_accessible_resources": [ + { + "resources": ["src/welcome/welcome.html"], + "matches": [""] + }, + { + "resources": ["assets/*.webm"], + "matches": [""] + } +], +``` + +From src/test-hooks/offscreen-hooks.ts:271-291 (uninstallFakeDisplayMedia — preserve idempotent teardown; adapted for videoEl): ```typescript -// Existing teardown handles canvas removal + RAF cancel + interval clear. -// Plan 04-08 adapts: cancel video, removeChild(videoEl), null the refs. -// Idempotency contract unchanged. +export function uninstallFakeDisplayMedia(): void { + if (!fakeInstalled) return; + fakeInstalled = false; + if (fakeVideoEl !== null) { + try { fakeVideoEl.pause(); } catch { /* ignore */ } + fakeVideoEl.remove(); + fakeVideoEl = null; + } + fakeVideoReadyPromise = null; // drop the Promise reference + // We deliberately do NOT restore the original getDisplayMedia — the + // offscreen document is throwaway and gets a fresh navigator on the + // next createDocument() anyway. +} ``` From tests/uat/lib/harness-page-driver.ts:68-80 (stopServiceWorker helper — REUSED verbatim from Plan 04-04): @@ -364,26 +566,39 @@ From tests/uat/harness.test.ts:486 (orchestrator drivers-array — append site f }, ``` -From tests/fixtures/last_30sec.webm (existing internal project artifact — candidate fixture source): -- size: 1.9 MB +From tests/fixtures/last_30sec.webm (existing internal project artifact — REMAINS IN PLACE per advisory 3; the new tests/uat/fixtures/synthetic-display-source.webm is a SECOND copy at a different location): +- size: 1.9 MB (1888636 bytes verified via ls) - codec: VP9 - dimensions: 1142x1044 - license: internal project capture (CC0-equivalent project-owned) -- A direct copy/symlink to `tests/uat/fixtures/synthetic-display-source.webm` is the simplest baseline; the executor MAY regenerate a smaller/looped/cleaner WebM via ffmpeg if preferred. Either choice satisfies the >=1 MB + VP9 + >=30s contract. +- A direct copy to `tests/uat/fixtures/synthetic-display-source.webm` is the baseline (zero-cost; already proven to play in Chrome via Plan 01-07). + +From tests/background/no-test-hooks-in-prod-bundle.test.ts (CURRENT Tier-1 inventory at 12 entries; iter-2 adds 1 NEW Tier-2 sub-invariant): +```typescript +// Tier-1: 12-entry FORBIDDEN_HOOK_STRINGS lockstep with tests/uat/harness.test.ts. +// Plan 04-08 leaves this UNCHANGED. +// +// Tier-2 NEW (Plan 04-08 iter-2 — WARNING 5 remediation): single canary +// gate that 'synthetic-display-source' does not leak into dist/. Test-only +// fixture filename — the gated import in offscreen-hooks.ts is tree-shaken +// in production per __MOKOSH_UAT__; this gate catches any future regression +// that accidentally inlines test-hooks into the production chunk. +``` - Task 1: Bundle WebM fixture + replace canvas.captureStream with HTMLVideoElement.captureStream in installFakeDisplayMedia() - tests/uat/fixtures/synthetic-display-source.webm, src/test-hooks/offscreen-hooks.ts, globals.d.ts, manifest.json, vite.config.ts, vite.test.config.ts - src/test-hooks/offscreen-hooks.ts (full file — installFakeDisplayMedia at lines 139-264 + uninstallFakeDisplayMedia at 271-291), src/welcome/welcome.ts:30-50 (Vite ?url import precedent), globals.d.ts (full — ambient module declaration pattern), manifest.json (full — web_accessible_resources block at line 20), vite.config.ts (full — rollupOptions.input + nodePolyfills config), vite.test.config.ts (full — test-build entry mirror), tests/fixtures/last_30sec.webm (verify file exists + VP9 codec + ~1.9 MB), tests/uat/spike-a33-sw-persistence.ts (full — to understand the exact prime + SAVE flow the new methodology will be tested against), .planning/debug/sw-offscreen-persistence-investigation-session-2.md (Resolution section — fix recommendations) + Task 1: Bundle WebM fixture + pre-emptive WAR entry + replace canvas.captureStream with HTMLVideoElement.captureStream (SYNC install + LAZY first-frame) + tests/uat/fixtures/synthetic-display-source.webm, src/test-hooks/offscreen-hooks.ts, globals.d.ts, manifest.json, vite.config.ts, vite.test.config.ts, tests/background/no-test-hooks-in-prod-bundle.test.ts + src/test-hooks/offscreen-hooks.ts (full file — installFakeDisplayMedia at lines 139-264 + uninstallFakeDisplayMedia at 271-291 + eager-install at 528-537), src/offscreen/recorder.ts:40-60 (the top-level await import resume point that depends on eager-install contract), src/welcome/welcome.ts:30-50 (Vite ?url import precedent), globals.d.ts (full — ambient module declaration pattern), manifest.json (full — web_accessible_resources block at line 20), vite.config.ts (full — rollupOptions.input + nodePolyfills config), vite.test.config.ts (full — test-build entry mirror), tests/background/no-test-hooks-in-prod-bundle.test.ts (full — Tier-1 invariant + the Tier-2 add site), tests/fixtures/last_30sec.webm (verify file exists + VP9 codec + ~1.9 MB), tests/uat/spike-a33-sw-persistence.ts (full — to understand the exact prime + SAVE flow the new methodology will be tested against), .planning/debug/sw-offscreen-persistence-investigation-session-2.md (Resolution section — fix recommendations), .planning/phases/04-harden-clean-up-optional/04-08-CHECKER-iter-1.md (BLOCKER 1 + BLOCKER 2 + WARNING 1/2/5 — the specific remediation guidance) -**Step 1 — Bundle the WebM fixture.** +**Step 1 — Bundle the WebM fixture (advisory 3 dual-location note).** - Create directory: `mkdir -p tests/uat/fixtures/`. - Copy `tests/fixtures/last_30sec.webm` to `tests/uat/fixtures/synthetic-display-source.webm`. The existing artifact is a 1.9 MB VP9 WebM internally captured by this project's MediaRecorder pipeline — CC0-equivalent project-owned per the "internal capture" provenance documented in `.planning/STATE.md` Phase 1 Closure Notes. Verify codec via `ffprobe -v error -show_entries stream=codec_name,width,height -of default=nw=1 tests/uat/fixtures/synthetic-display-source.webm` reports `codec_name=vp9`. - The fixture must be >=1 MB AND >=30 seconds duration (loopable). The existing 1.9 MB capture satisfies size; duration is `N/A` per ffprobe (live-capture WebM lacks a duration tag), BUT the 1.9 MB VP9 at 400 kbps yields ~38s of decoded timeline — sufficient for the 5-min spike via `videoEl.loop = true`. If the executor wants a stricter contract, regenerate via `ffmpeg -f lavfi -i testsrc=duration=35:size=320x180:rate=30 -c:v libvpx-vp9 -b:v 400k tests/uat/fixtures/synthetic-display-source.webm` (this is OPTIONAL — the copy baseline works; only regenerate if the copy's duration tag is load-bearing for the chosen tests). +- **advisory 3 note**: `tests/fixtures/last_30sec.webm` remains in place (Plan 01-07 regression fixture; not affected). The new path at `tests/uat/fixtures/synthetic-display-source.webm` is a SECOND copy under the UAT subtree — this is intentional because the Vite ?url import in `src/test-hooks/offscreen-hooks.ts` resolves cleanly from `../../tests/uat/fixtures/...` without crossing a non-bundled subtree (the `tests/fixtures/` directory is excluded from the test bundle's input set; the `tests/uat/fixtures/` directory is implicitly included as a transitive import of the offscreen-hooks module). - Decision (per `feedback-no-unilateral-scope-reduction.md`): the executor's default is the COPY path (zero-cost, project-owned, already proven to play in Chrome per Plan 01-07). Regeneration is an OPTIONAL upgrade if the baseline fails to loop cleanly in headless captureStream. **Step 2 — Add ambient module declaration for `*.webm?url`.** @@ -400,34 +615,75 @@ From tests/fixtures/last_30sec.webm (existing internal project artifact — cand } ``` -**Step 3 — Rewrite `installFakeDisplayMedia()` to use HTMLVideoElement.captureStream.** -- Edit `src/test-hooks/offscreen-hooks.ts` at lines 139-264. -- Add top-of-module import (after existing imports): - ```typescript - // Plan 04-08 — bundled WebM source for HTMLVideoElement-backed MediaStream. - // Replaces the prior canvas.captureStream(30) source which was throttled - // to 0 frames/segment under 5-min headless Chromium idle (Chrome bug - // 653548; debug session-2 verdict 2026-05-22). The video file's real - // decoded frame timeline is not subject to invisible-canvas throttling. - // - // The `?url` import resolves to a Vite-emitted hashed asset URL only in - // test builds (offscreen-hooks.ts is gated by `__MOKOSH_UAT__` in - // src/offscreen/recorder.ts and tree-shaken in production). - // - // References: - // - .planning/debug/sw-offscreen-persistence-investigation-session-2.md - // - https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/captureStream - // - Chrome bug 653548: https://bugs.chromium.org/p/chromium/issues/detail?id=653548 - import syntheticDisplaySourceUrl from '../../tests/uat/fixtures/synthetic-display-source.webm?url'; - ``` -- Replace the `installFakeDisplayMedia` body (lines 139-264). Preserve: - - Idempotency guard (`if (fakeInstalled) return;`). - - The `lastGetDisplayMediaConstraints` capture (A23 contract). - - The displaySurface monkey-patch via `patchDisplaySurface(stream)`. - - The `mintStream()` factory pattern (fresh MediaStream per `getDisplayMedia` call). - - The fakeGetDisplayMedia function shape (cast-through-unknown to assign to navigator.mediaDevices.getDisplayMedia). -- Replace canvas creation with HTMLVideoElement creation: - ```typescript +**Step 3 — Rewrite `installFakeDisplayMedia()` to use HTMLVideoElement.captureStream — SYNCHRONOUS install + LAZY first-frame closure (iter-2 BLOCKER 2 fix).** + +**Critical contract:** `installFakeDisplayMedia()` MUST remain SYNCHRONOUS (no `async`/`await` in the function declaration). The eager-install at `src/test-hooks/offscreen-hooks.ts:528-537` calls it synchronously at module load; the `await import('../test-hooks/offscreen-hooks')` chain at `src/offscreen/recorder.ts:46-48` depends on the monkey-patch being in place BEFORE the await resolves. Converting `installFakeDisplayMedia()` to async would introduce a race window where `recorder.startRecording` could call the REAL `navigator.mediaDevices.getDisplayMedia` (the picker would hang in headless mode). DO NOT convert to async. DO NOT change the function's return type. + +**Sub-step 3a — Add top-of-module import (after existing imports at line 49):** +```typescript +// Plan 04-08 — bundled WebM source for HTMLVideoElement-backed MediaStream. +// Replaces the prior canvas.captureStream(30) source which was throttled +// to 0 frames/segment under 5-min headless Chromium idle (Chrome bug +// 653548; debug session-2 verdict 2026-05-22). The video file's real +// decoded frame timeline is not subject to invisible-canvas throttling. +// +// The `?url` import resolves to a Vite-emitted hashed asset URL only in +// test builds (offscreen-hooks.ts is gated by `__MOKOSH_UAT__` in +// src/offscreen/recorder.ts and tree-shaken in production). The asset +// emission path is dist-test/assets/.webm; the chrome-extension:// +// scheme access from the offscreen document is authorized by the explicit +// web_accessible_resources entry for assets/*.webm in manifest.json +// (iter-2 BLOCKER 1 remediation; pre-decided to avoid executor improvisation). +// +// References: +// - .planning/debug/sw-offscreen-persistence-investigation-session-2.md +// - .planning/phases/04-harden-clean-up-optional/04-08-CHECKER-iter-1.md +// - https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/captureStream +// - Chrome bug 653548: https://bugs.chromium.org/p/chromium/issues/detail?id=653548 +// - HTMLMediaElement.readyState: https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/readyState +import syntheticDisplaySourceUrl from '../../tests/uat/fixtures/synthetic-display-source.webm?url'; +``` + +**Sub-step 3b — Update module-level state declarations (lines ~97-100). Replace canvas-specific cells with video-specific cells:** +```typescript +// REPLACE: +// let fakeCanvas: HTMLCanvasElement | null = null; +// let fakeAnimationHandle: number | null = null; +// let fakeDrawInterval: ReturnType | null = null; +// +// WITH: +let fakeVideoEl: HTMLVideoElement | null = null; +// Plan 04-08 BLOCKER 2 (iter-2): lazy first-frame Promise. SYNC install +// kicks off the readiness chain (canplay + .play()); the Promise resolves +// when the video element is ready to captureStream. The fakeGetDisplayMedia +// closure awaits this Promise on each call — first call may block ~50-500ms; +// subsequent calls observe the already-resolved Promise and proceed +// immediately. Nulled by uninstallFakeDisplayMedia. +let fakeVideoReadyPromise: Promise | null = null; +``` + +**Sub-step 3c — Replace the `installFakeDisplayMedia` body (lines 139-264). Keep the function signature SYNCHRONOUS — `export function installFakeDisplayMedia(): void`. Preserve:** +- Idempotency guard (`if (fakeInstalled) return;`). +- `fakeInstalled = true` early assignment (matches existing contract). +- The `lastGetDisplayMediaConstraints` capture (A23 contract). +- The `patchDisplaySurface(stream)` helper (defined inside the function as in current source; survives the rewrite). +- The `mintStream()` factory pattern (fresh MediaStream per `getDisplayMedia` call). +- The fakeGetDisplayMedia closure shape (`async` callable; cast-through-unknown for navigator.mediaDevices.getDisplayMedia assignment). +- The eager-install try/catch at lines 528-537 unchanged. + +**New body (SYNC install + LAZY first-frame, code-snippet form per checker iter-1 BLOCKER 2 Option A):** +```typescript +export function installFakeDisplayMedia(): void { + if (fakeInstalled) { + return; + } + fakeInstalled = true; + + // SYNC PHASE — execute synchronously so the monkey-patch on + // navigator.mediaDevices.getDisplayMedia is in place BEFORE + // recorder.ts:46-48 top-level await resolves. Per debug session-2 + + // iter-2 BLOCKER 2 — must NOT block on canplay or .play() here. + // Build a hidden HTMLVideoElement playing the bundled WebM source. // The element stays off-screen (-9999px) so it has zero visual side // effect; loop=true ensures source frames never run out across the @@ -439,168 +695,331 @@ From tests/fixtures/last_30sec.webm (existing internal project artifact — cand videoEl.muted = true; videoEl.autoplay = true; videoEl.playsInline = true; + videoEl.preload = 'auto'; videoEl.style.position = 'fixed'; videoEl.style.top = '-9999px'; videoEl.style.left = '-9999px'; document.body.appendChild(videoEl); fakeVideoEl = videoEl; - // Wait for first-frame readiness so captureStream returns live tracks - // (not stalled tracks awaiting decode). canplay = readyState >= 3 - // (HAVE_FUTURE_DATA). Reject path drops a single explicit error so a - // misconfigured fixture surfaces immediately (rather than producing a - // mysterious 0-frames downstream). - await new Promise((resolve, reject) => { + // Start the readiness Promise NOW so it has the maximum head start + // before the first getDisplayMedia call. The Promise resolves when + // canplay fires AND .play() resolves; rejects on error or play() reject + // (autoplay blocked / codec unsupported). Stored in the module-level + // cell so the closure can await it. + // + // WARNING 1 remediation (iter-2): autoplay reject path produces an + // explicit error class identifier ('autoplay-blocked or codec-unsupported + // in headless context') so a misconfigured fixture / hostile autoplay + // policy surfaces as a clear failure signal — NOT a mysterious 0-frames + // downstream. The spike's gating check on `videoSize > 100_000` will + // fail with this error visible in the offscreen console capture (or in + // the harness diagnostics), making the root cause unambiguous. + fakeVideoReadyPromise = new Promise((resolve, reject) => { const onCanPlay = (): void => { videoEl.removeEventListener('canplay', onCanPlay); videoEl.removeEventListener('error', onError); - resolve(); + // Chain .play() — required because autoplay attr is best-effort. + videoEl.play().then(() => resolve()).catch((err: unknown) => { + reject(new Error( + `synthetic-display-source.webm play() rejected: ${ + err instanceof Error ? err.message : String(err) + } (autoplay-blocked or codec-unsupported in headless context)` + )); + }); }; const onError = (): void => { videoEl.removeEventListener('canplay', onCanPlay); videoEl.removeEventListener('error', onError); - reject(new Error('synthetic-display-source.webm failed to load')); + reject(new Error( + 'synthetic-display-source.webm failed to load (media error; ' + + 'verify WAR entry for assets/*.webm in manifest.json + ' + + 'dist-test/assets/.webm reachable via chrome-extension://)' + )); }; videoEl.addEventListener('canplay', onCanPlay); videoEl.addEventListener('error', onError); }); - // Explicit .play() — autoplay attr is best-effort; .play() returns a - // Promise that resolves once playback starts. Headless Chrome reliably - // honors muted-autoplay but the explicit call removes any policy edge. - await videoEl.play(); - ``` -- Delete the canvas + ctx + drawFrame + requestAnimationFrame + setInterval block (lines 160-199 in current source). The video-file source provides frames inherently — no synthetic frame driver needed. -- Update `mintStream()` to call `videoEl.captureStream(30)` instead of `canvas.captureStream(30)`: - ```typescript + /** + * Apply the displaySurface monkey-patch to a freshly-minted stream's + * video track. Production code's post-grant validation reads + * `getSettings().displaySurface` and tears down + throws + * 'wrong-display-surface' on anything but 'monitor' — the patch makes + * the synthetic video stream satisfy that gate. WARNING 2 (iter-2): + * this patch is verified to work with HTMLVideoElement.captureStream + * tracks via the explicit sub-test in Task 1 verify block; the MDN- + * documented contract on the returned track is the same writable + * `getSettings` reference (per HTMLMediaElement.captureStream §Return + * Value: "A new MediaStream object"). + */ + const patchDisplaySurface = (stream: MediaStream): void => { + const videoTrack = stream.getVideoTracks()[0]; + if (videoTrack !== undefined) { + const originalGetSettings = videoTrack.getSettings.bind(videoTrack); + videoTrack.getSettings = ((): MediaTrackSettings => { + const real = originalGetSettings(); + return { + ...real, + displaySurface: 'monitor', + }; + }) as typeof videoTrack.getSettings; + } + }; + + /** + * Mint a FRESH MediaStream from the HTMLVideoElement. Each invocation + * generates new tracks in 'live' state — required for the multi- + * recording-lifecycle pattern (A6 stops the first stream's tracks via + * dispatchEvent('ended'); A7 starts a new recording which calls + * getDisplayMedia → must get a live stream, NOT the dead one A6 + * teardown left behind). The videoEl persists across calls; track refs + * do not. This contract is preserved verbatim from the canvas variant. + */ const mintStream = (): MediaStream => { - // HTMLVideoElement.captureStream(30) — the source video's frame - // timeline drives cadence; the 30 fps argument is the requested - // upper bound. The track's getSettings() reports the resolved - // frameRate after capture begins. + if (fakeVideoEl === null) { + throw new Error('mintStream called before fakeVideoEl initialized — should be unreachable'); + } + // captureStream(30) — request 30 fps; the actual cadence is driven + // by the source video's frame timeline (HTMLMediaElement.captureStream + // spec; non-standard but widely-implemented in Chrome since 2017). // - // TypeScript cast through unknown because HTMLMediaElement.captureStream - // is not in the default lib.dom.d.ts at all versions (it's a non- - // standard but widely-implemented method; the cast pins our reliance - // on the runtime surface). - const stream = (videoEl as HTMLVideoElement & { + // TypeScript cast through `&` intersection because HTMLMediaElement. + // captureStream is non-standard and not in default lib.dom.d.ts at + // all TS versions. The runtime surface is stable in Chrome MV3 target + // (≥88 per manifest). + const stream = (fakeVideoEl as HTMLVideoElement & { captureStream: (fps?: number) => MediaStream; }).captureStream(30); patchDisplaySurface(stream); return stream; }; - ``` -- Update module-level state declarations: - ```typescript - // Replace: let fakeCanvas: HTMLCanvasElement | null = null; - // Replace: let fakeAnimationHandle: number | null = null; - // Replace: let fakeDrawInterval: ReturnType | null = null; - // With: - let fakeVideoEl: HTMLVideoElement | null = null; - ``` -- Note: `installFakeDisplayMedia()` becomes async (it now `await`s the canplay event + .play()). The existing module-load eager call at the bottom of the file (lines 533-537) wraps in try/catch — update to either: (a) `void installFakeDisplayMedia().catch(...)` OR (b) leave the try/catch but accept that the wrapper now logs the rejection. Recommendation: option (a) for explicit fire-and-forget semantics; the bridge op `install-fake-display-media` (line 408-418) already awaits-and-responds properly. -- Update the bridge handler at lines 408-418 to await the async install: - ```typescript - if (op === 'install-fake-display-media') { + + // LAZY ASYNC PHASE — fakeGetDisplayMedia awaits readiness on first call. + // Subsequent calls observe the already-resolved Promise and proceed + // immediately. The closure is async-shaped to match the real + // getDisplayMedia signature; recorder.startRecording's + // `await navigator.mediaDevices.getDisplayMedia(...)` is fully compatible. + const fakeGetDisplayMedia = async ( + constraints?: DisplayMediaStreamOptions, + ): Promise => { + // Plan 01-14 A23: capture the production call's constraints object + // so the harness can verify `monitorTypeSurfaces: 'include'` reached + // the call site. Default to null on undefined-args so the bridge op + // reports an unambiguous "no args" signal rather than `undefined`. + lastGetDisplayMediaConstraints = constraints ?? null; + + // LAZY FIRST-FRAME WAIT (iter-2 BLOCKER 2). The Promise was kicked + // off synchronously at install time; by the time the production + // recorder.startRecording reaches this call site (after recorder.ts + // bootstrap + START_RECORDING handler dispatch), the video has had + // a head start of ~50ms+ on its decode. Most calls observe a + // resolved Promise here and proceed immediately. First call on a + // cold offscreen may block ~50-500ms while canplay fires. + if (fakeVideoReadyPromise !== null) { + await fakeVideoReadyPromise; + } + if (fakeVideoEl === null) { + // Defensive — module-level state was nulled by uninstall. + throw new Error('fake getDisplayMedia called after uninstall'); + } + return mintStream(); + }; + + // SYNC MONKEY-PATCH — the eager-install contract requires this assignment + // to happen synchronously at function call time. Cast through `unknown` + // because MediaDevices.getDisplayMedia has multiple overloads. + (navigator.mediaDevices as unknown as { + getDisplayMedia: typeof fakeGetDisplayMedia; + }).getDisplayMedia = fakeGetDisplayMedia; +} +``` + +**Sub-step 3d — Delete the canvas-specific code block (current lines ~160-199 in source: `const canvas = document.createElement('canvas')` + drawFrame + requestAnimationFrame + setInterval). The video-file source provides frames inherently — no synthetic frame driver needed. Remove all references to: `fakeCanvas`, `fakeAnimationHandle`, `fakeDrawInterval`, `drawFrame`, `frameCount`, `ctx`, `canvas.captureStream`.** + +**Sub-step 3e — Update `uninstallFakeDisplayMedia()` (current lines 271-291) to teardown the video element instead of canvas. Keep function SYNCHRONOUS (return `void`, not `Promise`):** +```typescript +export function uninstallFakeDisplayMedia(): void { + if (!fakeInstalled) { + return; + } + fakeInstalled = false; + if (fakeVideoEl !== null) { try { - await installFakeDisplayMedia(); - sendResponse({ ok: true }); - } catch (err) { - sendResponse({ - ok: false, - error: err instanceof Error ? err.message : String(err), - }); + fakeVideoEl.pause(); + } catch { + // ignore pause errors during teardown (e.g., already paused) } - return true; // signal async response (was false; required for await) + fakeVideoEl.remove(); + fakeVideoEl = null; } - ``` - Note: the listener's outer signature must allow `return true` for async ops — verify by checking neighboring ops; the listener returns false for sync, true for async. Update the wrapper to match the async contract. + // Drop the Promise reference so a subsequent install creates a fresh one. + fakeVideoReadyPromise = null; + // We deliberately do NOT restore the original getDisplayMedia — the + // offscreen document is throwaway and gets a fresh navigator on the + // next createDocument() anyway. +} +``` -**Step 4 — Update `uninstallFakeDisplayMedia()` to teardown the video element.** -- Edit `src/test-hooks/offscreen-hooks.ts` at lines 271-291. Replace canvas cleanup with video cleanup: - ```typescript - export function uninstallFakeDisplayMedia(): void { - if (!fakeInstalled) { - return; - } - fakeInstalled = false; - if (fakeVideoEl !== null) { - try { - fakeVideoEl.pause(); - } catch { - // ignore pause errors during teardown - } - fakeVideoEl.remove(); - fakeVideoEl = null; - } - // We deliberately do NOT restore the original getDisplayMedia — the - // offscreen document is throwaway and gets a fresh navigator on the - // next createDocument() anyway. +**Sub-step 3f — Verify the eager-install at lines 528-537 is UNCHANGED. The existing sync void call still works because `installFakeDisplayMedia()` remains a sync void function:** +```typescript +// UNCHANGED — preserves the eager-install contract per iter-2 BLOCKER 2 fix. +try { + installFakeDisplayMedia(); // SYNC — monkey-patch installed before recorder.ts await resolves +} catch (e) { + console.warn("[offscreen-hooks] eager installFakeDisplayMedia failed:", e); +} +``` + +**Sub-step 3g — Verify the bridge handler at lines 408-418 is UNCHANGED. The existing `try { installFakeDisplayMedia(); sendResponse({ok: true}); } ... return false;` pattern still works because the function remains sync. The `return false` (sync sendResponse signal) stays — no async dispatcher conversion needed:** +```typescript +// UNCHANGED from current source — the bridge op stays sync because +// installFakeDisplayMedia() stays sync. The async first-frame wait is +// inside fakeGetDisplayMedia, not at install time, so the bridge +// dispatcher doesn't need to await anything. +if (op === 'install-fake-display-media') { + try { + installFakeDisplayMedia(); // SYNC; returns immediately + sendResponse({ ok: true }); + } catch (err) { + sendResponse({ + ok: false, + error: err instanceof Error ? err.message : String(err), + }); } - ``` + return false; // sync sendResponse signal — unchanged +} +``` -**Step 5 — Bundle gating (decide if WAR entry needed).** -- The Vite `?url` import resolves the WebM via Vite's asset pipeline. For the offscreen document (which is loaded by `chrome.offscreen.createDocument({url: 'src/offscreen/index.html', ...})`), the `?url` import emits the asset to `dist-test/assets/.webm` and `@crxjs/vite-plugin` auto-generates a `web_accessible_resources` entry for the offscreen-reachable asset (same pattern as Plan 01-10 mokosh-mark.svg per the existing welcome.ts precedent). -- **Decision (executor verifies during Step 6 verify):** if `@crxjs/vite-plugin` auto-WARs the asset (high confidence per Plan 01-10 precedent + Plan 01-12 RESEARCH §155), NO manifest.json edit is required. If WAR auto-generation fails (verified by `grep` of the test-build dist-test/manifest.json post-build), add an explicit entry: +**Step 4 — Add the explicit web_accessible_resources entry (iter-2 BLOCKER 1 fix).** +- Edit `manifest.json`. Modify the `web_accessible_resources` array to append a second entry for the WebM assets. Final array (with the existing welcome entry preserved): ```json - { - "resources": ["assets/*.webm"], - "matches": [""] - } + "web_accessible_resources": [ + { + "resources": ["src/welcome/welcome.html"], + "matches": [""] + }, + { + "resources": ["assets/*.webm"], + "matches": [""] + } + ], ``` - The decision is captured in the SUMMARY; the BASELINE is "no manifest edit needed" per the existing analog. +- **Rationale (locked in iter-2; no executor decision):** + - Production `dist/` has zero `*.webm` assets (verified by `find dist -name '*.webm' | wc -l` returning 0 post-prod-build); the entry is **inert** in production — costs only the diff in the production manifest, costs nothing at runtime. + - Test `dist-test/` has the hashed asset at `dist-test/assets/.webm` (verified by `find dist-test -name '*.webm' | wc -l` returning ≥1 post-test-build); the entry **authorizes** `chrome-extension:///assets/.webm` URL access from the offscreen document context. + - The `@crxjs/vite-plugin` auto-WAR behavior for extracted media assets in offscreen-document context is empirically untested in this codebase (per checker iter-1 BLOCKER 1 — Plan 01-10 mokosh-mark.svg was 877 bytes and was inlined as data URI; `dist/manifest.json` shows it was NEVER auto-WAR'd because the SVG was never emitted as a file). Pre-emptive explicit entry sidesteps the question entirely. + - Threat surface: the entry exposes `assets/*.webm` URLs to all origins. Production has zero `*.webm` assets so the exposure surface is null in production. Test bundle is sandboxed in Puppeteer; no user-facing impact. -**Step 6 — Update Vite configs if necessary.** -- `vite.config.ts`: production build — verify nothing changes. The test-hooks module is tree-shaken in production per `__MOKOSH_UAT__`; the `?url` import resolves only in the test build. -- `vite.test.config.ts`: test build — verify the existing rollupOptions covers the offscreen entry (it already does per Plan 01-13). No new rollupOptions entry needed; the `?url` import is a side-effect of the offscreen-hooks.ts module being included in the test build via the existing gated dynamic import at src/offscreen/recorder.ts:46-48. +**Step 5 — Vite config verification (no edits needed; locked in iter-2).** +- `vite.config.ts`: production build — `?url` import is reachable only from `src/test-hooks/offscreen-hooks.ts`, which is gated by `__MOKOSH_UAT__` in `src/offscreen/recorder.ts:46-48` and tree-shaken to dead branch in production builds. NO config changes needed. +- `vite.test.config.ts`: test build — the offscreen entry is already present per Plan 01-13; the `?url` import resolves as a side-effect of the offscreen-hooks.ts module being included via the gated dynamic import. NO config changes needed. +- **assertion (verified by `npm run build:test` in Step 7):** `dist-test/assets/.webm` exists after test build; `dist/` has zero `*.webm` files after production build. -**Step 7 — Verify TypeScript + production build clean.** +**Step 6 — Add Tier-2 production-bundle filename leak gate (iter-2 WARNING 5 remediation).** +- Edit `tests/background/no-test-hooks-in-prod-bundle.test.ts`. After the existing Tier-1 FORBIDDEN_HOOK_STRINGS test block, add a new sub-test: + ```typescript + // Plan 04-08 iter-2 WARNING 5 — Tier-2 production-bundle filename leak canary. + // + // The test-only WebM fixture filename ('synthetic-display-source') + // appears in the TEST bundle as the resolved Vite hash URL but MUST + // NOT appear in the PRODUCTION dist/ bundle. The offscreen-hooks + // module that imports it is tree-shaken in production per + // __MOKOSH_UAT__; this gate catches any future regression that + // accidentally inlines test-hooks into the production chunk. + // + // The Tier-1 FORBIDDEN_HOOK_STRINGS inventory above tests __mokoshTest- + // family symbols; this Tier-2 gate tests the orthogonal axis of + // test-only ASSET filenames. Total inventory: + // Tier-1 (symbols): 12 entries (unchanged from Plan 01-14) + // Tier-2 (asset filenames): 1 entry (Plan 04-08 — synthetic-display-source) + test('Tier-2: synthetic-display-source filename does not leak into production dist/', () => { + // Walk dist/ files (skip the binary asset extensions in IGNORED_EXTENSIONS). + // Grep for the literal string 'synthetic-display-source'. Expected: 0 hits. + const distFiles = collectDistFiles(); // existing helper from the Tier-1 test + const offendingFiles: string[] = []; + for (const filePath of distFiles) { + const content = readFileSync(filePath, 'utf-8'); + if (content.includes('synthetic-display-source')) { + offendingFiles.push(filePath); + } + } + expect(offendingFiles).toEqual([]); + }); + ``` +- **Note**: the executor should READ the existing test file first to identify: + - The exact name of the helper that walks dist/ files (the snippet above calls it `collectDistFiles` — verify the actual name and adapt). + - The exact location to insert the new test block (after the Tier-1 inventory test, before the file's final close). + - The exact import shape (the snippet uses `readFileSync` — verify already imported). + +**Step 7 — Verify TypeScript + production build clean + test build emits asset + Tier-2 gate passes.** - `npx tsc --noEmit` exits 0. -- `npm run build` exits 0 (production build; test-hooks tree-shake verified — `grep -c 'synthetic-display-source' dist/` returns 0 across all production chunks). -- `npm run build:test` exits 0 (test build; `?url` import resolves to hashed asset in dist-test/; `grep -l 'synthetic-display-source\.webm' dist-test/` returns ≥ 1 hit pointing at the offscreen chunk). +- `npm run build` exits 0 (production build; test-hooks tree-shake verified — `find dist -name '*.webm' | wc -l` returns 0 + `grep -r "synthetic-display-source" dist/ 2>/dev/null | wc -l` returns 0). +- `npm run build:test` exits 0 (test build; ?url import resolves to hashed asset in dist-test/; `find dist-test -name '*.webm' | wc -l` returns ≥1; the chunk that contains the offscreen-hooks module includes a string match for the synthetic-display-source filename or its hashed URL). - `wc -c tests/uat/fixtures/synthetic-display-source.webm` reports ≥ 1_000_000 bytes. -- `grep -c "canvas.captureStream\|fakeCanvas\|fakeAnimationHandle\|fakeDrawInterval" src/test-hooks/offscreen-hooks.ts` returns 0 (all canvas symbols excised). -- `grep -c "videoEl.captureStream\|fakeVideoEl\|HTMLVideoElement" src/test-hooks/offscreen-hooks.ts` returns ≥ 3 (replacement symbols present). +- `grep -c "canvas.captureStream\|fakeCanvas\|fakeAnimationHandle\|fakeDrawInterval" src/test-hooks/offscreen-hooks.ts | head -1` returns 0 (all canvas symbols excised — filtered to code only via `grep -v '^//'` if needed to skip comment refs): + - Use `grep -v '^[[:space:]]*\(//\|\*\)' src/test-hooks/offscreen-hooks.ts | grep -c "canvas\.captureStream\|fakeCanvas\b\|fakeAnimationHandle\|fakeDrawInterval"` to filter out comment matches; expected count = 0. +- `grep -v '^[[:space:]]*\(//\|\*\)' src/test-hooks/offscreen-hooks.ts | grep -c "fakeVideoEl\|videoEl\.captureStream\|fakeVideoReadyPromise"` returns ≥ 3 (replacement symbols present in code, not just comments). +- `grep -c "installFakeDisplayMedia(): void" src/test-hooks/offscreen-hooks.ts` returns ≥ 1 (sync signature preserved; iter-2 BLOCKER 2 verification — function does NOT return Promise). +- `grep -c "export function installFakeDisplayMedia(): Promise" src/test-hooks/offscreen-hooks.ts` returns 0 (verifies the function was NOT converted to async). +- `grep -c "await installFakeDisplayMedia" src/test-hooks/offscreen-hooks.ts` returns 0 (verifies the bridge handler + eager-install were NOT converted to await). +- **Sub-verify gate for displaySurface monkey-patch compatibility (WARNING 2 remediation):** create a one-shot inline node script that loads dist-test (via Puppeteer's `enableExtensions`) + executes a snippet in the offscreen document that mints a stream + asserts `stream.getVideoTracks()[0].getSettings().displaySurface === 'monitor'`. If this fails, the production `displaySurface !== 'monitor'` gate at `src/offscreen/recorder.ts:294` would tear down the stream + throw `'wrong-display-surface'` — the spike would fail at Step 2 (assertA2 prime), not at Step 7 (videoSize check). Implementation: append to the existing `tests/uat/spike-a33-sw-persistence.ts` script a `--check-display-surface-only` mode that runs only Steps 1-2 (prime + 5s post-prime probe), invokes `__mokoshOffscreenQuery` op='get-display-surface', asserts result is 'monitor', exits. Run via `HEADLESS=1 SKIP_PROD_REBUILD=1 npx tsx tests/uat/spike-a33-sw-persistence.ts --check-display-surface-only 2>&1 | tee /tmp/04-08-display-surface-check.log` and grep `'DISPLAY-SURFACE-CHECK: PASSED'` in the log. The full spike re-run (Step 1 of Task 2) is a superset of this check, but running it as a fast sub-gate in Task 1 catches the WARNING 2 risk in <10s of wall-clock instead of waiting 6+ min into the spike. + - Alternative if the executor finds the spike-script-augmentation overhead too high: skip the dedicated sub-gate and rely on the spike re-run in Task 2 Step 1 (the displaySurface failure would manifest as 'wrong-display-surface' Error in the offscreen console capture; the spike's gating condition would still fail). The dedicated sub-gate is the LOW-LATENCY path; the spike re-run is the HIGH-LATENCY path; both gate the same invariant. +- **Sub-verify gate for segments invariant (advisory 2 remediation):** `grep -cE 'let segments: Blob\[\] = \[\];' src/offscreen/recorder.ts` returns 1 (architectural invariant per debug session-2; Plan 04-08 does NOT modify recorder.ts). - mkdir -p tests/uat/fixtures && test -s tests/uat/fixtures/synthetic-display-source.webm && wc -c tests/uat/fixtures/synthetic-display-source.webm | awk '$1 > 1000000 {exit 0} {exit 1}' && npx tsc --noEmit && npm run build 2>&1 | tail -5 && npm run build:test 2>&1 | tail -5 && grep -c "synthetic-display-source" dist/ -r 2>/dev/null | grep -v ":0$" | wc -l | awk '$1 == 0 {exit 0} {exit 1}' && grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts | awk '$1 == 0 {exit 0} {exit 1}' && grep -c "videoEl.captureStream\|fakeVideoEl" src/test-hooks/offscreen-hooks.ts | awk '$1 >= 2 {exit 0} {exit 1}' + mkdir -p tests/uat/fixtures && test -s tests/uat/fixtures/synthetic-display-source.webm && wc -c tests/uat/fixtures/synthetic-display-source.webm | awk '$1 > 1000000 {exit 0} {exit 1}' && npx tsc --noEmit && npm run build 2>&1 | tail -5 && npm run build:test 2>&1 | tail -5 && find dist -name '*.webm' | wc -l | awk '$1 == 0 {exit 0} {exit 1}' && find dist-test -name '*.webm' | wc -l | awk '$1 >= 1 {exit 0} {exit 1}' && grep -r "synthetic-display-source" dist/ 2>/dev/null | wc -l | awk '$1 == 0 {exit 0} {exit 1}' && grep -v '^[[:space:]]*\(//\|\*\)' src/test-hooks/offscreen-hooks.ts | grep -c "canvas\.captureStream\|fakeCanvas\b\|fakeAnimationHandle\|fakeDrawInterval" | awk '$1 == 0 {exit 0} {exit 1}' && grep -v '^[[:space:]]*\(//\|\*\)' src/test-hooks/offscreen-hooks.ts | grep -c "fakeVideoEl\|videoEl\.captureStream\|fakeVideoReadyPromise" | awk '$1 >= 3 {exit 0} {exit 1}' && grep -c "installFakeDisplayMedia(): void" src/test-hooks/offscreen-hooks.ts | awk '$1 >= 1 {exit 0} {exit 1}' && grep -c "export function installFakeDisplayMedia(): Promise" src/test-hooks/offscreen-hooks.ts | awk '$1 == 0 {exit 0} {exit 1}' && grep -c "await installFakeDisplayMedia" src/test-hooks/offscreen-hooks.ts | awk '$1 == 0 {exit 0} {exit 1}' && grep -cE 'let segments: Blob\[\] = \[\];' src/offscreen/recorder.ts | awk '$1 == 1 {exit 0} {exit 1}' && grep -c '"assets/\*\.webm"' manifest.json | awk '$1 >= 1 {exit 0} {exit 1}' && grep -c "synthetic-display-source" tests/background/no-test-hooks-in-prod-bundle.test.ts | awk '$1 >= 1 {exit 0} {exit 1}' && npm test -- tests/background/no-test-hooks-in-prod-bundle.test.ts 2>&1 | tail -10 | grep -E '(passed|PASS)' - `tests/uat/fixtures/synthetic-display-source.webm` exists, is VP9, >= 1 MB. + - `tests/fixtures/last_30sec.webm` remains in place (NOT moved). - `globals.d.ts` contains the `declare module '*.webm?url'` block. + - `manifest.json` web_accessible_resources contains an `assets/*.webm` resources entry (iter-2 BLOCKER 1 fix). - `src/test-hooks/offscreen-hooks.ts` imports `syntheticDisplaySourceUrl` via Vite `?url` suffix. - - `installFakeDisplayMedia()` is async; awaits videoEl `canplay` event + `videoEl.play()` before returning. + - **`installFakeDisplayMedia()` remains SYNCHRONOUS** (return type `: void`; no `async` keyword; iter-2 BLOCKER 2 verification — function signature preserved). + - `installFakeDisplayMedia()` body creates videoEl + appendChild + monkey-patch ASSIGNMENT all synchronously; the canplay wait + .play() are deferred INTO the `fakeGetDisplayMedia` closure. + - `fakeGetDisplayMedia` closure awaits `fakeVideoReadyPromise` on each call (first call may block ~50-500ms; subsequent calls fast-path). - `mintStream()` calls `videoEl.captureStream(30)` (NOT `canvas.captureStream`). - - `uninstallFakeDisplayMedia()` pauses + removes the video element (NOT canvas + RAF + interval cleanup). - - All 6 existing bridge ops (install-fake-display-media + dispatch-ended + has-stream + get-display-surface + get-segment-count + get-last-getDisplayMedia-constraints) remain functional; the install-fake-display-media op returns `true` (async dispatcher signal). + - `uninstallFakeDisplayMedia()` is SYNCHRONOUS (return type `: void`); pauses + removes the video element + nulls `fakeVideoReadyPromise`. + - All 6 existing bridge ops remain functional + remain in their sync `return false` form (the install-fake-display-media op stays sync — does NOT switch to `return true` because the install itself stays sync). + - The eager-install try/catch at lines 528-537 is UNCHANGED. - `npx tsc --noEmit` exits 0. - - `npm run build` exits 0; production bundle has zero references to `synthetic-display-source` OR `videoEl.captureStream` OR `fakeVideoEl` (test-hooks tree-shake verified). - - `npm run build:test` exits 0; test bundle resolves the `?url` import. - - `grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts` returns 0. + - `npm run build` exits 0; production bundle has zero `*.webm` files; zero references to `synthetic-display-source` in any dist/ file. + - `npm run build:test` exits 0; test bundle resolves the `?url` import; dist-test/assets/.webm exists. + - `grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts` (code only, comments stripped) returns 0. - The displaySurface monkey-patch (`patchDisplaySurface(stream)` call) is preserved. - The A23 `lastGetDisplayMediaConstraints` capture is preserved. - The idempotent install/uninstall contract is preserved. + - **Architectural invariant preserved:** `src/offscreen/recorder.ts:91 let segments: Blob[] = []` is UNCHANGED (advisory 2 fix; grep gate enforces). + - **Tier-2 gate (WARNING 5 fix):** `tests/background/no-test-hooks-in-prod-bundle.test.ts` contains a new test that greps dist/ for 'synthetic-display-source' and asserts 0 hits; the test PASSES. + - **Tier-1 inventory UNCHANGED at 12** (verified by line count of both inventories — Plan 04-08 introduces zero new __mokoshTest-family symbols). - Methodology fix landed; bundled WebM source replaces canvas.captureStream throttling. Atomic commit: `feat(04-08): video-file-backed MediaStream — replace canvas.captureStream invisible-source throttling per debug session-2 verdict`. + Methodology fix landed; bundled WebM source replaces canvas.captureStream throttling; eager-install contract preserved; explicit WAR entry locks in chrome-extension:// scheme access. Atomic commit: `feat(04-08): video-file MediaStream + sync-install/lazy-first-frame + explicit WAR — methodology reframe per debug session-2 + iter-2 BLOCKER fixes`. - Task 2: Re-run spike-a33-sw-persistence.ts + land A33 harness assertion + orchestrator wiring - tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts, tests/uat/spike-a33-sw-persistence.ts - tests/uat/spike-a33-sw-persistence.ts (full — re-run target), tests/uat/extension-page-harness.ts:3517-3700 (assertA30 — cs-injection-world precedent for reference; A33 does NOT use this pattern but reading clarifies the harness assertion shape), tests/uat/extension-page-harness.ts:3878-3917 (assertA31 — most-recent chrome.runtime.sendMessage SAVE_ARCHIVE pattern; copy this), tests/uat/extension-page-harness.ts:3932-4021 (__mokoshHarness global registration block — verify assertA2 is the canonical "prime fresh recording" entrypoint per Plan 04-04 REVISION iter-2 Option B), tests/uat/lib/harness-page-driver.ts:48-80 (stopServiceWorker helper — REUSED from Plan 04-04 3726eee), tests/uat/lib/harness-page-driver.ts:2039-2148 (driveA30 — host-side filter pattern for shape reference), tests/uat/lib/harness-page-driver.ts:1434-1480 (findLatestZip exported function — REUSED from Plan 04-04), tests/uat/harness.test.ts:100-110 (import block), tests/uat/harness.test.ts:340-360 (wrapped-driver block), tests/uat/harness.test.ts:459-487 (drivers-array push block; A32 is the most-recent entry), tests/uat/harness.test.ts:225-240 (SKIP_PROD_REBUILD env-gate pattern — analog for SKIP_LONG_UAT) + Task 2: Re-run spike-a33-sw-persistence.ts + land A33 harness assertion + orchestrator wiring + ROADMAP SC #1 flip + tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts, tests/uat/spike-a33-sw-persistence.ts, .planning/ROADMAP.md, .planning/STATE.md, .planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md + tests/uat/spike-a33-sw-persistence.ts (full — re-run target), tests/uat/extension-page-harness.ts:3517-3700 (assertA30 — cs-injection-world precedent for reference; A33 does NOT use this pattern but reading clarifies the harness assertion shape), tests/uat/extension-page-harness.ts:3878-3917 (assertA31 — most-recent chrome.runtime.sendMessage SAVE_ARCHIVE pattern; copy this), tests/uat/extension-page-harness.ts:3932-4021 (__mokoshHarness global registration block — verify assertA2 is the canonical "prime fresh recording" entrypoint per Plan 04-04 REVISION iter-2 Option B), tests/uat/lib/harness-page-driver.ts:48-80 (stopServiceWorker helper — REUSED from Plan 04-04 3726eee), tests/uat/lib/harness-page-driver.ts:2039-2148 (driveA30 — host-side filter pattern for shape reference), tests/uat/lib/harness-page-driver.ts:1434-1480 (findLatestZip exported function — REUSED from Plan 04-04), tests/uat/harness.test.ts:100-110 (import block), tests/uat/harness.test.ts:340-360 (wrapped-driver block), tests/uat/harness.test.ts:459-487 (drivers-array push block; A32 is the most-recent entry), tests/uat/harness.test.ts:225-240 (SKIP_PROD_REBUILD env-gate pattern — analog for SKIP_LONG_UAT), .planning/ROADMAP.md:250-262 (SC #1 row text — exact pre-edit string), .planning/phases/04-harden-clean-up-optional/04-08-CHECKER-iter-1.md (WARNING 3 + WARNING 4 — specific grep gates for spike probe values + ROADMAP edit) **Step 0 — GATING CONDITION (Task 1 complete + methodology valid).** -- Verify Task 1 acceptance criteria all met. If any fail (TS error, missing fixture, etc.), STOP and fix before proceeding. +- Verify Task 1 acceptance criteria all met. If any fail (TS error, missing fixture, sync signature violated, etc.), STOP and fix before proceeding. +- Verify the displaySurface sub-gate from Task 1 Step 7 PASSED. If skipped, the spike re-run in Step 1 below catches it but with 6+ min latency. **Step 1 — Re-run the spike script.** - Run: `HEADLESS=1 SKIP_PROD_REBUILD=0 npx tsx tests/uat/spike-a33-sw-persistence.ts 2>&1 | tee /tmp/04-08-spike-rerun.log` - Total wall-clock: ~6-7 min (5 min idle + ~1-2 min orchestration). - **Expected outcome:** `SPIKE OUTCOME: PASSED` with `videoSize > 100_000` (typical 1-3 MB). - **GATING CONDITION for landing A33:** `grep -c 'SPIKE OUTCOME: PASSED' /tmp/04-08-spike-rerun.log` returns ≥ 1 AND extracted videoSize > 100_000. +- **WARNING 3 remediation (iter-2): probe-value grep gates surfaced as explicit verify-block commands** — the spike script already emits `SPIKE PROBE [POST-PRIME]: segments.length=N`, `SPIKE PROBE [PRE-KILL]: segments.length=N`, `SPIKE PROBE [POST-KILL]: segments.length=N` lines (per debug session-2 Step B addition). The verify block below greps each value to confirm the methodology is healthy: + - `POST-PRIME=0` — no segment rotation has happened yet (the prime just established REC state). + - `PRE-KILL >= 3` — 5-min idle drove the rotation cadence to MAX_SEGMENTS (3 × 10s self-contained segments accumulated). + - `POST-KILL >= 3` — segments structurally survived the SW kill (per debug session-2 verdict: architecture is sound; this should match PRE-KILL). - If the re-run still FAILS (videoSize ≤ 100_000): STOP execution; document failure in SUMMARY; flag for further debug session. This branch is unlikely per session-2 verdict (canvas throttling is the only identified root cause; HTMLVideoElement source bypasses it definitively per MDN + Chrome bug 653548 design doc) but the spike-first contract requires explicit acknowledgement of the FAIL branch. -**Step 2 — Update the spike script to use assertA2 prime as canonical (cleanup).** -- Per debug session-2 Step B addition, the spike already uses `__mokoshHarness.assertA2` for the prime step. No edit needed — but verify the script's PROBE 1 (POST-PRIME) returns count=0 AND PROBE 2 (PRE-KILL after 5min idle) returns count=3 AND PROBE 3 (POST-KILL) returns count=3. The PRE-KILL=3 vs the previous PRE-KILL=3-but-frames=0 distinguishes "segments existed AND have frames" (PASSING methodology) from "segments existed but zero frames" (canvas throttling). The videoSize > 100_000 outcome IS the methodology success indicator. +**Step 2 — Update the spike script if needed (sync-install/lazy-first-frame compatibility).** +- The spike script's prime step uses `__mokoshHarness.assertA2` (canonical fresh-recording bootstrap; Plan 04-04 REVISION iter-2 Option B). Under the new methodology, `assertA2` triggers `recorder.startRecording` which calls `navigator.mediaDevices.getDisplayMedia` — this is now the patched closure that lazy-waits for video readiness. First call may take an extra ~50-500ms; the existing assertA2 timeout (per Plan 01-13 default ~15s) absorbs this comfortably. NO edit to the spike script's logic is needed. +- **IF** Task 1 Sub-step 3g's sub-gate (`--check-display-surface-only` mode) was implemented, that mode is preserved as a forensic helper. Otherwise the spike script is unchanged. **Step 3 — Land A33 in the harness (3-file lockstep per Plan 04-04 Wave 1 spec verbatim).** @@ -665,47 +1084,99 @@ From tests/fixtures/last_30sec.webm (existing internal project artifact — cand - **Full-mode UAT (closure gate):** `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat 2>&1 | tail -5 | tee /tmp/04-08-uat-full.log` — expect `34/34` GREEN in ~6.5 min; A33 actually runs and passes A33.1 + A33.2 + A33.3. **Step 5 — Lockstep + bundle-gate invariant verification.** -- Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 — verify by `wc -l` of both arrays returns the same count pre/post-edit: +- Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 — verify by counting code-only entries (comment-stripped) in both arrays: ```bash - # tests/uat/harness.test.ts:123-138 = 12 entries - # tests/background/no-test-hooks-in-prod-bundle.test.ts:108-... = 12 entries (matching) - grep -c "^[[:space:]]*'" tests/uat/harness.test.ts | head -1 # context-sensitive; check post-edit + # tests/uat/harness.test.ts FORBIDDEN_HOOK_STRINGS array entries (12 expected) + # tests/background/no-test-hooks-in-prod-bundle.test.ts the same array (12 expected, lockstep) ``` Confirm zero new entries added (no new __MOKOSH_UAT__-gated symbols). +- Tier-2 sub-invariant (Plan 04-08 iter-2): `npm test -- tests/background/no-test-hooks-in-prod-bundle.test.ts 2>&1 | grep -E '(passed|PASS)'` — both the Tier-1 inventory test AND the new Tier-2 filename-leak test pass. - Pre-checkpoint bundle gates (per `feedback-pre-checkpoint-bundle-gates.md`) — 6/6 PASS unchanged from Plan 04-03 baseline: - - `grep -c 'new Function' dist/assets/index-*.js` returns 0 (Plan 04-02 polarity preserved) - - `grep -c 'eval' dist/assets/index-*.js` returns 0 - - `grep -c 'Buffer\.' dist/assets/index-*.js` returns 1 (pre-existing JSZip polyfill; Plan 04-02 deferred) - - `grep -c 'window\.' dist/assets/index-*.js` returns 0 (SW chunk DOM-globals) - - `grep -c 'document\.' dist/assets/index-*.js` returns 0 + - `grep -v '^#\|^//' dist/assets/index-*.js | grep -c 'new Function'` returns 0 (Plan 04-02 polarity preserved) + - `grep -v '^#\|^//' dist/assets/index-*.js | grep -c '\beval\b'` returns 0 + - `grep -v '^#\|^//' dist/assets/index-*.js | grep -c 'Buffer\.'` returns 1 (pre-existing JSZip polyfill; Plan 04-02 deferred) + - SW chunk `window.`/`document.` references = 0 - Manifest validation + en/ru parity preserved (unchanged from Plan 04-03) -- vitest baseline preserved: `npm test 2>&1 | tail -5` — expect 183/183 GREEN (or 180/183 with the 3 documented pre-existing parallel-vitest flakes per 04-CONTEXT items 9-10; flakes pass in isolation). +- vitest baseline preserved: `npm test 2>&1 | tail -5` — expect 184/184 GREEN (was 183 baseline + 1 new Tier-2 test from Plan 04-08), or 181/184 with the 3 documented pre-existing parallel-vitest flakes per 04-CONTEXT items 9-10; flakes pass in isolation. **Step 6 — Write Plan 04-08 SUMMARY.** - Create `.planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md`: - Methodology reframe rationale (cite debug session-2 verdict verbatim) - - Bundled WebM fixture decision (copy vs regenerate; size + codec evidence) - - installFakeDisplayMedia diff summary (canvas -> HTMLVideoElement; 6 bridge ops preserved; A23 capture preserved) + - **iter-2 revision summary** (cite checker iter-1 + BLOCKER 1 + BLOCKER 2 + WARNING resolutions) + - Bundled WebM fixture decision (copy vs regenerate; size + codec evidence; dual-location note) + - installFakeDisplayMedia diff summary (canvas -> HTMLVideoElement; SYNC install + LAZY first-frame; 6 bridge ops preserved sync; A23 capture preserved; eager-install contract preserved) + - manifest.json WAR entry decision (BLOCKER 1 rationale: pre-emptive explicit entry; production inert; test bundle authorized) + - displaySurface compatibility evidence (sub-gate result; WARNING 2 closure) - Spike re-run evidence (videoSize before/after; segment-count probe values; elapsed time) - - A33 land evidence (driveA33 + orchestrator wiring; FORBIDDEN_HOOK_STRINGS at 12; pre-checkpoint gates 6/6) + - A33 land evidence (driveA33 + orchestrator wiring; FORBIDDEN_HOOK_STRINGS at 12; pre-checkpoint gates 6/6; Tier-2 gate added) - UAT before/after (33/33 -> 34/34 GREEN) - ROADMAP SC #1 closure with commit ref - Plan 04-04 SUMMARY post-debug amendment cross-reference - - Persisting artifacts (stopServiceWorker + spike script + canonical methodology pattern) - - Architectural integrity statement (offscreen-RAM segments: Blob[] is UNCHANGED and canonically correct) + - Persisting artifacts (stopServiceWorker + spike script + canonical methodology pattern + Tier-2 invariant) + - Architectural integrity statement (offscreen-RAM segments: Blob[] is UNCHANGED and canonically correct; grep gate enforces) -**Step 7 — Update STATE.md + ROADMAP.md markers (Phase 4 partial closure).** -- STATE.md: append to "Decisions" section: `[Phase 04-08]: Methodology reframe complete — video-file MediaStream replaces canvas.captureStream throttling per debug session-2 verdict; A33 harness assertion landed; UAT 33 -> 34 GREEN; ROADMAP SC #1 (SW state persistence) CLOSED.` -- ROADMAP.md: flip Phase 4 SC #1 row from "STATUS 2026-05-21: OPEN" to "STATUS 2026-05-22: CLOSED via Plan 04-08 — see .planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md". Add Plan 04-08 row to the Plans list with brief objective. +**Step 7 — Update STATE.md + ROADMAP.md markers (Phase 4 partial closure) — WARNING 4 remediation: exact edit specified + grep gate.** -**Step 8 — Atomic commit.** -- Single atomic commit with all Task 2 + Task 3 artifacts: `feat(04-08): A33 SW state persistence harness assertion — methodology reframe (34/34 GREEN; ROADMAP SC #1 CLOSED)`. +**STATE.md edit:** +- Append to "Decisions" section: `[Phase 04-08]: Methodology reframe complete — video-file MediaStream replaces canvas.captureStream throttling per debug session-2 verdict; A33 harness assertion landed; UAT 33 -> 34 GREEN; ROADMAP SC #1 (SW state persistence) CLOSED 2026-05-22.` + +**ROADMAP.md edit (exact pre-edit string + replacement; WARNING 4 fix):** +- Open `.planning/ROADMAP.md`. Locate the SC #1 prose at lines 250-262 (the existing OPEN status block). +- The current text (verified during planning) contains: + ``` + **STATUS 2026-05-21: OPEN.** Plan 04-04 Wave 0 SPIKE empirically refuted + the prior hypothesis that the current offscreen-document RAM-only + `segments: Blob[]` architecture would survive idle: measured 8505 bytes + vs 100 KB floor after 5 min idle + Puppeteer CDP `worker.close()`. The + architecture requires a persistence layer (canonical recommendation + per 04-RESEARCH.md Q2 sub-question b Option C: IndexedDB persistence + in offscreen). Plan-fix ceremony queued ahead of Plans 04-05/04-06/ + 04-07. Reproducible verification gate: tests/uat/spike-a33-sw-persistence.ts. + ``` +- Replace the entire block (from `**STATUS 2026-05-21: OPEN.**` through the line ending with `spike-a33-sw-persistence.ts.`) with: + ``` + **STATUS 2026-05-22: CLOSED via Plan 04-08 — see .planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md.** + The prior Plan 04-04 SPIKE FAILED outcome (8505 bytes; 2026-05-21) was + empirically REFUTED by debug session-2 (commit `4ea1bbb`): the + offscreen-RAM `segments: Blob[]` architecture is sound (POST-KILL probe + count=3 confirms structural persistence); the failure was test + methodology (canvas.captureStream invisible-source throttling per + Chrome bug 653548). Plan 04-08 replaced the canvas source with + HTMLVideoElement.captureStream backed by a bundled WebM (preserving + eager-install contract via SYNC-install + LAZY first-frame pattern); + spike re-run produces videoSize > 100_000 (typical 1-3 MB); A33 + harness assertion lands per Plan 04-04 Pattern 4 verbatim under + SKIP_LONG_UAT env-gate. Reproducible verification gate: + tests/uat/spike-a33-sw-persistence.ts (now PASSES under valid + methodology). + ``` +- Update the Plans list block at line ~277 — find `04-04-PLAN.md` entry and append a sibling line after it for Plan 04-08: + ``` + - [x] 04-08-PLAN.md — A33 methodology reframe + harness assertion: **CLOSED 2026-05-22** via debug session-2 verdict (canvas-captureStream invisible-source throttling root cause); HTMLVideoElement.captureStream replaces canvas.captureStream in installFakeDisplayMedia() with SYNC install + LAZY first-frame contract; spike re-run produces videoSize > 100_000 (vs 8505 baseline); A33 lands per original Plan 04-04 Wave 1 spec under SKIP_LONG_UAT env-gate; UAT 33 -> 34 GREEN. ROADMAP SC #1 CLOSED. + ``` +- Update the phase tracker table at line ~292: find the row for Phase 4 and update completion count + status note: + ``` + | 4. Harden + clean up (optional) | 5/8 | In Progress (Plan 04-08 closed ROADMAP SC #1 via methodology reframe; Plans 04-05/04-06/04-07 remain for build hygiene + visual polish + closure aggregator) | | + ``` +- **WARNING 4 grep gate (post-edit verify):** + ```bash + grep -c 'CLOSED via Plan 04-08' .planning/ROADMAP.md # MUST return >= 1 + grep -c 'STATUS 2026-05-21: OPEN' .planning/ROADMAP.md # MUST return 0 (the old status removed) + grep -c 'STATUS 2026-05-22: CLOSED' .planning/ROADMAP.md # MUST return >= 1 + ``` + +**Step 8 — Atomic commit (advisory 1 — corrected commit message; Task 1 + Task 2 only, no Task 3).** +- Single atomic commit for Task 2 with all Task 2 artifacts + SUMMARY + STATE/ROADMAP markers: `feat(04-08): A33 SW state persistence harness assertion — methodology reframe (34/34 GREEN; ROADMAP SC #1 CLOSED)`. +- Task 1 was committed separately at the close of Task 1 per its own done line: `feat(04-08): video-file MediaStream + sync-install/lazy-first-frame + explicit WAR — methodology reframe per debug session-2 + iter-2 BLOCKER fixes`. - npx tsc --noEmit && npm run build:test && HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat 2>&1 | tail -5 | tee /tmp/04-08-uat-skip.log; grep -c '34/34\|34 passed' /tmp/04-08-uat-skip.log | awk '$1 >= 1 {exit 0} {exit 1}' && grep -c 'driveA33' tests/uat/harness.test.ts | awk '$1 >= 3 {exit 0} {exit 1}' && grep -c 'SKIP_LONG_UAT' tests/uat/harness.test.ts | awk '$1 >= 2 {exit 0} {exit 1}' && grep -c 'dispatchSaveArchive' tests/uat/lib/harness-page-driver.ts tests/uat/extension-page-harness.ts tests/uat/harness.test.ts 2>/dev/null | grep -v ":0$" | wc -l | awk '$1 == 0 {exit 0} {exit 1}' + npx tsc --noEmit && npm run build:test && HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat 2>&1 | tail -5 | tee /tmp/04-08-uat-skip.log; grep -c '34/34\|34 passed' /tmp/04-08-uat-skip.log | awk '$1 >= 1 {exit 0} {exit 1}' && grep -c 'driveA33' tests/uat/harness.test.ts | awk '$1 >= 3 {exit 0} {exit 1}' && grep -c 'SKIP_LONG_UAT' tests/uat/harness.test.ts | awk '$1 >= 2 {exit 0} {exit 1}' && grep -c 'dispatchSaveArchive' tests/uat/lib/harness-page-driver.ts tests/uat/extension-page-harness.ts tests/uat/harness.test.ts 2>/dev/null | grep -v ":0$" | wc -l | awk '$1 == 0 {exit 0} {exit 1}' && grep -cE 'SPIKE PROBE \[POST-PRIME\]: segments\.length=0' /tmp/04-08-spike-rerun.log 2>/dev/null | awk '$1 >= 1 {exit 0} {exit 1}' && grep -cE 'SPIKE PROBE \[PRE-KILL\]: segments\.length=([3-9]|[1-9][0-9]+)' /tmp/04-08-spike-rerun.log 2>/dev/null | awk '$1 >= 1 {exit 0} {exit 1}' && grep -cE 'SPIKE PROBE \[POST-KILL\]: segments\.length=([3-9]|[1-9][0-9]+)' /tmp/04-08-spike-rerun.log 2>/dev/null | awk '$1 >= 1 {exit 0} {exit 1}' && grep -c 'SPIKE OUTCOME: PASSED' /tmp/04-08-spike-rerun.log 2>/dev/null | awk '$1 >= 1 {exit 0} {exit 1}' && grep -c 'CLOSED via Plan 04-08' .planning/ROADMAP.md | awk '$1 >= 1 {exit 0} {exit 1}' && grep -c 'STATUS 2026-05-21: OPEN' .planning/ROADMAP.md | awk '$1 == 0 {exit 0} {exit 1}' - Spike re-run output contains `SPIKE OUTCOME: PASSED` AND `videoSize > 100_000`. (If FAILED branch fires, STOP and document; A33 land is BLOCKED.) + - **WARNING 3 (iter-2):** Spike log contains `SPIKE PROBE [POST-PRIME]: segments.length=0` (post-prime baseline; no rotations yet). + - **WARNING 3 (iter-2):** Spike log contains `SPIKE PROBE [PRE-KILL]: segments.length=X` where X ≥ 3 (5-min idle drove rotation cadence to MAX_SEGMENTS). + - **WARNING 3 (iter-2):** Spike log contains `SPIKE PROBE [POST-KILL]: segments.length=X` where X ≥ 3 (architecture preserved across SW kill per debug session-2). - `npx tsc --noEmit` exits 0. - `npm run build:test` exits 0. - `tests/uat/lib/harness-page-driver.ts` contains `driveA33` function with the 4-arg signature `(page, browser, extensionId, downloadsDir) => Promise`. @@ -717,11 +1188,12 @@ From tests/fixtures/last_30sec.webm (existing internal project artifact — cand - Full-mode UAT: `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` reports 34/34 GREEN in ~6.5 min; A33.1 + A33.2 + A33.3 all PASS. - `dispatchSaveArchive` symbol is NOT introduced anywhere — `grep -c 'dispatchSaveArchive' tests/uat/` returns 0. - FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries — verify by line count of both arrays. + - **Tier-2 gate (Plan 04-08 iter-2):** `grep -c 'synthetic-display-source' tests/background/no-test-hooks-in-prod-bundle.test.ts` returns ≥ 1; the test PASSES under `npm test -- tests/background/no-test-hooks-in-prod-bundle.test.ts`. - Pre-checkpoint bundle gates 6/6 PASS unchanged from Plan 04-03 baseline. - - vitest 183 baseline preserved (or 180/183 with the 3 documented pre-existing flakes; pass in isolation). - - `.planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md` exists with: methodology rationale + spike re-run evidence + A33 land evidence + UAT before/after + ROADMAP SC #1 closure + cross-reference to Plan 04-04 post-debug amendment. + - vitest 184 baseline preserved (183 prior + 1 new Tier-2 test) — or 181/184 with the 3 documented pre-existing flakes; pass in isolation. + - **WARNING 4 (iter-2):** `.planning/ROADMAP.md` contains `'CLOSED via Plan 04-08'` (grep returns ≥ 1); contains zero hits on `'STATUS 2026-05-21: OPEN'` (the old status was removed); contains `'STATUS 2026-05-22: CLOSED'`. - `.planning/STATE.md` Decisions list contains a Plan 04-08 entry citing ROADMAP SC #1 CLOSED. - - `.planning/ROADMAP.md` Phase 4 SC #1 row flipped from OPEN to CLOSED with date + Plan 04-08 cite. + - `.planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md` exists with: methodology rationale + iter-2 revision summary + spike re-run evidence + probe values + A33 land evidence + UAT before/after + ROADMAP SC #1 closure + cross-reference to Plan 04-04 post-debug amendment. A33 lands; UAT 33->34 GREEN; ROADMAP SC #1 CLOSED. Atomic commit: `feat(04-08): A33 SW state persistence harness assertion — methodology reframe (34/34 GREEN; ROADMAP SC #1 CLOSED)`. @@ -733,72 +1205,104 @@ From tests/fixtures/last_30sec.webm (existing internal project artifact — cand | Boundary | Description | |----------|-------------| -| Bundled WebM fixture -> offscreen test bundle | the `tests/uat/fixtures/synthetic-display-source.webm` is project-owned (CC0-equivalent internal capture) and loaded via Vite's `?url` asset pipeline only in test builds. Production builds tree-shake the entire offscreen-hooks module per `__MOKOSH_UAT__`; the fixture has zero attack surface in production. | +| Bundled WebM fixture -> offscreen test bundle | the `tests/uat/fixtures/synthetic-display-source.webm` is project-owned (CC0-equivalent internal capture) and loaded via Vite's `?url` asset pipeline only in test builds. Production builds tree-shake the entire offscreen-hooks module per `__MOKOSH_UAT__`; the fixture has zero attack surface in production (manifest.json WAR entry for assets/*.webm is inert because production dist/ has zero *.webm files). | | HTMLVideoElement.captureStream -> MediaRecorder | the video element source is bundled at build time (not network-fetched); no remote-origin attack vector. The captured stream feeds the production MediaRecorder code path verbatim — same boundary as canvas.captureStream had. | | Puppeteer CDP -> Chrome MV3 SW | unchanged from Plan 04-04 (stopServiceWorker helper reused verbatim). | +| manifest.json web_accessible_resources -> chrome-extension:// URL access | new explicit entry for `assets/*.webm` extends the existing `src/welcome/welcome.html` WAR pattern. Matches ``; in production this is null-surface (no *.webm files); in test this authorizes the bundled fixture. | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| -| T-04-08-01 | Tampering | future change to `installFakeDisplayMedia()` reintroduces canvas.captureStream silently | mitigate | `grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts` invariant = 0 codified in Task 1 verify. A33 acts as a regression-catching gate; if a future PR moves source back to canvas, the spike fails fast (videoSize=8505). | +| T-04-08-01 | Tampering | future change to `installFakeDisplayMedia()` reintroduces canvas.captureStream silently | mitigate | `grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts` (code-only, comment-filtered) invariant = 0 codified in Task 1 verify. A33 acts as a regression-catching gate; if a future PR moves source back to canvas, the spike fails fast (videoSize=8505). | | T-04-08-02 | Tampering | bundled `synthetic-display-source.webm` is renamed/deleted/replaced with a tiny or invalid WebM | mitigate | Task 1 verify checks `wc -c` >= 1_000_000 bytes; Task 2 spike re-run catches any content-level corruption (videoSize-floor gate). | | T-04-08-03 | Information disclosure | the bundled WebM contains operator content (it's a project-internal capture of the developer's screen) | accept | The existing `tests/fixtures/last_30sec.webm` has been in the repo since 2026-05-15 (Plan 01-07 closure); the new copy at `tests/uat/fixtures/synthetic-display-source.webm` is the same project-owned artifact. Tradeoff: rebuilding a fresh WebM from `ffmpeg -f lavfi -i testsrc` is OPTIONAL per Task 1 Step 1. If the developer prefers content-free fixtures, the regeneration path is the documented escape hatch. | | T-04-08-04 | DoS (test runtime) | A33's 5-min idle adds ~5 min to harness wall-clock (95s -> 395s); per-commit CI lanes would suffer | mitigate | Env-gated by `SKIP_LONG_UAT=1` (default RUN for closure + alpha; documented per-commit fast-skip path). Same gate Plan 04-04 specified. | -| T-04-08-05 | Repudiation | HTMLVideoElement.captureStream is non-standard (not in lib.dom.d.ts at all TS versions); a TS strict-mode bump could break the cast-through-unknown idiom | accept | The cast is pinned in the code at Task 1 Step 3; project's TS pin is documented; if a TS upgrade ever breaks the cast, the build-test gate (Task 1 verify) catches it. The runtime surface itself (HTMLMediaElement.captureStream) is implemented in Chrome since 2017 and stable per MDN. | +| T-04-08-05 | Repudiation | HTMLVideoElement.captureStream is non-standard (not in lib.dom.d.ts at all TS versions); a TS strict-mode bump could break the cast-through-intersection idiom | accept | The cast is pinned in the code at Task 1 Step 3; project's TS pin is documented; if a TS upgrade ever breaks the cast, the build-test gate (Task 1 verify) catches it. The runtime surface itself (HTMLMediaElement.captureStream) is implemented in Chrome since 2017 and stable per MDN. | | T-04-08-06 | Spoofing | a malicious replacement `synthetic-display-source.webm` could inject malicious VP9 codec data | accept | the fixture is committed to git; any tamper is visible via `git diff`. Plan 04-08's threat surface is test-only and the test build runs in a sandboxed Puppeteer browser instance. No production user is exposed. | +| T-04-08-07 | Elevation of Privilege | manifest.json's new web_accessible_resources entry for `assets/*.webm` could expose unintended content if a future plan accidentally bundles a sensitive WebM into production dist/ | mitigate | Production dist/ has zero *.webm assets (verified by Task 1 Step 7 grep gate: `find dist -name '*.webm' | wc -l == 0`); Tier-2 filename-leak gate at tests/background/no-test-hooks-in-prod-bundle.test.ts catches any regression. The threat is constructed (no current attack surface) but the gate makes it self-extinguishing. | +| T-04-08-08 | Tampering | future refactor converts `installFakeDisplayMedia()` to async (breaking the eager-install contract — recurrence of iter-2 BLOCKER 2) | mitigate | Grep gate codified in Task 1 verify: `grep -c "export function installFakeDisplayMedia(): Promise" src/test-hooks/offscreen-hooks.ts == 0` AND `grep -c "installFakeDisplayMedia(): void" >= 1`. Any future PR that flips the signature trips the build-test pipeline. Threat is high-impact (silent re-introduction of the 8505-byte race) but the gate is deterministic. | +| T-04-08-09 | Tampering | future refactor removes the explicit `assets/*.webm` WAR entry (breaking the chrome-extension:// access path for the bundled fixture) | mitigate | Task 1 verify gate: `grep -c '"assets/\*\.webm"' manifest.json >= 1`. The fixture would still load via Vite hash URL in test build but the chrome-extension:// scheme access would fail; the spike re-run would catch the regression. | - `npx tsc --noEmit` exits 0. - `npm run build` exits 0 (production; test-hooks tree-shake verified). -- `npm run build:test` exits 0 (test; ?url import resolves). +- `npm run build:test` exits 0 (test; ?url import resolves; dist-test/assets/.webm emitted). - `wc -c tests/uat/fixtures/synthetic-display-source.webm` reports >= 1_000_000 bytes; codec=vp9 per ffprobe. -- `grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts` returns 0. -- `grep -c "videoEl.captureStream\|fakeVideoEl" src/test-hooks/offscreen-hooks.ts` returns >= 2. -- `grep -c "synthetic-display-source" dist/ -r` returns 0 (production tree-shake). +- `find dist -name '*.webm' | wc -l` returns 0 (production has zero WebM assets — WAR entry is inert there). +- `find dist-test -name '*.webm' | wc -l` returns ≥ 1 (test bundle emits the asset). +- **Code-only grep gates (filtering comments via `grep -v '^[[:space:]]*\(//\|\*\)'`):** + - `canvas.captureStream` references: 0 + - `fakeVideoEl|videoEl.captureStream|fakeVideoReadyPromise` references: ≥ 3 +- `grep -c "installFakeDisplayMedia(): void" src/test-hooks/offscreen-hooks.ts` returns ≥ 1 (sync signature preserved). +- `grep -c "export function installFakeDisplayMedia(): Promise" src/test-hooks/offscreen-hooks.ts` returns 0 (NOT async; iter-2 BLOCKER 2 fix verified). +- `grep -c "await installFakeDisplayMedia" src/test-hooks/offscreen-hooks.ts` returns 0 (eager-install + bridge handler preserve sync calls). +- `grep -c '"assets/\*\.webm"' manifest.json` returns ≥ 1 (iter-2 BLOCKER 1 fix verified). +- `grep -c "synthetic-display-source" tests/background/no-test-hooks-in-prod-bundle.test.ts` returns ≥ 1 (Tier-2 gate present; WARNING 5). +- `grep -r "synthetic-display-source" dist/ 2>/dev/null | wc -l` returns 0 (production tree-shake). +- `grep -cE 'let segments: Blob\[\] = \[\];' src/offscreen/recorder.ts` returns 1 (advisory 2; architecture invariant). - Spike re-run: `HEADLESS=1 npx tsx tests/uat/spike-a33-sw-persistence.ts` exits 0 with `videoSize > 100_000` (typical 1-3 MB). +- **WARNING 3 (iter-2):** spike log contains: + - `SPIKE PROBE [POST-PRIME]: segments.length=0` + - `SPIKE PROBE [PRE-KILL]: segments.length=X` where X ≥ 3 + - `SPIKE PROBE [POST-KILL]: segments.length=X` where X ≥ 3 + - `SPIKE OUTCOME: PASSED` - UAT harness count 33 -> 34. - Skip-mode UAT: `HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat` GREEN 34/34 in ~95s. - Full-mode UAT: `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` GREEN 34/34 in ~6.5 min. -- ROADMAP SC #1 row flipped from OPEN to CLOSED (Plan 04-08 cite + 2026-05-22 date). +- **WARNING 4 (iter-2):** ROADMAP edits: + - `grep -c 'CLOSED via Plan 04-08' .planning/ROADMAP.md` returns ≥ 1 + - `grep -c 'STATUS 2026-05-21: OPEN' .planning/ROADMAP.md` returns 0 (old status removed) + - `grep -c 'STATUS 2026-05-22: CLOSED' .planning/ROADMAP.md` returns ≥ 1 - Tier-1 FORBIDDEN_HOOK_STRINGS lockstep at 12 entries (unchanged). +- Tier-2 gate (Plan 04-08): `npm test -- tests/background/no-test-hooks-in-prod-bundle.test.ts` passes. - Pre-checkpoint bundle gates 6/6 PASS (new Function=0 + eval=0 + Buffer.=1 + window.=0 + document.=0 in SW + Tier-1=12 + en/ru parity). -- vitest baseline 183 preserved (or 180/183 with 3 documented pre-existing flakes; pass in isolation). +- vitest baseline 184 preserved (183 prior + 1 new Tier-2; or 181/184 with 3 documented pre-existing flakes; pass in isolation). - A29 + A30 + A31 + A32 unchanged (no regression to existing assertions). - Architecture invariant preserved: `src/offscreen/recorder.ts:91 let segments: Blob[] = []` UNCHANGED — debug session-2 verdict is honored (NO IndexedDB persistence work). - spike-FAILED forensic-evidence preservation: `tests/uat/spike-a33-sw-persistence.ts` retained (Plan 04-04 commit 3726eee + debug session-2 Step B + C additions); now functions as a PASSING regression test under the new methodology. - `dispatchSaveArchive` symbol NOT introduced anywhere — `grep -c` across spike script + harness files returns 0. +- **iter-2 BLOCKER 1 verify:** WAR entry pre-decided AND the @crxjs/vite-plugin auto-WAR question is sidestepped (no executor improvisation needed). +- **iter-2 BLOCKER 2 verify:** eager-install contract preserved — sync monkey-patch in `installFakeDisplayMedia()`; lazy first-frame inside `fakeGetDisplayMedia` closure; recorder.ts:46-48 await chain race window eliminated. - Bundled WebM fixture lands at `tests/uat/fixtures/synthetic-display-source.webm` (>=1 MB VP9; project-owned). -- `installFakeDisplayMedia()` rewritten — canvas.captureStream replaced by HTMLVideoElement.captureStream; displaySurface monkey-patch + A23 capture + idempotency contract preserved. +- `installFakeDisplayMedia()` rewritten — canvas.captureStream replaced by HTMLVideoElement.captureStream; **function signature remains SYNCHRONOUS** (iter-2 BLOCKER 2); displaySurface monkey-patch + A23 capture + idempotency contract preserved. +- `fakeGetDisplayMedia` closure performs the lazy first-frame wait — first call awaits `fakeVideoReadyPromise`; subsequent calls observe resolved Promise + proceed immediately. - Ambient module decl for `*.webm?url` added to `globals.d.ts`. +- Explicit web_accessible_resources entry for `assets/*.webm` added to `manifest.json` (iter-2 BLOCKER 1). - Spike re-run produces `videoSize > 100_000` (typical 1-3 MB) — methodology reframe empirically validated. +- Spike probe values confirm methodology health: POST-PRIME=0, PRE-KILL≥3, POST-KILL≥3 (WARNING 3). - A33 harness assertion lands per Plan 04-04 Pattern 4 verbatim: driveA33 + driveA33Wrapped + orchestrator entry + SKIP_LONG_UAT env-gate. - UAT harness count 33 -> 34 GREEN (skip-mode in ~95s; full-mode in ~6.5 min). -- ROADMAP SC #1 (SW state persistence) flipped CLOSED in ROADMAP.md. +- ROADMAP SC #1 (SW state persistence) flipped CLOSED in ROADMAP.md with exact pre-edit/post-edit grep gates verified (WARNING 4). - Plan 04-04 SUMMARY post-debug amendment cross-referenced in Plan 04-08 SUMMARY. - Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries. +- Tier-2 production-bundle filename-leak gate added: `synthetic-display-source` returns 0 hits in dist/ (WARNING 5). - Pre-checkpoint bundle gates 6/6 PASS. -- Architectural integrity preserved (no IndexedDB persistence; no chrome.storage migration; src/offscreen/recorder.ts:91 segments array unchanged). -- vitest 183 baseline preserved. +- Architectural integrity preserved (no IndexedDB persistence; no chrome.storage migration; src/offscreen/recorder.ts:91 segments array unchanged; grep gate enforces). +- vitest 184 baseline preserved (183 prior + 1 new Tier-2 test). - Plan 04-04 forensic artifacts (`stopServiceWorker` helper + spike script) repurposed as PASSING regression tests under valid methodology. +- iter-2 BLOCKER 1 + BLOCKER 2 + WARNINGs 1-5 + advisories 1-3 all addressed per checker iter-1 remediation guidance. After completion, create `.planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md` capturing: - **Methodology reframe rationale** — verbatim cite of debug session-2 verdict (REFUTED-architecture; canvas-captureStream issue); explicit rejection of the previously-proposed IndexedDB persistence plan-fix with the empirical reasoning (the spike would STILL produce 8505 bytes after IDB lands because the failure is in the fake stream, not in segment persistence). -- **Bundled WebM fixture decision** — copy from `tests/fixtures/last_30sec.webm` vs regenerate via ffmpeg; size + codec + duration evidence; license/provenance attestation (CC0-equivalent project-owned internal capture). -- **installFakeDisplayMedia diff summary** — before (canvas.captureStream + RAF + setInterval) vs after (HTMLVideoElement + canplay event + .play() + captureStream); preserved invariants (displaySurface monkey-patch, A23 capture, idempotency, 6 bridge ops). -- **Spike re-run evidence** — videoSize before (8505) vs after (>100_000; typical 1-3 MB); segment-count probe values (POST-PRIME=0, PRE-KILL=3, POST-KILL=3); elapsed time (~308s baseline preserved); SPIKE OUTCOME: PASSED. -- **A33 land evidence** — driveA33 (4-arg signature); 3-file lockstep diff (extension-page-harness.ts unchanged per Option B; harness-page-driver.ts +driveA33; harness.test.ts +import +wrapped +array push); FORBIDDEN_HOOK_STRINGS at 12 entries (unchanged); pre-checkpoint gates 6/6 PASS. +- **iter-2 revision summary** — cite checker iter-1 BLOCKER 1 (Vite asset-emission path) + BLOCKER 2 (eager-install contract) resolutions; explain SYNC install + LAZY first-frame contract; document the explicit WAR entry decision (production inert / test authorized); enumerate WARNING resolutions (autoplay fallback; displaySurface compat sub-gate; spike probe-value grep gates; ROADMAP edit pre-spec; Tier-2 filename-leak gate). +- **Bundled WebM fixture decision** — copy from `tests/fixtures/last_30sec.webm` vs regenerate via ffmpeg; size + codec + duration evidence; license/provenance attestation (CC0-equivalent project-owned internal capture); dual-location note (original fixture remains in place at tests/fixtures/). +- **installFakeDisplayMedia diff summary** — before (canvas.captureStream + RAF + setInterval; sync but throttled) vs after (HTMLVideoElement + sync install + lazy first-frame closure; sync signature + async first-call wait); preserved invariants (displaySurface monkey-patch, A23 capture, idempotency, 6 bridge ops all sync); explicit verification that the function did NOT become async. +- **manifest.json WAR entry decision** — iter-2 BLOCKER 1 rationale; production inert + test authorized; alternative options considered (Option A probe-then-decide; Option C blob URL from ?raw); chosen path = explicit pre-emptive entry. +- **displaySurface compatibility evidence** — WARNING 2 sub-gate result OR spike re-run high-latency catch evidence. +- **Spike re-run evidence** — videoSize before (8505) vs after (>100_000; typical 1-3 MB); segment-count probe values (POST-PRIME=0, PRE-KILL≥3, POST-KILL≥3); elapsed time (~308s baseline preserved); SPIKE OUTCOME: PASSED. +- **A33 land evidence** — driveA33 (4-arg signature); 3-file lockstep diff (extension-page-harness.ts unchanged per Option B; harness-page-driver.ts +driveA33; harness.test.ts +import +wrapped +array push); FORBIDDEN_HOOK_STRINGS at 12 entries (unchanged); Tier-2 gate added; pre-checkpoint gates 6/6 PASS. - **UAT before/after** — 33/33 -> 34/34 GREEN; skip-mode wall-clock ~95s; full-mode wall-clock ~6.5 min. -- **ROADMAP SC #1 closure** — flipped OPEN -> CLOSED with Plan 04-08 cite + 2026-05-22 date. +- **ROADMAP SC #1 closure** — flipped OPEN -> CLOSED with Plan 04-08 cite + 2026-05-22 date; exact ROADMAP.md edit pre/post snapshots; grep gate verifications. - **Plan 04-04 cross-reference** — debug session-2 verdict honored; spike-FAILED forensic artifacts (`stopServiceWorker` helper + `spike-a33-sw-persistence.ts`) repurposed as PASSING regression tests. -- **Architectural integrity statement** — `let segments: Blob[] = []` at src/offscreen/recorder.ts:91 is UNCHANGED and canonically correct per debug session-2 segment-count probe evidence. -- **Commit refs** — Task 1 atomic commit (methodology fix) + Task 2 atomic commit (A33 land + SUMMARY + STATE/ROADMAP markers). +- **Architectural integrity statement** — `let segments: Blob[] = []` at src/offscreen/recorder.ts:91 is UNCHANGED and canonically correct per debug session-2 segment-count probe evidence; grep gate enforces. +- **Commit refs** — Task 1 atomic commit (methodology fix + WAR entry + Tier-2 gate) + Task 2 atomic commit (A33 land + SUMMARY + STATE/ROADMAP markers). - **Next plan handoff** — Plan 04-07 (closure aggregator) can now reference ROADMAP SC #1 as GREEN; v1 milestone close prep unblocked from the SW persistence side. - + \ No newline at end of file