Plan 01-09 SUMMARY:
• 17 new tests landed GREEN (4 displaySurface + 5 toolbar-action
including W-02 popup-idle-race + 4 badge + 4 notification).
• Baseline 64 + 17 new = 81 GREEN. Full suite 18 files / 81 tests.
• Tier-1 SW-bundle-import gate (Layer 1 + 2) remains GREEN.
• tsc clean; npm run build clean; dist/manifest.json carries
notifications permission.
• 4 deviation rules auto-fixed inline (navigator getter helper,
jsdom-free W-02 Test E refactor, cursor type-widening cast,
chrome.* listener try/catch for pre-existing test compatibility).
• Task 5 (operator empirical checkpoint) deferred per plan.
15 KiB
Phase 1 Plan 9: Toolbar-onClicked Direct-to-Picker + Display-Surface Monitor + Badge State Machine + Notifications + SAVE-only Popup
Activation-cost reduction (3 clicks → 2) + footgun retirement (operator-picks-tab no longer reaches recording) + operator-visible recording state via badge + OS notifications inviting fresh starts and surfacing errors.
One-Liner
displaySurface:'monitor' + cursor:'always' getDisplayMedia constraints with post-grant enforcement; chrome.action.onClicked toolbar-to-picker flow (popup SAVE-only); 3-state badge machine (REC/OFF/ERROR) + chrome.runtime.onStartup notification + chrome.notifications.onClicked recovery flow.
What Landed
Tests (17 new, all GREEN end-of-plan)
| Test File | Tests | Contract Pinned |
|---|---|---|
tests/offscreen/display-surface-constraint.test.ts |
4 | getDisplayMedia exact constraints + non-monitor pick teardown + monitor over-fire guard + classifyCaptureError branch |
tests/background/toolbar-action.test.ts |
5 (A-E) | onClicked registered + no-record→start + recording→no-double-start + setPopup state-machine + popup SAVE-only init contract (W-02 Test E) |
tests/background/badge-state-machine.test.ts |
4 | REC palette + OFF palette (init) + ERROR palette + RECORDING_ERROR-triggered ERROR transition |
tests/background/onstartup-notification.test.ts |
4 | onStartup registered + creates mokosh-startup- notification + onClicked clears + START_RECORDING + RECORDING_ERROR triggers mokosh-recovery- |
Source Changes
-
src/offscreen/recorder.ts—CaptureErrorCodeunion extended with'wrong-display-surface';classifyCaptureErrormatches the prefix;getDisplayMediacall carries{video:{displaySurface:'monitor',cursor:'always'},audio:false}(typed via explicit widening cast — lib.dom.d.ts omitscursor); post-grant validation readstrack.getSettings().displaySurface, on mismatch tears down stream + nulls mediaStream + throwswrong-display-surfaceError which routes through the existing catch+RECORDING_ERROR pipeline. -
src/background/index.ts— Added badge palette constants (SCREAMING_SNAKE),setBadgeState(state: 'REC'|'OFF'|'ERROR')helper (3-state machine),setIdleMode/setRecordingMode/setErrorModemode helpers,startVideoCapturewiressetRecordingModeon success andsetErrorModein catch,chrome.action.onClickeddirect-to-picker listener,chrome.runtime.onStartupnotification creator,chrome.notifications.onClickedmokosh-prefix-gated handler, RECORDING_ERROR onMessage branch with recovery notification,initialize()callssetIdleModeat boot. All registrations defensively wrapped in try/catch. -
src/popup/index.ts— RemovedcheckPermissions+requestPermissionsfunctions;popupStatedefaults tohasPermissions:true, isRecording:trueunder SAVE-only charter;init()no longer fires REQUEST_PERMISSIONS; empty-state copy points operator back to toolbar icon. -
manifest.json— Added"notifications"to permissions array. -
smoke.sh—SHARE_TARGET="Entire screen"(locale-English) + 14-line locale-fallback comment block citing Chromium grit generated_resources.grd as authoritative source for IDS_DESKTOP_MEDIA_PICKER_SOURCE_TYPE_SCREEN identifier (locale strings drift across Chrome versions); HTML title made distinct from screen-source string;<ol>and intro paragraph updated to reflect entire-screen sharing. T+/wall timer overlay (commit923aaca) preserved.
Test Counts
- Baseline before Plan 01-09: 15 files / 64 tests / all GREEN.
- After Task 1 RED commit: 16 files / 68 tests / 4 RED (Tests 1, 2, 3, 4 → only Test 3 GREEN because no over-fire from a non-existent validation block).
- After Task 2 GREEN: 16 files / 68 tests / all GREEN.
- After Task 3 RED commit: 18 files / 81 tests / 13 RED.
- After Task 4 GREEN: 18 files / 81 tests / all GREEN.
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 - Blocking] Vitest node env: navigator getter-only
- Found during: Task 1 (running the preserved RED test).
- Issue:
globalThis.navigatorin Vitest's node env has only a getter; direct assignment throwsCannot set property navigator of #<Object> which has only a getter. All 4 preserved tests failed at the env-setup step instead of for the right contract reasons. - Fix: Added
installNavigatorStub(spy)helper that usesObject.defineProperty(globalThis, 'navigator', {value, configurable:true, writable:true})instead of direct assignment. All 4 navigator assignments rewired to the helper. - Files modified:
tests/offscreen/display-surface-constraint.test.ts - Commit:
333e0dc
2. [Rule 3 - Blocking] jsdom not installed; W-02 Test E refactored to node-env
- Found during: Task 3 (running the new test files).
- Issue: The plan's W-02 Test E originally used
@vitest-environment jsdompragma butjsdomis not innode_modules. Installing it would be an unscoped dependency add. - Fix: Refactored Test E to a node-env-friendly form that manually stubs the document surface the popup module touches (
getElementById('saveButton'/'statusMessage'),querySelector('.button-text'),addEventListeneron both button and document, plus a_docListenersmap so the test can synthesize theDOMContentLoadedevent). Test E still pins both contracts (E1: no REQUEST_PERMISSIONS at popup-open; E2: saveButton enabled). - Files modified:
tests/background/toolbar-action.test.ts - Commit:
2d7ff7d
3. [Rule 1 - Bug] TypeScript: cursor not in MediaTrackConstraints
- Found during: Task 2 (post-source-edit
npx tsc --noEmit). - Issue: lib.dom.d.ts's
MediaTrackConstraintstype omitscursor(the Screen Capture spec defines it but TypeScript's bundled types lag; cited MDN). Initial GREEN edit failed TSC withTS2353: Object literal may only specify known properties. - Fix: Used explicit type-widening cast
as DisplayMediaStreamOptions & {video:{displaySurface:'monitor';cursor:'always'}}(NOTas anyper project style — CLAUDE.md "Don't ignore lint/type errors without researching proper fixes first") to add the field precisely. - Files modified:
src/offscreen/recorder.ts - Commit:
de162b4
4. [Rule 1 - Bug] Pre-existing background tests broken by new chrome. listener registrations*
- Found during: Task 4 (full-suite run after GREEN edit).
- Issue: 6 pre-existing background tests (5 in
request-id-protocol.test.ts, 1 inport-lifecycle-continuous.test.ts) crashed at module load withTypeError: Cannot read properties of undefined (reading 'onClicked')because their chrome stubs predatechrome.action,chrome.notifications,chrome.runtime.onStartup. - Fix: Wrapped each of the 3 new top-level listener
addListenercalls intry/catch(matches the existing defensive pattern in the file, e.g.chrome.offscreen?.hasDocumentcheck at line 673). Production MV3 always provides these surfaces. - Files modified:
src/background/index.ts - Commit:
06dee24
Tier-1 SW-Bundle-Import Gate
GREEN throughout. The gate exercises npm run build + Layer 1 (SW module loads under jsdom-like environment) + Layer 2 (remuxSegments runs with the Buffer polyfill). The Plan 01-09 changes (new listener registrations, badge state machine, popup SAVE-only) compile and bundle cleanly; the Buffer polyfill from Plan 01-08 commit dd7bf00 + ebml CJS resolution from cc6e81a both remain intact (no vite.config.ts edits in this plan).
Task 5 Status: Operator Checkpoint — Deferred
Task 5 (checkpoint:human-verify) requires an operator to run npm run build + ./smoke.sh, open Chrome, load the unpacked extension, and walk through 13 end-to-end verification steps covering: notification at browser startup, monitor-only picker, badge transitions REC/OFF/ERROR, recovery notification on stop-sharing, popup-opens-when-recording, idle-toolbar-click-triggers-picker. The how-to-verify section in the plan file lists all 13 steps + the resume-signal contract.
The autonomous side (Tasks 1-4) is complete with 17/17 new tests GREEN and full suite 18 files / 81 tests / all GREEN. tsc clean; build clean; dist/manifest.json carries the notifications permission.
Architectural Notes Worth Carrying Forward
-
Constraint-hint-vs-enforcement gap.
getDisplayMedia({video:{displaySurface:'monitor',...}})is a HINT to Chrome's picker, NOT a hard constraint — operators can still pick a tab or window. Post-granttrack.getSettings().displaySurfaceis the actual enforcement. Future plans that add new MediaTrackConstraints should treat constraints as preferences and add post-grant validation for anything load-bearing. -
setPopup '' ⇄ html dance. chrome.action.onClicked.addListener fires ONLY when no default_popup is set (or it's been cleared via
chrome.action.setPopup({popup:''})). Dynamically swapping the popup is the canonical Chrome pattern for "click-action depends on state" toolbar buttons. The SAVE-only popup charter relies on this — when recording is OFF the click does start-action; when REC it opens SAVE. -
mokosh- notification id namespace. All Plan 01-09 notifications use
mokosh-startup-ormokosh-recovery-prefixes withDate.now()suffix for uniqueness. The onClicked handler validates the prefix before any side effect (T-1-09-01 spoofing mitigation). Future notification work should keep extending this namespace (e.g.mokosh-savefail-for save-error toasts). -
Defense-in-depth chrome. try/catch.* Every chrome API call that touches a surface a unit-test stub might not fully implement is wrapped in try/catch + a logger.warn. This is now the project's documented pattern (per the deviation 4 footnote) — unit tests should be able to load the SW module without crashing even if they only stub the subset they care about.
Self-Check: PASSED
- FOUND: tests/offscreen/display-surface-constraint.test.ts
- FOUND: tests/background/toolbar-action.test.ts
- FOUND: tests/background/badge-state-machine.test.ts
- FOUND: tests/background/onstartup-notification.test.ts
- FOUND: .planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md
- FOUND:
333e0dc(Task 1 RED) - FOUND:
de162b4(Task 2 GREEN) - FOUND:
2d7ff7d(Task 3 RED) - FOUND:
06dee24(Task 4 GREEN)
Known Stubs
None. The popup empty-state copy points operators to use the toolbar icon — this is intentional UX, not a stub (the toolbar icon is the start path; the popup is the save path). No placeholder data or hardcoded empty values flowing to UI.