Files
mokosh/src/test-hooks/offscreen-hooks.ts
Mark b467123578 feat(01-14): monitorTypeSurfaces:'include' — narrow picker to monitor surfaces only
[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>
2026-05-19 21:37:59 +02:00

541 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 {};