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>
This commit is contained in:
2026-05-19 21:37:59 +02:00
parent 433ee280f3
commit b467123578
7 changed files with 228 additions and 13 deletions

View File

@@ -267,11 +267,27 @@ async function startRecording(): Promise<void> {
// Use a typed widening through `DisplayMediaStreamOptions &
// {video: Record<...>}` instead of `as any` to stay explicit
// about the precise field we're adding.
//
// Plan 01-14: `monitorTypeSurfaces: 'include'` is a TOP-LEVEL member
// of DisplayMediaStreamOptions (W3C Screen Capture spec §6.1 — NOT
// nested in `video:`; that would route it to MediaTrackConstraints).
// Chrome ≥ 119 (Chrome screen-sharing-controls release notes:
// https://developer.chrome.com/docs/web-platform/screen-sharing-controls)
// honors the constraint by narrowing the picker dialog to ONLY
// monitor surfaces — the Window and Chrome-Tab panes are elided.
// The post-grant validation block immediately below remains the
// actual enforcement (constraints are HINT-class per spec — the
// operator can still override via the picker UI on older Chrome
// versions, and managed-policy / DevTools can override anywhere).
// Belt (picker-UI narrowing) + suspenders (post-grant tear-down)
// per Plan 01-10 RESEARCH §5 + §Pitfall-5 recommendation.
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: 'monitor', cursor: 'always' },
monitorTypeSurfaces: 'include', // Plan 01-14 — Chrome ≥ 119 picker narrowing per W3C spec §6.1
audio: false, // SPEC §9 — Phase 2 / CAP-01 territory
} as DisplayMediaStreamOptions & {
video: { displaySurface: 'monitor'; cursor: 'always' };
monitorTypeSurfaces: 'include';
});
mediaStream = stream;
// Plan 01-11 Task 2: wire the live MediaStream into the test hook

View File

@@ -99,6 +99,22 @@ 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
@@ -233,8 +249,13 @@ export function installFakeDisplayMedia(): void {
// straight assignment would trip the type checker. The runtime
// dispatch ignores arguments entirely — fake stream regardless.
const fakeGetDisplayMedia = async (
_constraints?: DisplayMediaStreamOptions,
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 {
@@ -355,6 +376,14 @@ globalThis.__mokoshTest = {
// 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
@@ -433,6 +462,34 @@ chrome.runtime.onMessage.addListener((rawMessage, _sender, sendResponse) => {
}
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