f86fd60d4a1654d2c20d1259324f223128ed319a
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 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>
|
|||
| 1baaf45702 |
feat(01-13-A14-invert): A14 — invert to assert continuous-recording post-SAVE
Plan 01-09 Amendment 3 (2026-05-19) end-to-end lock. Inverted A14 to
match the reversed charter (SAVE creates zip, recording continues).
Page-side (tests/uat/extension-page-harness.ts):
- assertA14: assert badge==='REC' (was ''), popup endsWith
'src/popup/index.html' (was ''), no-new-recovery-notif (unchanged).
- A14 name + check labels updated to reflect continuous-recording semantic.
- New constant A14_POPUP_HTML_SUFFIX for the popup endsWith check
(ext-id-agnostic via suffix match).
- A13 docstring + diag strings refreshed: setupFreshRecording is now
defensive (orthogonal to A12 ordering) rather than a workaround for
the prior auto-stop. 11s settle preserved (same wall-clock cost).
Host-side (tests/uat/lib/harness-page-driver.ts):
- driveA14 docstring refreshed to mention Amendment 3 + the inverted
contract; mechanical wrapper unchanged.
Verification:
- npm run test:uat: 15/15 GREEN
- A14 actual output:
badge='REC'
popup='chrome-extension://<ext-id>/src/popup/index.html'
recoveryDelta=0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 2b6c24b2d9 |
feat(01-13): A14 — post-SAVE state check (badge='', popup='', no new recovery notif)
Plan 01-13 Task 9 closure for operator empirical UAT bug
.planning/debug/01-09-save-stops-recording.md. Adds the harness
assertion that empirically verifies the SAVE-auto-stops-recording fix
(committed at
|
|||
| d793c9e1e5 |
feat(01-13): wave-3D — A11+A12+A13 GREEN + get-segment-count bridge op; 14/14 GREEN
Lands the final three UAT-harness assertions. All 14 assertions (A0..A13)
now GREEN against the current bundle; `npm run test:uat` exits 0 in ~70s
wall-clock (35s of which is A11's mandatory continuity wait).
Assertions wired:
- A11 — 35s buffer continuity → segments.length >= 3. Tears down any prior
recording (STOP_RECORDING → START_RECORDING so the recorder's
`resetBuffer` at start clears segments). Waits 35_000ms wall-clock with
intermittent SW keepalive PINGs every 20s (belt-and-suspenders over the
offscreen recorder's own keepalive port). Queries the new
`get-segment-count` bridge op. Asserts count >= 3 (per D-13:
SEGMENT_DURATION_MS=10s × MAX_SEGMENTS=3).
- A12 — SAVE_ARCHIVE produces zip; webm passes ffprobe. Page side
dispatches SAVE_ARCHIVE (recording from A11 still alive). Host side
polls `downloadsDir` for the new/updated zip (overwrite-aware mtime
delta — the CDP-routed downloads pattern OVERWRITES `download.zip`
rather than numbering it, empirically verified during initial RED).
Extracts `video/last_30sec.webm` via JSZip to a tmpfile. Runs
`/usr/bin/ffprobe -v error -f matroska <path>`; asserts exit 0 + clean
stderr. Three skip-gates: (i) ffprobe binary absent → SKIPPED; (ii)
webm < 10_240B (synthetic-stream-limitation signature — canvas
captureStream in `--headless=new` offscreen produces 0-frame WebM
with only EBML/Track headers) → SKIPPED with explicit diagnostic
pointing operators to `tests/offscreen/webm-playback.test.ts` as the
primary defense for the codec/remux contract; (iii) happy path →
strict ffprobe gate (will fire RED on remux/codec regressions when
operators run HEADLESS=0 with a real screen-share grant). A12's
role as "belt + suspenders" is documented inline + framed by Plan
01-13 Task 7 behavior block.
- A13 — Zip structure + meta.json shape. Second SAVE_ARCHIVE (verifies
idempotency over A12's first save). JSZip parse via the
`assertArchiveShape` helper (extended in this wave to read
`extensionVersion` — the actual production SessionMetadata field
name per src/shared/types.ts:103, vs. the earlier 01-11 prototype's
incorrect `version` assumption). Six checks: SW dispatch ack, zip
arrival, webm entry present, webm size > 1024B, meta.json entry
present, meta.json.extensionVersion matches
chrome.runtime.getManifest().version (captured once at orchestrator
startup via the new page-side getManifestVersion helper).
Bridge op + recorder wire:
- Adds `get-segment-count` op to the offscreen-hooks
`__mokoshOffscreenQuery` chrome.runtime.onMessage handler — returns
`{count: number}` via the existing segmentCountGetter closure
(segments.length captured at recorder.ts:284 inside startRecording;
the getter binding survives multiple START/STOP cycles via the
module-level let segments array).
- Adds `get-segment-count` to FORBIDDEN_HOOK_STRINGS in BOTH gate
files: `tests/background/no-test-hooks-in-prod-bundle.test.ts`
(Tier-1 unit gate; 9 → 10 entries; vitest 93 → 94 GREEN) and
`tests/uat/harness.test.ts:assertA0_GrepGate` (UAT-level mirror).
Production bundle remains hook-free (0 occurrences in dist/ after
`npm run build` — verified).
Harness surface:
- `tests/uat/extension-page-harness.ts` extends `window.__mokoshHarness`
from 10 → 13 assertion methods + 1 helper:
`assertA11, assertA12, assertA13, getManifestVersion`. Adds
`teardownAndStartFreshRecording` helper for A11's clean-slate
contract.
- `tests/uat/lib/harness-page-driver.ts` retires the Wave-3 stub
marker (no more NYI throws). Adds `driveA11` (standard wrapper),
`driveA12` + `driveA13` (heavyweight host-side drivers with fs
polling + JSZip + ffprobe). Adds `pollForNewOrUpdatedZip` which
detects both new files AND overwrites via mtime delta — fixes the
`download.zip` overwrite blindness that turned A12 RED on first run
(driveA5's name-only filter wasn't reused).
- `tests/uat/lib/zip.ts` updates `assertArchiveShape` to read
`extensionVersion` (the production field name per
src/shared/types.ts:103); adds the A13_MIN_VIDEO_BYTES=1024 floor
constant.
- `tests/uat/harness.test.ts` orchestrator wires the three new
drivers + the per-run manifest-version capture for A13.
Baseline:
- `npx tsc --noEmit`: exit 0.
- `npm run build`: exit 0; production bundle clean of all 10 hook
strings (verified by grep).
- `npm run build:test`: exit 0; test bundle ships `get-segment-count`.
- `npx vitest run`: 94/94 GREEN (was 93; +1 from the new gate string).
- `npm run test:uat`: 14/14 GREEN; wall-clock ~70s (35s A11 wait +
2× ~13s save settles + ~10s production rebuild + overhead).
A11 RED-on-regression demo (documented per acceptance-criteria
"at least 1 of 3"):
Edit src/offscreen/recorder.ts:52: `SEGMENT_DURATION_MS = 10_000`
→ `SEGMENT_DURATION_MS = 30_000`. Rebuild dist-test. Re-run UAT.
A11 FAILS (only 1 segment rotates in 35s, vs floor of 3). Revert
the edit; A11 PASSES. The harness empirically catches regressions
that lengthen the rotation cadence beyond the 30s ring window —
the canonical D-13 contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| b665919c5f |
feat(01-13): wave-3C — A8+A9+A10 GREEN + Bug A canonical regression rewind
Plan 01-13 Task 6 (Wave 3C). Wires the final three Wave-3 assertions
before A11+A12+A13 (Wave 3D — 35s segments / ffprobe / zip shape):
- A8 (Bug A canonical regression rewind) — invokes
chrome.notifications.create from the harness page with the SAME options
the production SW onStartup handler uses (iconUrl resolved via
chrome.runtime.getURL('icons/icon128.png')). Exercises Chrome's
imageUtil icon validation — the exact code path Bug A regressed on
(
|
|||
| 6a77967b6c |
feat(01-13): wave-3B — A5+A6+A7 GREEN + Bug B canonical regression rewind
Wave 3B lands the A5 (SAVE_ARCHIVE → zip on disk) and A7 (genuine
RECORDING_ERROR → ERR + recovery notification) assertions, completing
8/14 of the orchestrator's GREEN floor (A0+A1+A2+A3+A4+A5+A6+A7).
Bails at A8 (Wave 3C scope).
Changes per file:
tests/uat/extension-page-harness.ts
- assertA5: 11s settle (>= SEGMENT_DURATION_MS so first rotation
lands a segment) + send SAVE_ARCHIVE + assert resp.success=true.
Page-side only checks SW handler ack; host-side driver verifies
disk-side outcome (zip presence + size floor).
- assertA7: setupFreshRecording helper (A6 tears down; A7 needs
REC state) → snapshot notif count → send RECORDING_ERROR with
a non-Bug-B error code ('codec-unsupported') → 200ms settle →
assert badge='ERR' + popup endsWith popup.html + notif delta=1
+ set-membership for 'mokosh-recovery-*' prefix.
- setupFreshRecording: shared helper for A7 + future assertions
that need a fresh REC state after a teardown.
tests/uat/lib/harness-page-driver.ts
- driveA5: page.evaluate(assertA5) THEN host-side fs polling for
*.zip in handles.downloadsDir. The CDP Browser.setDownloadBehavior
override renames the file to download.zip (data: URL filename
gap), so we accept any *.zip suffix. Merges page-side check +
host-side checks into a single AssertionRecord. Signature now
takes downloadsDir as a second arg.
- driveA7: standard page.evaluate wrapper (no host-side work).
tests/uat/harness.test.ts
- Wraps driveA5 in a closure that captures handles.downloadsDir.
- Reordered: launchHarnessBrowser MUST run before driver list so
the closure can read handles without a TDZ trap.
tests/uat/lib/launch.ts
- Victim page switched from about:blank to a file:// URL backed by
a tmp HTML file in downloadsDir. About:blank breaks A5 because
chrome.tabs.captureVisibleTab needs <all_urls> permission which
matches http/https/file/ftp but NOT about: or data: URLs. The
stub HTML satisfies <all_urls> + provides a real .url for the
production saveArchive's chrome.tabs.query.
src/test-hooks/offscreen-hooks.ts (test-only — tree-shaken from prod)
- installFakeDisplayMedia: mintStream() helper called per
fakeGetDisplayMedia invocation; each call mints a FRESH
MediaStream from the persistent canvas. Real getDisplayMedia
returns a new stream per call — fake now matches. Required for
A7's setupFreshRecording where the previous recording's stream
tracks were stopped by A6's onUserStoppedSharing teardown.
- Added 33ms setInterval-driven drawFrame() alongside the
existing requestAnimationFrame loop. RAF can throttle in
headless Chrome on offscreen documents (page-visibility
heuristics produce 0 fps), which yields zero-byte
MediaRecorder segments that crash ts-ebml's VINT decode in
webm-remux.extractFramesFromSegment with "Unrepresentable
length: Infinity". The setInterval is redundant when RAF fires
at full rate; it's a safety net for the headless-MV3 corner.
Bug B regression-catch demo (success_criteria #3 — MANDATORY per plan):
Step 1 — apply local regression patch (NOT committed):
src/background/index.ts:792 setIdleMode() → setErrorMode()
Step 2 — npm run build:test && npm run test:uat RED snippet:
A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: FAIL
[PASS] SETUP: badge becomes REC after start
[FAIL] A6.1: badge text is '' (NOT 'ERR') after user-stop
expected: ""
actual: "ERR"
[FAIL] A6.2: popup is '' (NOT manifest default) after user-stop
expected: ""
actual: "chrome-extension://<id>/src/popup/index.html"
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
UAT harness: 6/14 assertions passed (bailed: A6 failed; see above)
Step 3 — revert local patch (git checkout -- src/background/index.ts).
Step 4 — npm run build:test && npm run test:uat GREEN snippet:
A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: PASS
[PASS] SETUP: badge becomes REC after start
[PASS] A6.1: badge text is '' (NOT 'ERR') after user-stop
[PASS] A6.2: popup is '' (NOT manifest default) after user-stop
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
UAT harness: 8/14 assertions passed (bailed: A8 failed — NOT YET
IMPLEMENTED — Wave 3C wires driveA8)
The harness CORRECTLY catches the Bug B regression — the canonical
debug 01-09-recovery-flow scenario (operator-initiated stop routed
through setErrorMode locks the operator out of restart because popup
stays pinned to SAVE-only mode). Bug B is now CI-callable end-to-end.
vitest 93/93 GREEN throughout (unit-test layer unaffected). Tier-1
grep gate GREEN (9 forbidden hook strings: 0 occurrences in dist/).
npm run build exit 0; npx tsc --noEmit exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 1b67b1c1d3 |
feat(01-13): wave-3A — A1+A2+A3+A4 GREEN + harness.test.ts orchestrator (5/14 assertions GREEN)
Wave 3A landed. `npm run test:uat` now exercises 5/14 assertions
end-to-end (A0 + A1 + A2 + A3 + A4); bails at A5 NOT YET IMPLEMENTED
(Wave 3B scope). A6 still PASSES 5/5 through the standalone
`npx tsx tests/uat/a6.test.ts` entry — the orchestrator-level A6 won't
reach in Wave 3A because the sequential loop bails at A5; once Wave 3B
wires driveA5 the loop will fall through to A6 (which uses the proven
Wave-2 driveA6 driver — no rework needed there).
Files changed:
- `tests/uat/extension-page-harness.ts` — extends `window.__mokoshHarness`
from `{ assertA6 }` to `{ assertA1, assertA2, assertA3, assertA4,
assertA6 }`. Per-assertion contracts:
• A1 — chrome.action.getBadgeText({}) === '' + getPopup({}) === ''
+ isRecording=false (badge !== 'REC' proxy per state-machine atomic
pairing). 3 CheckRecords.
• A2 — ensureOffscreen + START_RECORDING direct-to-offscreen
(workaround for the `tabs` manifest permission gap per
01-11-SUMMARY + plan resolved-questions row 2) + manual
setBadgeText('REC') + setPopup(POPUP_HTML_PATH) + waitFor
badge==='REC'. The bypassed chrome.action.onClicked →
startVideoCapture path is unit-tested in
tests/background/badge-state-machine.test.ts; A2 verifies the
contract that matters (recording reaches the REC state-machine
row). 2 CheckRecords.
• A3 — offscreen bridge query 'get-display-surface' (new in this
plan via the prior commit's offscreen-hooks extension) → asserts
=== 'monitor'. 1 CheckRecord.
• A4 — getPopup remains 'src/popup/index.html' + hasDocument()===true
(no duplicate offscreen). Essentially a no-op verification —
regression protection against future refactors that might unpin
the popup during recording or spawn extra offscreens on stray
events. 2 CheckRecords.
• IMPORTANT: chrome.action.getPopup() returns the FULL absolute
chrome-extension://<id>/... URL (not the manifest-relative path).
A2.2 + A4.1 assert via .endsWith('src/popup/index.html') to stay
extension-id independent. Empirical finding from first orchestrator
run; documented inline.
- `tests/uat/lib/harness-page-driver.ts` — wires `driveA1/A2/A3/A4`
(replaces the 4 NOT YET IMPLEMENTED Wave-3A stubs from
|
|||
| eb2258a880 |
feat(01-13): wave-1 — promote c647f61 prototype to production paths; A6 GREEN
Move the three load-bearing prototype files from `tests/uat/prototype/`
to their production paths under `tests/uat/`, leaving the architectural
narrative (research findings, BLOCKER citations, falsification table
references) intact. No behavioral changes — A6 still PASSES 5/5 in ~7s
end-to-end from the new paths.
File moves (git mv preserves history):
- tests/uat/prototype/extension-page-harness.html
→ tests/uat/extension-page-harness.html
- tests/uat/prototype/extension-page-harness.ts
→ tests/uat/extension-page-harness.ts
- tests/uat/prototype/a6.test.ts
→ tests/uat/a6.test.ts
The `tests/uat/prototype/` directory is now empty (git does not track
empty directories; will not appear in subsequent `git status`).
Path-reference updates inside the moved files:
- tests/uat/extension-page-harness.html: `<p>` line referencing the
chrome-extension:// URL updated to drop `/prototype/`.
- tests/uat/extension-page-harness.ts: file-header docstring rewritten
to cite Plan 01-13 / Approach B / inheritance from
|