[per Plan 01-14; closes B-01-14-01 via Step 1b lockstep]
- src/offscreen/recorder.ts: add monitorTypeSurfaces:'include' as top-level
DisplayMediaStreamOptions sibling of video: (W3C Screen Capture spec §6.1;
Chrome >= 119; removes tab/window panes from the operator's picker per
Plan 01-10 RESEARCH §5 + §Pitfall-5 recommendation). Typed widening cast
extended in lockstep to keep the explicit-typing contract (no `as any`).
D-15 post-grant validation block at recorder.ts:294 UNCHANGED — belt
(picker narrowing) + suspenders (post-grant tear-down) chain preserved.
- tests/offscreen/display-surface-constraint.test.ts: lockstep update of
the strict-deep-equality assertion at lines 223-226 with the same key
ordering as the source change (video -> monitorTypeSurfaces -> audio).
toHaveBeenCalledWith contract preserved (NO expect.objectContaining —
the test author's "catches future drops of ANY field" discipline is
honored). This edit + the source change land in the SAME commit so the
98/98 baseline never crosses a commit boundary in RED state.
- src/test-hooks/offscreen-hooks.ts: capture last constraints object in
module-scoped `lastGetDisplayMediaConstraints` cell (was `_constraints`
received-but-unused; renamed to `constraints`); add `get-last-getDisplayMedia-constraints`
bridge op to the __mokoshOffscreenQuery dispatcher between
get-display-surface and get-segment-count. Defensive try/catch mirrors
the existing dispatcher pattern; the cell is module-internal so the
MokoshTestSurface cross-cast in types.ts requires NO change (decision
documented inline in offscreen-hooks.ts).
- tests/uat/extension-page-harness.ts: add `assertA23` mirroring `assertA3`
(bridge query → 2-check AssertionResult: non-null constraints + value).
Extend the `Window.__mokoshHarness` declaration + runtime export + status
bar text + console.log to reference A23.
- tests/uat/lib/harness-page-driver.ts: export `driveA23(page)` mirroring
the `driveA14` page.evaluate wrapper shape. Standard read-only driver.
- tests/uat/harness.test.ts: extend FORBIDDEN_HOOK_STRINGS (line 85) with
`lastGetDisplayMediaConstraints` and `get-last-getDisplayMedia-constraints`.
Import driveA23. Append `{ name: 'A23', drive: driveA23 }` to the drivers
array after the A14 entry. Update header comment + orchestrator stdout
to reflect A14 + A23 chain. The `Total = drivers.length + 1` arithmetic
adapts automatically: 14 + 1 = 15 → 15 + 1 = 16.
- tests/background/no-test-hooks-in-prod-bundle.test.ts: lockstep
extension of FORBIDDEN_HOOK_STRINGS (line 105) with the same 2 strings.
Header comment updated to "Total: 12 surface strings." (was 10).
Confirms production `dist/` has ZERO occurrences after `npm run build`
via the `__MOKOSH_UAT__` dead-branch tree-shake (T-01-14-04 mitigation).
D-01 (whole-desktop only via getDisplayMedia; reject window/tab surfaces) is
the design intent that monitorTypeSurfaces:'include' realizes at the picker-
UI level. D-15 post-grant validation (recorder.ts:294-307) remains the
actual enforcement against managed-policy/DevTools/older-Chrome overrides.
Verification chain (per Plan 01-14 §verify; clean post-commit):
- `npx tsc --noEmit` exit 0
- `npm run build` exit 0; dist/ produced, monitorTypeSurfaces ships in
the offscreen chunk as the operator-facing picker hint
- `npm run build:test` exit 0; dist-test/ produced with the harness
hooks intact (gated)
- `npm test` 100/100 GREEN (was 98/98; +2 via the 2 new FORBIDDEN_HOOK_STRINGS
parametrized tests — both PASS, production bundle hook-free)
- `npm run test:uat` 16/16 GREEN (15 → 16 via A23). A23 reads constraints
`{video: {...}, monitorTypeSurfaces: 'include', audio: false}` from the
fakeGetDisplayMedia capture cell — round-trips through the full call site.
- Production bundle spot-check:
`grep -rc 'lastGetDisplayMediaConstraints\|get-last-getDisplayMedia-constraints' dist/ | grep -v ':0$'`
→ empty (all `:0` filtered) → ZERO leakage.
References:
- W3C Screen Capture §6.1 DisplayMediaStreamOptions:
https://www.w3.org/TR/screen-capture/#dom-displaymediastreamoptions-monitortypesurfaces
- Chrome screen-sharing-controls (Chrome 119+):
https://developer.chrome.com/docs/web-platform/screen-sharing-controls
- Plan 01-10 RESEARCH §5 + §Pitfall-5 (recommendation provenance):
.planning/phases/01-stabilize-video-pipeline/01-10-RESEARCH.md
- Architectural-note (replaces retired AMENDMENT-A.md improvisation per
01-11-SUMMARY): canonical GSD ceremony — plan → checker (B-01-14-01)
→ executor → SUMMARY (this commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
541 lines
23 KiB
TypeScript
541 lines
23 KiB
TypeScript
// src/test-hooks/offscreen-hooks.ts — Plan 01-11 Task 2 (offscreen-side test hook).
|
||
//
|
||
// Installs `globalThis.__mokoshTest` in the offscreen document's isolate.
|
||
// The harness reads this surface via `offPage.evaluate(...)` to:
|
||
// - read `getCurrentStream().getVideoTracks()[0].getSettings().displaySurface`
|
||
// (assertion 3 — verifies monitor-only enforcement);
|
||
// - dispatch `new Event('ended')` on the video track (assertion 6 —
|
||
// the Bug B simulation per RESEARCH §7 BLOCKER. `track.stop()` does
|
||
// NOT fire 'ended' per W3C spec, so the production
|
||
// onUserStoppedSharing handler would never run — that is the trap
|
||
// this hook exists to expose);
|
||
// - read `getSegmentCount()` (assertion 11 — verifies 30s ring buffer
|
||
// per D-13).
|
||
//
|
||
// Plan 01-11 PROTOTYPE addition (synthetic MediaStream bypass + offscreen
|
||
// bridge): the `installFakeDisplayMedia()` shim patches
|
||
// `navigator.mediaDevices.getDisplayMedia` so the offscreen recorder's
|
||
// `startRecording` path resolves WITHOUT spawning Chrome's screen-share
|
||
// picker. The `__mokoshOffscreenQuery` chrome.runtime.onMessage bridge
|
||
// allows the extension-internal harness page to invoke
|
||
// installFakeDisplayMedia + dispatch 'ended' on the active track,
|
||
// because page → offscreen direct evaluate is not available; the only
|
||
// cross-isolate path is chrome.runtime.sendMessage.
|
||
//
|
||
// The offscreen recorder wires the runtime references via the two
|
||
// setters exported below. These imports are gated by the same
|
||
// `__MOKOSH_UAT__` token in src/offscreen/recorder.ts as the SW-side
|
||
// hook; production builds tree-shake the entire module away (Tier-1
|
||
// grep gate verifies).
|
||
//
|
||
// Cross-isolate note: SW and offscreen are SEPARATE isolates with
|
||
// SEPARATE `globalThis`. The SW-side sw-hooks.ts installs handler
|
||
// captures + notification observability on the SW's globalThis;
|
||
// this file installs stream + segment-count observability on the
|
||
// offscreen's globalThis. The harness queries the appropriate isolate
|
||
// per assertion. handlers / notificationCount / notificationIds /
|
||
// lastNotificationOptions in this offscreen surface are present-but-
|
||
// inert (initialized to empty values) to keep the type uniform across
|
||
// isolates — the harness never reads them off the offscreen surface.
|
||
//
|
||
// References:
|
||
// - MediaStreamTrack 'ended' event:
|
||
// https://developer.mozilla.org/docs/Web/API/MediaStreamTrack/ended_event
|
||
// - HTMLCanvasElement.captureStream:
|
||
// https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/captureStream
|
||
// - Offscreen document isolation in MV3:
|
||
// https://developer.chrome.com/docs/extensions/reference/api/offscreen
|
||
|
||
import type { MokoshTestSurface } from './types';
|
||
|
||
// Module-level mutable cells holding the runtime references. The
|
||
// recorder calls the setters; the surface getters close over the cells.
|
||
let currentStream: MediaStream | null = null;
|
||
let segmentCountGetter: () => number = () => 0;
|
||
|
||
/**
|
||
* Wire the current MediaStream into the test surface. Called by
|
||
* src/offscreen/recorder.ts:startRecording immediately after the
|
||
* `mediaStream = stream` assignment AND on stream teardown (with
|
||
* null) so the surface tracks the live recording lifecycle.
|
||
*
|
||
* Idempotent — calling with the same value is a no-op equivalent.
|
||
*
|
||
* @param stream - The active MediaStream, or null when teardown.
|
||
*/
|
||
export function setCurrentStream(stream: MediaStream | null): void {
|
||
currentStream = stream;
|
||
}
|
||
|
||
/**
|
||
* Wire the segment-count getter into the test surface. Called once
|
||
* by src/offscreen/recorder.ts when the test bundle is active. The
|
||
* recorder holds a `let segments: Blob[]` module-local; the getter
|
||
* captures it by closure so the harness reads the live count without
|
||
* needing to import the recorder's source from the harness side.
|
||
*
|
||
* @param getter - Closure returning the current segments.length.
|
||
*/
|
||
export function setSegmentCountGetter(getter: () => number): void {
|
||
segmentCountGetter = getter;
|
||
}
|
||
|
||
// ─── Synthetic getDisplayMedia (prototype path) ───────────────────────
|
||
// State for the canvas-driven fake stream. We retain references at
|
||
// module scope so a second installFakeDisplayMedia() call is a no-op
|
||
// (idempotent) and so the canvas + animation handle stay alive for the
|
||
// lifetime of the offscreen document (canvas-captureStream tracks die
|
||
// silently when the source element is GC'd).
|
||
//
|
||
// The displaySurface override is the critical detail: production code
|
||
// in src/offscreen/recorder.ts:294 enforces displaySurface === 'monitor'
|
||
// via `track.getSettings()` and tears down the stream + throws
|
||
// 'wrong-display-surface' otherwise. Canvas captureStream tracks have
|
||
// displaySurface === undefined by default — so we monkey-patch
|
||
// getSettings() on the video track to return 'monitor'.
|
||
|
||
let fakeInstalled = false;
|
||
let fakeCanvas: HTMLCanvasElement | null = null;
|
||
let fakeAnimationHandle: number | null = null;
|
||
let fakeDrawInterval: ReturnType<typeof setInterval> | null = null;
|
||
|
||
/**
|
||
* Plan 01-14 A23 contract — record the last-received constraints object
|
||
* from every `fakeGetDisplayMedia` invocation. The `installFakeDisplayMedia`
|
||
* shim already accepts the `constraints` parameter (previously prefixed
|
||
* `_constraints` as received-but-unused — see below), so the production
|
||
* call site's `monitorTypeSurfaces: 'include'` sibling lands here. The
|
||
* `get-last-getDisplayMedia-constraints` bridge op (later in this file)
|
||
* reads it for harness inspection.
|
||
*
|
||
* `null` until any `getDisplayMedia` call has been made — A23 always
|
||
* runs AFTER A2's `setupFreshRecording` so the cell is populated by
|
||
* then. Test bundle only; gated identically to the rest of this module
|
||
* by the top-of-module `__MOKOSH_UAT__` import in src/offscreen/recorder.ts.
|
||
*/
|
||
let lastGetDisplayMediaConstraints: DisplayMediaStreamOptions | null = null;
|
||
|
||
/**
|
||
* Replace `navigator.mediaDevices.getDisplayMedia` with a synthetic
|
||
* implementation backed by a hidden 30 fps canvas. The returned
|
||
* MediaStream contains exactly one video track; the track's
|
||
* `getSettings()` is monkey-patched to report `displaySurface: 'monitor'`
|
||
* so the production code's post-grant monitor-only validation passes.
|
||
*
|
||
* SAFE to call multiple times — second and subsequent calls are no-ops.
|
||
*
|
||
* The fake stream behaves like a real getDisplayMedia result:
|
||
* - track.kind === 'video'
|
||
* - track.readyState === 'live' until stopped (or until 'ended' dispatched)
|
||
* - track.addEventListener('ended', cb) works as expected
|
||
* - track.dispatchEvent(new Event('ended')) fires registered listeners
|
||
* — this is the Bug B simulation path per RESEARCH §7.
|
||
*
|
||
* Called from the harness via the `__mokoshOffscreenQuery` bridge
|
||
* 'install-fake-display-media' op BEFORE triggering the production
|
||
* recording-start flow. The patch persists for the lifetime of the
|
||
* offscreen document.
|
||
*/
|
||
export function installFakeDisplayMedia(): void {
|
||
if (fakeInstalled) {
|
||
return;
|
||
}
|
||
fakeInstalled = true;
|
||
|
||
// Build a 320x180 canvas drawing a frame counter — the actual pixel
|
||
// content is irrelevant for A6 (the test only cares about the
|
||
// recording state machine, not the video content) but giving the
|
||
// canvas a moving update keeps the captureStream track in a 'live'
|
||
// state for the rotation-segments lifecycle.
|
||
//
|
||
// Plan 01-13 Wave 3B contract: the canvas + drawing loop are persistent
|
||
// across MULTIPLE recording lifecycles within the same offscreen
|
||
// document (A6 tears recording down via dispatch-ended, A7 starts a
|
||
// FRESH recording — both share the same canvas). Each
|
||
// `fakeGetDisplayMedia` call mints a fresh `MediaStream` via
|
||
// `canvas.captureStream(30)` so the per-call track is in 'live' state
|
||
// even after the previous recording's tracks were `.stop()`-ed by the
|
||
// teardown path (real getDisplayMedia returns a new stream per call;
|
||
// the fake matches that contract).
|
||
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);
|
||
fakeCanvas = canvas;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
let frameCount = 0;
|
||
/**
|
||
* Draw one frame on the synthetic canvas. Keeps the captureStream
|
||
* track from going silent (which can cause MediaRecorder to stop
|
||
* emitting dataavailable events on some Chrome versions).
|
||
*/
|
||
const drawFrame = (): void => {
|
||
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();
|
||
|
||
// Belt-and-suspenders frame driver: requestAnimationFrame fires on
|
||
// page-visibility heuristics in headless Chrome (offscreen documents
|
||
// are not "visible" tabs — RAF cadence drops to near-zero under
|
||
// certain throttling regimes, producing 0-frame segments that then
|
||
// crash ts-ebml's VINT decode in `webm-remux.extractFramesFromSegment`
|
||
// 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
|
||
// captureStream track sees real pixel mutations every tick. Both
|
||
// timers are cleaned up in `uninstallFakeDisplayMedia`.
|
||
fakeDrawInterval = setInterval(drawFrame, 33);
|
||
|
||
/**
|
||
* 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 canvas stream satisfy that gate.
|
||
*
|
||
* @param stream - The stream whose first video track is patched in-place.
|
||
*/
|
||
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 persistent canvas. 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). Closure variables (fakeCanvas above) persist
|
||
* across calls; track refs do not.
|
||
*
|
||
* @returns Fresh MediaStream with displaySurface monkey-patch applied
|
||
* to its video track.
|
||
*/
|
||
const mintStream = (): MediaStream => {
|
||
const stream = canvas.captureStream(30);
|
||
patchDisplaySurface(stream);
|
||
return stream;
|
||
};
|
||
|
||
// Replace navigator.mediaDevices.getDisplayMedia with a function
|
||
// that mints a FRESH synthetic stream on each call. Production code's
|
||
// `await navigator.mediaDevices.getDisplayMedia(...)` resolves with a
|
||
// newly-minted stream immediately — no picker.
|
||
//
|
||
// Cast through `unknown` because the MediaDevices.getDisplayMedia
|
||
// type has multiple overloads (with/without constraints) and a
|
||
// straight assignment would trip the type checker. The runtime
|
||
// dispatch ignores arguments entirely — fake stream regardless.
|
||
const fakeGetDisplayMedia = async (
|
||
constraints?: DisplayMediaStreamOptions,
|
||
): Promise<MediaStream> => {
|
||
// 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;
|
||
return mintStream();
|
||
};
|
||
(navigator.mediaDevices as unknown as {
|
||
getDisplayMedia: typeof fakeGetDisplayMedia;
|
||
}).getDisplayMedia = fakeGetDisplayMedia;
|
||
}
|
||
|
||
/**
|
||
* Uninstall the fake getDisplayMedia. Used for cleanup between test
|
||
* runs if multiple recordings need to start fresh. Not called by the
|
||
* A6 prototype (single recording lifecycle).
|
||
*/
|
||
export function uninstallFakeDisplayMedia(): void {
|
||
if (!fakeInstalled) {
|
||
return;
|
||
}
|
||
fakeInstalled = false;
|
||
if (fakeAnimationHandle !== null) {
|
||
cancelAnimationFrame(fakeAnimationHandle);
|
||
fakeAnimationHandle = null;
|
||
}
|
||
if (fakeDrawInterval !== null) {
|
||
clearInterval(fakeDrawInterval);
|
||
fakeDrawInterval = null;
|
||
}
|
||
if (fakeCanvas !== null) {
|
||
fakeCanvas.remove();
|
||
fakeCanvas = null;
|
||
}
|
||
// We deliberately do NOT restore the original getDisplayMedia — the
|
||
// offscreen document is throwaway and gets a fresh navigator on the
|
||
// next createDocument() anyway.
|
||
}
|
||
|
||
/**
|
||
* Dispatch a synthetic 'ended' event on the active stream's video
|
||
* track. This is the Bug B simulation path per RESEARCH §7 BLOCKER —
|
||
* `track.stop()` does NOT fire 'ended' per W3C spec; only
|
||
* dispatchEvent does.
|
||
*
|
||
* Used by A6: the harness calls this after the recording is live;
|
||
* the production `onUserStoppedSharing` handler fires; the SW state
|
||
* machine routes through setIdleMode.
|
||
*
|
||
* @returns Result with ok status; ok=false when no current stream.
|
||
*/
|
||
export function dispatchEndedOnTrack(): { ok: boolean; error?: string } {
|
||
if (currentStream === null) {
|
||
return {
|
||
ok: false,
|
||
error: 'no current MediaStream — recording must be active',
|
||
};
|
||
}
|
||
const track = currentStream.getVideoTracks()[0];
|
||
if (track === undefined) {
|
||
return { ok: false, error: 'no video track in stream' };
|
||
}
|
||
track.dispatchEvent(new Event('ended'));
|
||
return { ok: true };
|
||
}
|
||
|
||
// ─── Install the global surface ───────────────────────────────────────
|
||
// Note: the offscreen isolate's globalThis is FRESH per offscreen
|
||
// document creation (each createDocument restart resets it). The
|
||
// gated dynamic import in recorder.ts top-of-module runs once per
|
||
// offscreen lifetime, so each new offscreen document gets a fresh
|
||
// surface install — there is no cross-lifetime contamination.
|
||
//
|
||
// Augment the surface with the installFakeDisplayMedia entrypoint so
|
||
// the harness can invoke it via offPage.evaluate. The MokoshTestSurface
|
||
// type widens to include this method via a cross-cast at install time
|
||
// — keeping the type clean while still exposing the prototype hook.
|
||
globalThis.__mokoshTest = {
|
||
handlers: {
|
||
onClicked: null,
|
||
onStartup: null,
|
||
notificationOnClicked: null,
|
||
},
|
||
notificationCount: 0,
|
||
lastNotificationOptions: null,
|
||
get notificationIds() {
|
||
return [];
|
||
},
|
||
getCurrentStream: () => currentStream,
|
||
getSegmentCount: () => segmentCountGetter(),
|
||
installFakeDisplayMedia,
|
||
uninstallFakeDisplayMedia,
|
||
dispatchEndedOnTrack,
|
||
} as MokoshTestSurface & {
|
||
installFakeDisplayMedia: typeof installFakeDisplayMedia;
|
||
uninstallFakeDisplayMedia: typeof uninstallFakeDisplayMedia;
|
||
dispatchEndedOnTrack: typeof dispatchEndedOnTrack;
|
||
};
|
||
|
||
// ─── Offscreen bridge: __mokoshOffscreenQuery ────────────────────────
|
||
// The extension-internal harness page cannot evaluate directly in the
|
||
// offscreen isolate (separate globalThis; chrome.runtime.sendMessage
|
||
// is the only cross-isolate path). So we register a dedicated
|
||
// onMessage handler that responds to __mokoshOffscreenQuery messages
|
||
// with the requested operation result.
|
||
//
|
||
// Protocol — page → offscreen message:
|
||
// { type: '__mokoshOffscreenQuery', op: <string> }
|
||
// Response shapes (sync via sendResponse, return false):
|
||
// op='install-fake-display-media' → { ok: true } OR { ok: false, error }
|
||
// op='dispatch-ended' → { ok: true } OR { ok: false, error: 'no stream' }
|
||
// op='has-stream' → { hasStream: boolean }
|
||
// op='get-display-surface' → { displaySurface: string|null } OR { ok: false, error }
|
||
// — Plan 01-13 Wave 3A A3 contract. Returns the active track's
|
||
// `getSettings().displaySurface` value (monkey-patched to 'monitor'
|
||
// by `installFakeDisplayMedia`); returns null when no stream is live.
|
||
// op='get-segment-count' → { count: number } OR { count: -1, error }
|
||
// — Plan 01-13 Wave 3D A11 contract. Returns the offscreen recorder's
|
||
// live `segments.length` via the `segmentCountGetter` closure wired
|
||
// at startRecording (see src/offscreen/recorder.ts:284). Before any
|
||
// startRecording, the getter is the default `() => 0` from line 54
|
||
// above — A11 always calls this AFTER setupFreshRecording so a non-
|
||
// zero count is meaningful. The 10s rotation cadence (D-13;
|
||
// SEGMENT_DURATION_MS) means a recording that has been live for
|
||
// ~35s should report count ≥ 3 (3 × 10s = 30s = MAX_SEGMENTS).
|
||
// op='get-last-getDisplayMedia-constraints' → { constraints: object|null }
|
||
// — Plan 01-14 A23 contract. Returns the most-recent constraints
|
||
// object captured by `fakeGetDisplayMedia`. Used by the harness to
|
||
// verify the production call site passes `monitorTypeSurfaces: 'include'`
|
||
// (W3C Screen Capture spec §6.1; Chrome ≥ 119 picker-narrowing
|
||
// semantics). `null` only when no getDisplayMedia call has happened
|
||
// yet — A23 always runs AFTER A2's setupFreshRecording so a non-null
|
||
// value is the expected case.
|
||
// Unknown ops respond { ok: false, error: 'unknown-op' }.
|
||
//
|
||
// The bridge handler MUST run BEFORE the production offscreen bridge
|
||
// installed at recorder.ts:838 — but offscreen-hooks runs at top-of-
|
||
// module via the gated dynamic import (before bootstrap()), so this
|
||
// ordering is satisfied by construction.
|
||
//
|
||
// IMPORTANT: chrome.runtime.onMessage dispatches to ALL registered
|
||
// listeners; our handler returns false for non-matching message types
|
||
// so the production handler still sees them. The production handler
|
||
// also returns false for unknown types, so there is no two-way
|
||
// contention.
|
||
chrome.runtime.onMessage.addListener((rawMessage, _sender, sendResponse) => {
|
||
if (rawMessage === null || typeof rawMessage !== 'object') {
|
||
return false;
|
||
}
|
||
const message = rawMessage as { type?: unknown; op?: unknown };
|
||
if (message.type !== '__mokoshOffscreenQuery') {
|
||
return false;
|
||
}
|
||
const op = String(message.op ?? '');
|
||
if (op === 'install-fake-display-media') {
|
||
try {
|
||
installFakeDisplayMedia();
|
||
sendResponse({ ok: true });
|
||
} catch (err) {
|
||
sendResponse({
|
||
ok: false,
|
||
error: err instanceof Error ? err.message : String(err),
|
||
});
|
||
}
|
||
return false;
|
||
}
|
||
if (op === 'dispatch-ended') {
|
||
try {
|
||
const r = dispatchEndedOnTrack();
|
||
sendResponse(r);
|
||
} catch (err) {
|
||
sendResponse({
|
||
ok: false,
|
||
error: err instanceof Error ? err.message : String(err),
|
||
});
|
||
}
|
||
return false;
|
||
}
|
||
if (op === 'has-stream') {
|
||
sendResponse({ hasStream: currentStream !== null });
|
||
return false;
|
||
}
|
||
if (op === 'get-display-surface') {
|
||
// Plan 01-13 Wave 3A A3 contract — return the active track's
|
||
// displaySurface so the harness can verify the offscreen-hooks
|
||
// monkey-patched getSettings() correctly reports 'monitor'.
|
||
// Production code in src/offscreen/recorder.ts enforces this same
|
||
// invariant (tears down + throws 'wrong-display-surface' otherwise),
|
||
// so if recording is live the value is guaranteed monitor — the
|
||
// harness asserts === 'monitor' for explicit empirical verification.
|
||
try {
|
||
if (currentStream === null) {
|
||
sendResponse({ displaySurface: null });
|
||
return false;
|
||
}
|
||
const track = currentStream.getVideoTracks()[0];
|
||
if (track === undefined) {
|
||
sendResponse({ displaySurface: null });
|
||
return false;
|
||
}
|
||
const settings = track.getSettings();
|
||
const displaySurface = settings.displaySurface ?? null;
|
||
sendResponse({ displaySurface });
|
||
} catch (err) {
|
||
sendResponse({
|
||
ok: false,
|
||
error: err instanceof Error ? err.message : String(err),
|
||
});
|
||
}
|
||
return false;
|
||
}
|
||
if (op === 'get-last-getDisplayMedia-constraints') {
|
||
// Plan 01-14 A23 contract — return the most-recent constraints object
|
||
// captured by the `fakeGetDisplayMedia` shim. Production code at
|
||
// src/offscreen/recorder.ts:270 calls `navigator.mediaDevices.getDisplayMedia({
|
||
// video: {...}, monitorTypeSurfaces: 'include', audio: false })`; this op
|
||
// exposes that object so the harness can assert
|
||
// `constraints.monitorTypeSurfaces === 'include'`.
|
||
//
|
||
// `null` is returned in two cases:
|
||
// (a) No `getDisplayMedia` call has been made yet — A23 always runs
|
||
// AFTER A2's setupFreshRecording so this is unreachable in the
|
||
// orchestrated sequence;
|
||
// (b) The call was made with `undefined` args — also unreachable for
|
||
// the production call site which always supplies the constraints
|
||
// object.
|
||
// try/catch is defensive — bridge handlers must never propagate
|
||
// exceptions to chrome.runtime.sendMessage (the dispatcher contract
|
||
// shared by all ops above).
|
||
try {
|
||
sendResponse({ constraints: lastGetDisplayMediaConstraints });
|
||
} catch (err) {
|
||
sendResponse({
|
||
ok: false,
|
||
error: err instanceof Error ? err.message : String(err),
|
||
});
|
||
}
|
||
return false;
|
||
}
|
||
if (op === 'get-segment-count') {
|
||
// Plan 01-13 Wave 3D A11 contract — return the offscreen recorder's
|
||
// live segment count via the `segmentCountGetter` closure wired at
|
||
// startRecording (src/offscreen/recorder.ts:284). The closure
|
||
// captures the recorder's module-local `segments: Blob[]` array,
|
||
// which the rotation lifecycle (D-13; SEGMENT_DURATION_MS = 10s,
|
||
// MAX_SEGMENTS = 3) populates with self-contained WebM segments.
|
||
// After ~35s of continuous recording, A11 asserts count >= 3.
|
||
//
|
||
// The default getter (`() => 0`) at module load returns 0 — A11
|
||
// therefore MUST call this AFTER setupFreshRecording so the
|
||
// recorder has wired the live getter. A pre-recording call would
|
||
// legitimately return 0; the harness orders the assertion so this
|
||
// failure mode is unreachable.
|
||
//
|
||
// -1 sentinel on error preserves the dispatcher contract (every
|
||
// op returns a numeric `count` field on the happy path or -1 +
|
||
// `error` on failure). A `try/catch` is defensive against a future
|
||
// getter that throws (the closure-bound module-level array is a
|
||
// pure read, so no throw is expected, but bridge handlers should
|
||
// never propagate exceptions to chrome.runtime.sendMessage).
|
||
try {
|
||
sendResponse({ count: segmentCountGetter() });
|
||
} catch (err) {
|
||
sendResponse({
|
||
count: -1,
|
||
error: err instanceof Error ? err.message : String(err),
|
||
});
|
||
}
|
||
return false;
|
||
}
|
||
sendResponse({ ok: false, error: 'unknown-op' });
|
||
return false;
|
||
});
|
||
|
||
// ─── Auto-install fake getDisplayMedia at module load ────────────────
|
||
// 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();
|
||
} catch (e) {
|
||
console.warn("[offscreen-hooks] eager installFakeDisplayMedia failed:", e);
|
||
}
|
||
|
||
|
||
export {};
|