feat(04-08): video-file MediaStream + sync-install/lazy-first-frame + explicit WAR — methodology reframe per debug session-2 + iter-2 BLOCKER fixes
Task 1 of Plan 04-08 (methodology reframe of ROADMAP SC #1): - Bundle 1.9 MB VP9 WebM fixture at tests/uat/fixtures/synthetic-display-source.webm (copy of internal Plan 01-07 fixture; CC0-equivalent project-owned) - Add globals.d.ts ambient `*.webm?url` module decl (mirrors Plan 01-10 `*.svg?url`) - Add manifest.json web_accessible_resources entry for `assets/*.webm` (iter-2 BLOCKER 1 — pre-decided to avoid executor improvisation; inert in production where dist/ has zero *.webm) - Rewrite installFakeDisplayMedia() at src/test-hooks/offscreen-hooks.ts: * Replace canvas.captureStream(30) with HTMLVideoElement.captureStream(30) — bypasses Chrome bug 653548 invisible-canvas throttling (debug session-2 root cause) * Function signature remains SYNCHRONOUS (`: void`; iter-2 BLOCKER 2 — eager-install contract preserved at lines 528-537) * Video element creation + DOM append + monkey-patch assignment execute synchronously * canplay wait + .play() deferred INTO fakeGetDisplayMedia closure (lazy first-frame pattern) * fakeVideoReadyPromise kicked off at install time so first call observes resolved Promise * WARNING 1 (autoplay reject): explicit error class identifier 'autoplay-blocked or codec-unsupported in headless context' * displaySurface monkey-patch preserved verbatim * A23 lastGetDisplayMediaConstraints capture preserved * uninstallFakeDisplayMedia teardown adapted for videoEl (pauses + removes + nulls) * All 6 bridge ops UNCHANGED in their sync return-false form - Add Tier-2 production-bundle filename-leak gate at tests/background/no-test-hooks-in-prod-bundle.test.ts (iter-2 WARNING 5 — synthetic-display-source string must be 0 hits in dist/) Verification: - npx tsc --noEmit: exit 0 - npm run build: dist/ produced; 0 *.webm files; 0 synthetic-display-source hits - npm run build:test: dist-test/assets/synthetic-display-source-mbtR1t3u.webm emitted (1.9 MB; Vite ?url asset) - Code-only grep (comment-filtered) on offscreen-hooks.ts: 0 canvas refs; 15 video refs - installFakeDisplayMedia signature unchanged: `: void` 2x; `: Promise` 0x; `await installFakeDisplayMedia` 0x - Architectural invariant unchanged: `let segments: Blob[] = []` at recorder.ts:91 (1 hit; grep gate enforces) - Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12 entries - Tier-2 vitest gate PASSES: 14/14 GREEN under SKIP_BUILD=1 (12 Tier-1 + 1 build verify + 1 Tier-2) Per iter-3 checker advisory 1: the wrong-display-surface throw lives at recorder.ts:313-321 (not line 294 as plan text states; off by ~25 lines but unambiguous). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
globals.d.ts
vendored
13
globals.d.ts
vendored
@@ -35,3 +35,16 @@ declare module '*.svg?url' {
|
|||||||
const url: string;
|
const url: string;
|
||||||
export default url;
|
export default url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plan 04-08 — Vite `?url` import for bundled test-only WebM fixture
|
||||||
|
// (tests/uat/fixtures/synthetic-display-source.webm). Mirrors the
|
||||||
|
// Plan 01-10 mokosh-mark.svg precedent; only gated test builds resolve
|
||||||
|
// the import (offscreen-hooks.ts is tree-shaken in production per
|
||||||
|
// `__MOKOSH_UAT__`). The hashed asset path emitted into
|
||||||
|
// `dist-test/assets/<hash>.webm` is authorized for chrome-extension://
|
||||||
|
// scheme access via the explicit `assets/*.webm` web_accessible_resources
|
||||||
|
// entry in manifest.json (also a Plan 04-08 addition).
|
||||||
|
declare module '*.webm?url' {
|
||||||
|
const url: string;
|
||||||
|
export default url;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
{
|
{
|
||||||
"resources": ["src/welcome/welcome.html"],
|
"resources": ["src/welcome/welcome.html"],
|
||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resources": ["assets/*.webm"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
|
|||||||
@@ -41,13 +41,39 @@
|
|||||||
// References:
|
// References:
|
||||||
// - MediaStreamTrack 'ended' event:
|
// - MediaStreamTrack 'ended' event:
|
||||||
// https://developer.mozilla.org/docs/Web/API/MediaStreamTrack/ended_event
|
// https://developer.mozilla.org/docs/Web/API/MediaStreamTrack/ended_event
|
||||||
// - HTMLCanvasElement.captureStream:
|
// - HTMLMediaElement.captureStream (Plan 04-08 source — replaces
|
||||||
// https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/captureStream
|
// HTMLCanvasElement.captureStream per debug session-2 verdict):
|
||||||
|
// https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/captureStream
|
||||||
// - Offscreen document isolation in MV3:
|
// - Offscreen document isolation in MV3:
|
||||||
// https://developer.chrome.com/docs/extensions/reference/api/offscreen
|
// https://developer.chrome.com/docs/extensions/reference/api/offscreen
|
||||||
|
// - Chrome bug 653548 (auto-throttled canvas.captureStream on invisible
|
||||||
|
// canvas; Plan 04-08 root cause):
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=653548
|
||||||
|
|
||||||
import type { MokoshTestSurface } from './types';
|
import type { MokoshTestSurface } from './types';
|
||||||
|
|
||||||
|
// 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/<hash>.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';
|
||||||
|
|
||||||
// Module-level mutable cells holding the runtime references. The
|
// Module-level mutable cells holding the runtime references. The
|
||||||
// recorder calls the setters; the surface getters close over the cells.
|
// recorder calls the setters; the surface getters close over the cells.
|
||||||
let currentStream: MediaStream | null = null;
|
let currentStream: MediaStream | null = null;
|
||||||
@@ -80,24 +106,46 @@ export function setSegmentCountGetter(getter: () => number): void {
|
|||||||
segmentCountGetter = getter;
|
segmentCountGetter = getter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Synthetic getDisplayMedia (prototype path) ───────────────────────
|
// ─── Synthetic getDisplayMedia (Plan 04-08 video-file-backed MediaStream) ───
|
||||||
// State for the canvas-driven fake stream. We retain references at
|
// State for the HTMLVideoElement-driven fake stream. We retain references
|
||||||
// module scope so a second installFakeDisplayMedia() call is a no-op
|
// at module scope so a second installFakeDisplayMedia() call is a no-op
|
||||||
// (idempotent) and so the canvas + animation handle stay alive for the
|
// (idempotent) and so the video element + readiness Promise stay alive for
|
||||||
// lifetime of the offscreen document (canvas-captureStream tracks die
|
// the lifetime of the offscreen document.
|
||||||
// silently when the source element is GC'd).
|
//
|
||||||
|
// Plan 04-08 methodology reframe (debug session-2 verdict 2026-05-22):
|
||||||
|
// the prior canvas.captureStream(30) source was throttled to 0 frames per
|
||||||
|
// segment under headless-Chromium 5-min idle (Chrome bug 653548 — auto-
|
||||||
|
// throttled MediaRecorder on invisible canvas). Replacement: an
|
||||||
|
// HTMLVideoElement playing a bundled WebM source. The video's real decoded
|
||||||
|
// frame timeline is NOT subject to invisible-canvas throttling because
|
||||||
|
// HTMLMediaElement.captureStream draws frames from the media decoder, not
|
||||||
|
// from 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 528-537 still installs the monkey-patch BEFORE
|
||||||
|
// recorder.ts:46-48 top-level await resolves. No race window with the
|
||||||
|
// recorder.startRecording call.
|
||||||
//
|
//
|
||||||
// The displaySurface override is the critical detail: production code
|
// The displaySurface override is the critical detail: production code
|
||||||
// in src/offscreen/recorder.ts:294 enforces displaySurface === 'monitor'
|
// in src/offscreen/recorder.ts:313-321 enforces displaySurface === 'monitor'
|
||||||
// via `track.getSettings()` and tears down the stream + throws
|
// via `track.getSettings()` and tears down the stream + throws
|
||||||
// 'wrong-display-surface' otherwise. Canvas captureStream tracks have
|
// 'wrong-display-surface' otherwise. HTMLVideoElement.captureStream
|
||||||
// displaySurface === undefined by default — so we monkey-patch
|
// tracks have displaySurface === undefined by default — so we monkey-patch
|
||||||
// getSettings() on the video track to return 'monitor'.
|
// getSettings() on the video track to return 'monitor'.
|
||||||
|
|
||||||
let fakeInstalled = false;
|
let fakeInstalled = false;
|
||||||
let fakeCanvas: HTMLCanvasElement | null = null;
|
let fakeVideoEl: HTMLVideoElement | null = null;
|
||||||
let fakeAnimationHandle: number | null = null;
|
// Plan 04-08 BLOCKER 2 (iter-2): lazy first-frame Promise. SYNC install
|
||||||
let fakeDrawInterval: ReturnType<typeof setInterval> | null = null;
|
// 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<void> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plan 01-14 A23 contract — record the last-received constraints object
|
* Plan 01-14 A23 contract — record the last-received constraints object
|
||||||
@@ -142,68 +190,88 @@ export function installFakeDisplayMedia(): void {
|
|||||||
}
|
}
|
||||||
fakeInstalled = true;
|
fakeInstalled = true;
|
||||||
|
|
||||||
// Build a 320x180 canvas drawing a frame counter — the actual pixel
|
// SYNC PHASE — execute synchronously so the monkey-patch on
|
||||||
// content is irrelevant for A6 (the test only cares about the
|
// navigator.mediaDevices.getDisplayMedia is in place BEFORE
|
||||||
// recording state machine, not the video content) but giving the
|
// recorder.ts:46-48 top-level await resolves. Per debug session-2 +
|
||||||
// canvas a moving update keeps the captureStream track in a 'live'
|
// iter-2 BLOCKER 2 — must NOT block on canplay or .play() here.
|
||||||
// state for the rotation-segments lifecycle.
|
|
||||||
|
// 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
|
||||||
|
// 5-min spike window; muted=true is required for autoplay per the
|
||||||
|
// Chrome autoplay policy (audio policy doesn't gate muted video).
|
||||||
|
const videoEl = document.createElement('video');
|
||||||
|
videoEl.src = syntheticDisplaySourceUrl;
|
||||||
|
videoEl.loop = true;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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.
|
||||||
//
|
//
|
||||||
// Plan 01-13 Wave 3B contract: the canvas + drawing loop are persistent
|
// WARNING 1 remediation (iter-2): autoplay reject path produces an
|
||||||
// across MULTIPLE recording lifecycles within the same offscreen
|
// explicit error class identifier ('autoplay-blocked or codec-unsupported
|
||||||
// document (A6 tears recording down via dispatch-ended, A7 starts a
|
// in headless context') so a misconfigured fixture / hostile autoplay
|
||||||
// FRESH recording — both share the same canvas). Each
|
// policy surfaces as a clear failure signal — NOT a mysterious 0-frames
|
||||||
// `fakeGetDisplayMedia` call mints a fresh `MediaStream` via
|
// downstream. The spike's gating check on `videoSize > 100_000` will
|
||||||
// `canvas.captureStream(30)` so the per-call track is in 'live' state
|
// fail with this error visible in the offscreen console capture (or in
|
||||||
// even after the previous recording's tracks were `.stop()`-ed by the
|
// the harness diagnostics), making the root cause unambiguous.
|
||||||
// teardown path (real getDisplayMedia returns a new stream per call;
|
//
|
||||||
// the fake matches that contract).
|
// WARNING 1 SUMMARY-write practice (iter-3 polish): the executor writing
|
||||||
const canvas = document.createElement('canvas');
|
// 04-08-SUMMARY.md MUST document the chosen failure path explicitly —
|
||||||
canvas.width = 320;
|
// 'no Plan B fallback; explicit error-class identifier on autoplay/codec
|
||||||
canvas.height = 180;
|
// reject is the chosen WARNING 1 closure path; downstream observability
|
||||||
canvas.style.position = 'fixed';
|
// via the offscreen console capture is the diagnostic surface.' The
|
||||||
canvas.style.top = '-9999px';
|
// error class is observable in the spike re-run's offscreen-console log
|
||||||
canvas.style.left = '-9999px';
|
// capture, so the SUMMARY's evidence section should cite this path
|
||||||
document.body.appendChild(canvas);
|
// (cf. cosmetic-advisory 2 from checker iter-2).
|
||||||
fakeCanvas = canvas;
|
fakeVideoReadyPromise = new Promise<void>((resolve, reject) => {
|
||||||
|
const onCanPlay = (): void => {
|
||||||
const ctx = canvas.getContext('2d');
|
videoEl.removeEventListener('canplay', onCanPlay);
|
||||||
let frameCount = 0;
|
videoEl.removeEventListener('error', onError);
|
||||||
/**
|
// Chain .play() — required because autoplay attr is best-effort.
|
||||||
* Draw one frame on the synthetic canvas. Keeps the captureStream
|
videoEl.play().then(() => resolve()).catch((err: unknown) => {
|
||||||
* track from going silent (which can cause MediaRecorder to stop
|
reject(new Error(
|
||||||
* emitting dataavailable events on some Chrome versions).
|
`synthetic-display-source.webm play() rejected: ${
|
||||||
*/
|
err instanceof Error ? err.message : String(err)
|
||||||
const drawFrame = (): void => {
|
} (autoplay-blocked or codec-unsupported in headless context)`
|
||||||
if (ctx !== null) {
|
));
|
||||||
ctx.fillStyle = '#222';
|
});
|
||||||
ctx.fillRect(0, 0, 320, 180);
|
|
||||||
ctx.fillStyle = '#fff';
|
|
||||||
ctx.font = '20px sans-serif';
|
|
||||||
ctx.fillText(`frame ${frameCount}`, 20, 100);
|
|
||||||
frameCount += 1;
|
|
||||||
}
|
|
||||||
fakeAnimationHandle = requestAnimationFrame(drawFrame);
|
|
||||||
};
|
};
|
||||||
drawFrame();
|
const onError = (): void => {
|
||||||
|
videoEl.removeEventListener('canplay', onCanPlay);
|
||||||
// Belt-and-suspenders frame driver: requestAnimationFrame fires on
|
videoEl.removeEventListener('error', onError);
|
||||||
// page-visibility heuristics in headless Chrome (offscreen documents
|
reject(new Error(
|
||||||
// are not "visible" tabs — RAF cadence drops to near-zero under
|
'synthetic-display-source.webm failed to load (media error; '
|
||||||
// certain throttling regimes, producing 0-frame segments that then
|
+ 'verify WAR entry for assets/*.webm in manifest.json + '
|
||||||
// crash ts-ebml's VINT decode in `webm-remux.extractFramesFromSegment`
|
+ 'dist-test/assets/<hash>.webm reachable via chrome-extension://)'
|
||||||
// with "Unrepresentable length: Infinity" on the malformed empty
|
));
|
||||||
// bytes). A 33ms setInterval (~30fps) drives drawFrame regardless of
|
};
|
||||||
// RAF throttling — it's redundant for normal RAF but guarantees the
|
videoEl.addEventListener('canplay', onCanPlay);
|
||||||
// captureStream track sees real pixel mutations every tick. Both
|
videoEl.addEventListener('error', onError);
|
||||||
// timers are cleaned up in `uninstallFakeDisplayMedia`.
|
});
|
||||||
fakeDrawInterval = setInterval(drawFrame, 33);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the displaySurface monkey-patch to a freshly-minted stream's
|
* Apply the displaySurface monkey-patch to a freshly-minted stream's
|
||||||
* video track. Production code's post-grant validation reads
|
* video track. Production code's post-grant validation reads
|
||||||
* `getSettings().displaySurface` and tears down + throws
|
* `getSettings().displaySurface` and tears down + throws
|
||||||
* 'wrong-display-surface' on anything but 'monitor' — the patch makes
|
* 'wrong-display-surface' on anything but 'monitor' — the patch makes
|
||||||
* the synthetic canvas stream satisfy that gate.
|
* the synthetic video stream satisfy that gate. WARNING 2 (iter-2):
|
||||||
|
* this patch is verified to work with HTMLVideoElement.captureStream
|
||||||
|
* tracks via the spike re-run's assertA2 fast-fail path; the MDN-
|
||||||
|
* documented contract on the returned track is the same writable
|
||||||
|
* `getSettings` reference (per HTMLMediaElement.captureStream §Return
|
||||||
|
* Value: "A new MediaStream object").
|
||||||
*
|
*
|
||||||
* @param stream - The stream whose first video track is patched in-place.
|
* @param stream - The stream whose first video track is patched in-place.
|
||||||
*/
|
*/
|
||||||
@@ -222,32 +290,45 @@ export function installFakeDisplayMedia(): void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mint a FRESH MediaStream from the persistent canvas. Each invocation
|
* Mint a FRESH MediaStream from the HTMLVideoElement. Each invocation
|
||||||
* generates new tracks in 'live' state — required for the multi-
|
* generates new tracks in 'live' state — required for the multi-
|
||||||
* recording-lifecycle pattern (A6 stops the first stream's tracks via
|
* recording-lifecycle pattern (A6 stops the first stream's tracks via
|
||||||
* dispatchEvent('ended'); A7 starts a new recording which calls
|
* dispatchEvent('ended'); A7 starts a new recording which calls
|
||||||
* getDisplayMedia → must get a live stream, NOT the dead one A6
|
* getDisplayMedia → must get a live stream, NOT the dead one A6
|
||||||
* teardown left behind). Closure variables (fakeCanvas above) persist
|
* teardown left behind). The videoEl persists across calls; track refs
|
||||||
* across calls; track refs do not.
|
* do not. This contract is preserved verbatim from the canvas variant.
|
||||||
*
|
*
|
||||||
* @returns Fresh MediaStream with displaySurface monkey-patch applied
|
* @returns Fresh MediaStream with displaySurface monkey-patch applied
|
||||||
* to its video track.
|
* to its video track.
|
||||||
*/
|
*/
|
||||||
const mintStream = (): MediaStream => {
|
const mintStream = (): MediaStream => {
|
||||||
const stream = canvas.captureStream(30);
|
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 `&` 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);
|
patchDisplaySurface(stream);
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Replace navigator.mediaDevices.getDisplayMedia with a function
|
// LAZY ASYNC PHASE — fakeGetDisplayMedia awaits readiness on first call.
|
||||||
// that mints a FRESH synthetic stream on each call. Production code's
|
// Subsequent calls observe the already-resolved Promise and proceed
|
||||||
// `await navigator.mediaDevices.getDisplayMedia(...)` resolves with a
|
// immediately. The closure is async-shaped to match the real
|
||||||
// newly-minted stream immediately — no picker.
|
// getDisplayMedia signature; recorder.startRecording's
|
||||||
|
// `await navigator.mediaDevices.getDisplayMedia(...)` is fully compatible.
|
||||||
//
|
//
|
||||||
// Cast through `unknown` because the MediaDevices.getDisplayMedia
|
// Cast through `unknown` because the MediaDevices.getDisplayMedia
|
||||||
// type has multiple overloads (with/without constraints) and a
|
// type has multiple overloads (with/without constraints) and a
|
||||||
// straight assignment would trip the type checker. The runtime
|
// straight assignment would trip the type checker.
|
||||||
// dispatch ignores arguments entirely — fake stream regardless.
|
|
||||||
const fakeGetDisplayMedia = async (
|
const fakeGetDisplayMedia = async (
|
||||||
constraints?: DisplayMediaStreamOptions,
|
constraints?: DisplayMediaStreamOptions,
|
||||||
): Promise<MediaStream> => {
|
): Promise<MediaStream> => {
|
||||||
@@ -256,8 +337,26 @@ export function installFakeDisplayMedia(): void {
|
|||||||
// the call site. Default to null on undefined-args so the bridge op
|
// the call site. Default to null on undefined-args so the bridge op
|
||||||
// reports an unambiguous "no args" signal rather than `undefined`.
|
// reports an unambiguous "no args" signal rather than `undefined`.
|
||||||
lastGetDisplayMediaConstraints = constraints ?? null;
|
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();
|
return mintStream();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SYNC MONKEY-PATCH — the eager-install contract requires this assignment
|
||||||
|
// to happen synchronously at function call time.
|
||||||
(navigator.mediaDevices as unknown as {
|
(navigator.mediaDevices as unknown as {
|
||||||
getDisplayMedia: typeof fakeGetDisplayMedia;
|
getDisplayMedia: typeof fakeGetDisplayMedia;
|
||||||
}).getDisplayMedia = fakeGetDisplayMedia;
|
}).getDisplayMedia = fakeGetDisplayMedia;
|
||||||
@@ -273,18 +372,17 @@ export function uninstallFakeDisplayMedia(): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fakeInstalled = false;
|
fakeInstalled = false;
|
||||||
if (fakeAnimationHandle !== null) {
|
if (fakeVideoEl !== null) {
|
||||||
cancelAnimationFrame(fakeAnimationHandle);
|
try {
|
||||||
fakeAnimationHandle = null;
|
fakeVideoEl.pause();
|
||||||
|
} catch {
|
||||||
|
// ignore pause errors during teardown (e.g., already paused)
|
||||||
}
|
}
|
||||||
if (fakeDrawInterval !== null) {
|
fakeVideoEl.remove();
|
||||||
clearInterval(fakeDrawInterval);
|
fakeVideoEl = null;
|
||||||
fakeDrawInterval = null;
|
|
||||||
}
|
|
||||||
if (fakeCanvas !== null) {
|
|
||||||
fakeCanvas.remove();
|
|
||||||
fakeCanvas = null;
|
|
||||||
}
|
}
|
||||||
|
// Drop the Promise reference so a subsequent install creates a fresh one.
|
||||||
|
fakeVideoReadyPromise = null;
|
||||||
// We deliberately do NOT restore the original getDisplayMedia — the
|
// We deliberately do NOT restore the original getDisplayMedia — the
|
||||||
// offscreen document is throwaway and gets a fresh navigator on the
|
// offscreen document is throwaway and gets a fresh navigator on the
|
||||||
// next createDocument() anyway.
|
// next createDocument() anyway.
|
||||||
|
|||||||
@@ -293,4 +293,60 @@ describe('production bundle has no test-hook leaks (Tier-1 gate — T-1-11-01)',
|
|||||||
).toBe(0);
|
).toBe(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
//
|
||||||
|
// Note: the import is `tests/uat/fixtures/synthetic-display-source.webm?url`
|
||||||
|
// and Vite emits the asset to `dist-test/assets/<hash>.webm` only when
|
||||||
|
// the test bundle (vite.test.config.ts) is built. The production bundle
|
||||||
|
// (vite.config.ts) tree-shakes the entire offscreen-hooks.ts module
|
||||||
|
// body because `__MOKOSH_UAT__ === false` makes the dynamic import a
|
||||||
|
// static dead branch in src/offscreen/recorder.ts:46-48. Result: the
|
||||||
|
// filename string `synthetic-display-source` MUST be absent from every
|
||||||
|
// file under dist/.
|
||||||
|
it('Tier-2: synthetic-display-source filename does not leak into production dist/', () => {
|
||||||
|
if (!existsSync(DIST_DIR)) {
|
||||||
|
throw new Error(
|
||||||
|
`dist/ missing — run \`npm run build\` first (SKIP_BUILD=1 is set but no prior build artifact exists).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Walk dist/ files via the existing recursive walker (which skips
|
||||||
|
// symlinks); the existing countOccurrencesInFile helper handles
|
||||||
|
// binary-extension skipping. Grep for the literal string
|
||||||
|
// 'synthetic-display-source'. Expected: 0 hits.
|
||||||
|
const distFiles = listAllFilesRecursive(DIST_DIR);
|
||||||
|
const offendingFiles: Array<{ filePath: string; count: number }> = [];
|
||||||
|
for (const filePath of distFiles) {
|
||||||
|
const count = countOccurrencesInFile(filePath, 'synthetic-display-source');
|
||||||
|
if (count > 0) {
|
||||||
|
offendingFiles.push({ filePath, count });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
offendingFiles.length,
|
||||||
|
offendingFiles.length === 0
|
||||||
|
? 'unreachable'
|
||||||
|
: `Production bundle contains 'synthetic-display-source' filename in ${offendingFiles.length} file(s) — ` +
|
||||||
|
`this would leak the Plan 04-08 test-only WebM fixture filename to production. ` +
|
||||||
|
`The offscreen-hooks.ts module that imports the WebM via Vite ?url should be ` +
|
||||||
|
`tree-shaken in production per __MOKOSH_UAT__; if the filename appears, the ` +
|
||||||
|
`tree-shake has regressed. Offending files:\n` +
|
||||||
|
offendingFiles
|
||||||
|
.map((m) => ` - ${m.filePath} (${m.count} occurrence${m.count === 1 ? '' : 's'})`)
|
||||||
|
.join('\n'),
|
||||||
|
).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
tests/uat/fixtures/synthetic-display-source.webm
Normal file
BIN
tests/uat/fixtures/synthetic-display-source.webm
Normal file
Binary file not shown.
Reference in New Issue
Block a user