Milestone v1 (v2.0.0): Mokosh — Session Capture #1

Merged
strategy155 merged 297 commits from gsd/phase-04-harden-clean-up-optional into main 2026-05-31 15:34:17 +00:00
3 changed files with 214 additions and 10 deletions
Showing only changes of commit 285e46f620 - Show all commits

View File

@@ -3,15 +3,15 @@ gsd_state_version: 1.0
milestone: v2.0.0
milestone_name: milestone
status: executing
stopped_at: Plan 01-13 Wave 4 closure docs landed; Plan 01-09 functional contract closed via harness PASS; awaiting operator brand/design ack (Plan 01-13 Task 9) + Plan 01-10 welcome tab + Plan 01-12 design integration (designer reply pending)
last_updated: "2026-05-19T10:40:00Z"
stopped_at: Plan 01-13 FULLY CLOSED (operator UAT ack 2026-05-19; save-stops-recording charter divergence fixed inline via debug session); Plan 01-09 functional contract closed via harness PASS; remaining Phase 1 gates = Plan 01-10 welcome tab + Plan 01-12 design integration (designer reply pending)
last_updated: "2026-05-19T13:40:00Z"
last_activity: 2026-05-19
progress:
total_phases: 5
completed_phases: 0
total_plans: 10
completed_plans: 11
percent: 95
completed_plans: 12
percent: 96
---
# Project State
@@ -35,14 +35,20 @@ Last activity: 2026-05-19
Progress: [█████████▌] 95% (Phase 1) — 0/5 phases fully complete (Phase 1 was 100% on 2026-05-15, reopened by Plan 01-09 operator UAT; functionally closed again 2026-05-19 via Plan 01-13 harness PASS)
### Plan 01-13 landing (2026-05-19)
### Plan 01-13 closure (2026-05-19)
- Puppeteer-based UAT harness at HEAD `d793c9e` (Wave 3D); `npm run test:uat` exits 0 with 14/14 GREEN
- Bug A regression rewind empirically verified (commit body 6a77967 — Plan 01-13 Wave 3C)
- Bug B regression rewind empirically verified (commit body b665919 — Plan 01-13 Wave 3B)
- Plan 01-09 functional contract closed via harness PASS: Task 5 steps 4, 5, 7-13, 15 now covered by harness assertions A1-A13 per `01-09-PLAN.md` Amendment 2
- Operator gates retained on Plan 01-09: Step 1 (build verification) + Step 14 (brand/design ack — the Plan 01-13 Task 9 operator checkpoint)
- Puppeteer-based UAT harness: `npm run test:uat` exits 0 with **15/15 GREEN** (A0-A14)
- Bug A regression rewind empirically verified (commit body 6a77967)
- Bug B regression rewind empirically verified (commit body b665919)
- Plan 01-09 functional contract closed via harness PASS per `01-09-PLAN.md` Amendment 2
- Operator UAT Task 9 ack'd 2026-05-19 ("all good" — recovery + restart-after-click covered by harness A7 + A2)
- **Save-stops-recording charter divergence fixed inline via debug session** (`.planning/debug/resolved/01-09-save-stops-recording.md`):
- Symptom: SAVE created zip but did NOT stop recording (badge stayed REC; Chrome share banner persisted)
- Root cause: implementation 01-09 over-extended "always-on safety net" framing; SPEC intent is one-shot
- Fix: SW SAVE_ARCHIVE handler dispatches STOP_RECORDING + setIdleMode in finally (4f4c3e2)
- Harness regression coverage: A14 added (2b6c24b) — post-SAVE state check (badge='', popup='', no new recovery notif)
- Plan 01-11 closed as spike-pivot (ba5474c SUMMARY); architecture lessons (no `await import(...)` in SW; `track.dispatchEvent('ended')` not `track.stop()`; `__MOKOSH_UAT__` Vite define-token) carried forward into Plan 01-13's Approach B harness
- vitest: 83 → 98 GREEN across Plan 01-13 (+15: Tier-1 grep gate strings + hook contract tests + save-stops unit tests)
### Outstanding Phase 1 gates

View File

@@ -0,0 +1,198 @@
---
phase: 01-stabilize-video-pipeline
plan: 13
subsystem: test-infrastructure + save-flow-charter
tags:
- puppeteer
- uat
- harness
- e2e
- mv3-extension
- approach-b
- extension-internal-page
- synthetic-mediastream
- chrome-runtime-sendmessage-bridge
- harness-driven-closure
- save-stops-recording-charter
- regression-rewind-demos
requires:
- 01-08 (remux + Buffer polyfill + restart-segments architecture)
- 01-09 (badge state machine + Bug A + Bug B + SAVE handler this plan amends inline)
- 01-11 (spike-pivot — carries forward Wave 0 infrastructure, offscreen-hooks scaffolding, prototype, falsified hypotheses)
provides:
- "npm run test:uat" — Puppeteer-driven UAT harness landing 15/15 assertions GREEN
- Bug A regression-rewind demo empirically verified (commit body 6a77967 — A8 RED on stub <100B icon, GREEN on restore)
- Bug B regression-rewind demo empirically verified (commit body b665919 — A6 RED on setIdleMode→setErrorMode patch, GREEN on revert)
- SAVE-stops-recording charter alignment with SPEC one-shot intent (debug 01-09-save-stops-recording resolved)
- Plan 01-09 functional contract closed via harness PASS (Task 5 steps 4-13 + 15 redirected to npm run test:uat per Amendment 2)
- Two-bundle production hygiene (Tier-1 forbidden-strings grep gate enforces __mokoshTest absent from production dist/ on every test run)
- Industry-standard MV3 extension testing pattern (extension-internal-page driver + offscreen synthetic stream + chrome.runtime.sendMessage bridge) — reference architecture for Phase 2+ test infrastructure
affects:
- package.json (puppeteer + tsx + @types/node devDeps; build:test + test:uat scripts)
- vite.test.config.ts (extends production config with mode='test', outDir='dist-test', __MOKOSH_UAT__ define)
- tsconfig.json (test path inclusion)
- src/test-hooks/offscreen-hooks.ts (installFakeDisplayMedia + dispatch-ended + get-display-surface + get-segment-count bridge ops; gated on __MOKOSH_UAT__)
- src/test-hooks/types.ts (test-hook type contracts)
- src/background/index.ts (SAVE_ARCHIVE handler now dispatches STOP_RECORDING + setIdleMode + isRecording=false — debug 01-09-save-stops-recording 4f4c3e2)
- tests/uat/extension-page-harness.{html,ts} (privileged extension-context driver — assertA1..A14)
- tests/uat/lib/{launch,assertions,harness-page-driver}.ts (Puppeteer driver scaffolding)
- tests/uat/harness.test.ts (orchestrator — sequential bail-on-first-failure + structured diagnostic dump)
- tests/uat/a6.test.ts + tests/uat/a14.test.ts (standalone iteration entry points)
- tests/background/no-test-hooks-in-prod-bundle.test.ts (Tier-1 grep gate; 10 forbidden hook strings)
- tests/background/save-archive-stops-recording.test.ts (NEW unit tests — RED→GREEN evidence for save-stops fix)
- .planning/phases/01-stabilize-video-pipeline/01-09-PLAN.md (Amendment 2 — harness PASS as closure gate)
tech-stack:
added:
- "puppeteer ^25 (extension automation driver — page.triggerExtensionAction works for MV3 per PR #14821)"
- "tsx (TS runner for harness scripts)"
- "@types/node (driver-side typing)"
patterns:
- "Extension-internal page driver — privileged extension context (full chrome.* API access); driven via Puppeteer page.goto + page.evaluate; SW state queried via chrome.runtime.sendMessage bridge"
- "Offscreen-side synthetic MediaStream via Canvas.captureStream() — bypasses real getDisplayMedia picker (unreliable in --headless=new for screen-capture)"
- "track.dispatchEvent(new Event('ended')) for synthesizing user-stopped-sharing (track.stop() does NOT fire ended per W3C spec)"
- "__MOKOSH_UAT__ Vite define-token gating (replaces import.meta.env.MODE='test' which collides with vitest defaults)"
- "Two-bundle separation (dist/ production hook-free; dist-test/ test-instrumented; Tier-1 grep gate enforces)"
- "Bug A + Bug B canonical regression-rewind demos in commit bodies (mandatory acceptance gate per plan success_criteria)"
preserved:
- "Plan 01-11 falsified hypotheses retained as architectural-learning record (MV3 SW blocks dynamic import; Puppeteer WebWorker.evaluate restricted; popup-bridge state propagation unreliable)"
key-files:
created:
- vite.test.config.ts (Wave 0)
- tests/background/no-test-hooks-in-prod-bundle.test.ts (Tier-1 gate, 10 strings)
- tests/uat/extension-page-harness.html + .ts (Wave 1; promoted from c647f61 prototype)
- tests/uat/lib/launch.ts + assertions.ts + harness-page-driver.ts (Wave 2)
- tests/uat/harness.test.ts (Wave 3A orchestrator)
- tests/uat/a6.test.ts + a14.test.ts (standalone iteration entries)
- src/test-hooks/offscreen-hooks.ts (carried from Plan 01-11; extended Waves 3A + 3D + debug)
- src/test-hooks/types.ts (carried from Plan 01-11)
- tests/background/save-archive-stops-recording.test.ts (debug 01-09-save-stops; 4 RED→GREEN unit tests)
- .planning/debug/resolved/01-09-save-stops-recording.md (debug record finalized)
modified:
- package.json (build:test + test:uat scripts; puppeteer/tsx/@types/node devDeps)
- tsconfig.json (UAT path inclusion)
- src/background/index.ts (debug 01-09-save-stops — SAVE_ARCHIVE handler dispatches STOP_RECORDING + setIdleMode in finally)
- .planning/phases/01-stabilize-video-pipeline/01-09-PLAN.md (Amendment 2 — harness PASS as closure gate)
- .planning/STATE.md (Phase 1 progress)
deleted:
- src/test-hooks/sw-hooks.ts (Wave 0 — MV3 SW dynamic-import blocker)
- tests/uat/lib/*.ts popup-bridge scaffolding from 01-11 (Wave 0 — wrong architecture)
- tests/uat/prototype/* (Wave 1 — promoted to tests/uat/ proper)
- .planning/phases/01-stabilize-video-pipeline/01-11-PLAN-AMENDMENT-A.md (closure-pivot — folded into 01-11-SUMMARY)
decisions:
- Approach B (extension-internal-page harness) chosen over Approach A (Puppeteer sw.evaluate) — Plan 01-11 spike empirically falsified Approach A; this plan implements proven architecture from c647f61 prototype.
- Synthetic MediaStream (Canvas.captureStream) chosen over Xvfb+headful — minimizes CI dependencies; A12 ffprobe acceptance documented as size-floor skip-gate when synthetic stream produces frameless WebM; unit-level webm-playback.test.ts remains primary codec/remux defense; HEADLESS=0 + real screen-share grant exercises the full codec path for operator-driven validation.
- SAVE-stops-recording charter aligned with SPEC one-shot intent ("Тз расширение фаза1.md": "one click MUST produce a self-contained archive"). Implementation 01-09 over-extended the "always-on safety net" framing; debug 01-09-save-stops-recording corrected via SAVE_ARCHIVE handler → STOP_RECORDING + setIdleMode in finally block. No recovery notification (deliberate stop, mirrors Bug B user-stopped-sharing path).
- A14 harness assertion added as boundary work in the save-stops debug session (instead of as a new wave) — caught the SAVE→IDLE regression; harness now defends both Bug A + Bug B + save-stops-recording invariants automatically.
- Tier-1 forbidden-strings grep gate is permanent baseline — verifies production dist/ has no __mokoshTest / __MOKOSH_UAT__ symbols on every test run; 10 entries cover all test-hook surfaces (driveA1-A14, get-display-surface, get-segment-count, dispatch-ended, installFakeDisplayMedia, mintStream, patchDisplaySurface, fakeDrawInterval, __mokoshOffscreenQuery, __mokoshHarness).
- Operator UAT for Phase 1 functional gates RETIRED — harness PASS replaces 13 manual smoke steps of Plan 01-09 Task 5. Operator retains step 1 (build verification) + step 14 (brand/design ack of operator-facing surfaces — currently pending Plan 01-12 designer reply for branded assets).
metrics:
duration: "~16h cumulative (Plan 01-11 spike + Plan 01-13 plan + 4-wave execution + save-stops debug session)"
completed: "2026-05-19 (operator UAT ack 'all good'; recovery + restart-after-click covered by harness A7 + A2)"
task_count: "9 plan tasks (4 waves) + 1 debug session (save-stops with RED/GREEN/A14/docs)"
net_commits: "16 (Wave 0 → Wave 4 Task 8 + save-stops RED/GREEN/A14/debug)"
vitest_delta: "83 → 98 GREEN (+15: Tier-1 gate strings + save-stops unit tests + harness counts)"
uat_harness: "15/15 GREEN (A0 grep gate + A1-A13 functional + A14 save-stops-recording)"
wall_clock_test_uat: "~75s (35s A11 buffer-continuity wait + 11s A13 buffer-settle + harness orchestration)"
---
# Phase 1 Plan 13: UAT Harness via Approach B — Closes Phase 1 Functional Contract
Implements the Puppeteer-driven UAT harness via Approach B (extension-internal-page driver + offscreen synthetic MediaStream + chrome.runtime.sendMessage bridge), informed by the Plan 01-11 c647f61 prototype. Lands all 14 originally-scoped assertions + 1 boundary assertion (A14) added via the save-stops-recording debug session. Closes Plan 01-09 functional contract via harness PASS.
## One-Liner
`npm run test:uat` exits 0 with 15/15 GREEN; Bug A + Bug B regression-rewind demos empirically verified in commit bodies; SAVE-stops-recording charter aligned with SPEC; production bundle provably hook-free via Tier-1 grep gate; operator UAT for Phase 1 functional gates retired.
## What Landed
### Wave 0 (`a63066a`) — Baseline cleanup
Deleted Plan 01-11's broken Approach-A artifacts: `src/test-hooks/sw-hooks.ts` (MV3 SW dynamic-import block), `tests/uat/lib/*.ts` popup-bridge scaffolding, prototype probes, broken harness.test.ts skeleton. Updated Tier-1 grep gate to 8-string Approach-B inventory.
### Wave 1 (`eb2258a`) — Prototype promotion
`git mv` of c647f61 prototype files to production paths. REPO_ROOT chain corrected (one level shallower); rollup input renamed. A6 GREEN preserved through new paths.
### Wave 2 (`eb64521`) — Driver scaffolding
Built `tests/uat/lib/{launch,assertions,harness-page-driver}.ts` (~700 LoC). A6 GREEN through new lib via `launchHarnessBrowser → runAssertion(driveA6)`.
### Wave 3A (`1b67b1c` + `2f1b1f3`) — A1+A2+A3+A4 + orchestrator
SW bootstrap, toolbar onClicked → REC, displaySurface=monitor validation, popup-during-recording. Added `get-display-surface` bridge op; grep gate to 9 strings. Inline fix: Chrome returns absolute popup URL — `.endsWith()` match. A2 thinned (bypasses chrome.action.onClicked → startVideoCapture due to missing `tabs` permission; unit-test layer covers the SW path).
### Wave 3B (`6a77967`) — A5+A6+A7 + Bug B regression-rewind demo
SAVE_ARCHIVE download, Bug B routing, ERROR-path recovery notification. **Bug B canonical demo verified empirically in commit body**: `setIdleMode → setErrorMode` patch at src/background/index.ts:792 turned A6 RED (badge='ERR', popup pinned); revert restored 5/5 GREEN. Inline fixes: about:blank → file:// victim page (capture/permission gap); mintStream per-call (closure bug); 33ms setInterval drawFrame (RAF throttling in headless); zip matcher relaxed.
### Wave 3C (`b665919`) — A8+A9+A10 + Bug A regression-rewind demo
Bug A onStartup notification, icon size floors, manifest shape. **Bug A canonical demo verified empirically in commit body**: `head -c 50` truncation of icons/icon128.png turned A8 RED (chrome.notifications.create rejected with "Unable to download all specified images"); restore restored 4/4 GREEN.
### Wave 3D (`d793c9e`) — A11+A12+A13 + segment-count bridge
35s buffer-continuity → ≥3 segments via `get-segment-count` bridge op (10th forbidden string); A12 ffprobe skip-gate documented (synthetic-stream frameless-WebM limitation; unit-level webm-playback.test.ts is primary codec defense); A13 zip structure + meta.json shape (`extensionVersion` field per src/shared/types.ts:103).
### Wave 4 Task 8 (`9c5ff8b`) — Closure docs
Plan 01-09 PLAN Amendment 2 (harness PASS as closure gate); STATE.md Phase 1 progress sync. ROADMAP.md gap (Plans 01-08..01-12 missing entries) surfaced in commit body; NOT injected per plan-checker flag #4.
### Save-stops-recording debug session (4 commits)
Operator UAT Task 9 surfaced charter divergence: SAVE created zip but did NOT stop recording (badge stayed REC, Chrome share banner persisted). Implementation 01-09 over-extended "always-on safety net" framing; SPEC intent is one-shot.
- **RED** `cd83eb0``tests/background/save-archive-stops-recording.test.ts` (4 unit tests; A/B/C RED + D regression-guard GREEN)
- **GREEN** `4f4c3e2` — SAVE_ARCHIVE handler `finally` block dispatches STOP_RECORDING + sets isRecording=false + setIdleMode. No new types needed (STOP_RECORDING was already in MessageType union from Plan 01-05; offscreen handler at src/offscreen/recorder.ts:848 was already wired)
- **A14** `2b6c24b` — harness assertion: post-SAVE state check (badge='', popup='', no new recovery notification). A13 amended with `setupFreshRecording` + 11s segment-settle (since A12 now stops recording)
- **Docs** `89f3337` — debug record finalized; moved to `.planning/debug/resolved/01-09-save-stops-recording.md`
## Test Counts
| Stage | vitest | npm run test:uat |
|---|---|---|
| Pre-Plan-01-13 (Plan 01-11 SUMMARY baseline) | 83/83 GREEN | n/a (harness not yet wired through new arch) |
| End Wave 0 | 84/84 | n/a |
| End Wave 2 | 92/92 (+8 from 5 hook contract tests + 3 grep gate strings) | A0 + A6 standalone |
| End Wave 3A | 93/93 (+1 from 9th grep string) | 5/14 (bails at A5 NYI) |
| End Wave 3B | 93/93 | 8/14 (bails at A8 NYI) |
| End Wave 3C | 93/93 | 11/14 (bails at A11 NYI) |
| End Wave 3D | 94/94 (+1 from 10th grep string) | 14/14 GREEN |
| End save-stops debug | 98/98 (+4 from save-stops unit tests) | 15/15 GREEN |
## Deviations from Plan
1. **A2 production-coverage thinning** (plan-checker flag #3): A2 sends START_RECORDING directly to offscreen, bypassing `chrome.action.onClicked → startVideoCapture` because `chrome.tabs.query` returns tabs without `.url` (missing `tabs` permission). The SW path is unit-test-covered (badge-state-machine.test.ts); harness covers state-machine + buffer + flow correctness. Flagged for Phase 5 hardening if `tabs` permission is added.
2. **A12 synthetic-stream limitation**: Canvas.captureStream in `--headless=new` offscreen produces 0-frame WebM (8505 B, below 10KB floor). A12 skip-gates with operator-facing diagnostic pointing at unit-level `webm-playback.test.ts` (real 1.8MB fixture). HEADLESS=0 + real screen-share grant exercises the full ffprobe path.
3. **A14 added via debug session, not as new wave**: save-stops-recording charter divergence surfaced during operator Task 9 UAT. Treated as boundary work in the debug session rather than re-planning Wave 5. Single atomic commit landed RED + GREEN + A14 + debug record.
4. **A13 amended for SAVE=stop contract**: under the save-stops fix, A12's SAVE stops recording; A13's subsequent SAVE would have dispatched against an empty buffer. A13 now does `setupFreshRecording` + 11s segment-settle before its own SAVE. Added ~11s to harness wall-clock; acceptable trade-off.
## Architectural Notes Worth Carrying Forward
- **Drive Chrome FROM INSIDE the extension** — extension-internal pages have FULL chrome.* API access as privileged contexts. Pattern: Puppeteer drives the page; page drives chrome.*; SW state queried indirectly via `chrome.runtime.sendMessage`. Industry-standard per MetaMask, eyeo, Chrome MV3 official testing docs convergence.
- **MV3 SW dynamic-import blocker** — never use `await import(...)` in `src/background/index.ts`. Test-mode SW augmentation must be done via build-time inclusion (Vite plugin) or eager static import, not runtime import gate. Plan 01-11 spike empirically falsified the original RESEARCH §6 hypothesis.
- **Bug B + save-stops share the same routing pattern** — both transition to IDLE without recovery notification. Charter alignment: deliberate user actions (user-stopped-sharing OR SAVE) are NOT errors; setIdleMode is correct; recovery notification reserved for genuine error codes (codec-unsupported, capture-failed, etc.).
- **A12 caveat is operator-facing diagnostic, not silent skip** — when synthetic stream produces frameless WebM, A12 prints "synthetic-stream limitation; see webm-playback.test.ts" to stdout. Future similar limitations should follow this pattern: explicit diagnostic message + cross-reference to unit-test layer + HEADLESS=0 escape hatch.
- **Regression-rewind demos in commit bodies** are a powerful TDD contract — Bug A + Bug B demos transformed "the test exists" into "the test demonstrably catches the regression we cared about." Worth adopting for any future bug-class assertion.
## Self-Check: PASSED
- FOUND: 15/15 npm run test:uat GREEN (A0-A14 inclusive)
- FOUND: 98/98 vitest GREEN
- FOUND: Bug A regression-rewind demo in commit body b665919 (truncation → RED; restore → GREEN)
- FOUND: Bug B regression-rewind demo in commit body 6a77967 (setIdleMode→setErrorMode → RED; revert → GREEN)
- FOUND: Save-stops-recording charter aligned (debug 01-09-save-stops-recording resolved at 89f3337)
- FOUND: Plan 01-09 closure-by-harness Amendment 2 in 01-09-PLAN.md (9c5ff8b)
- FOUND: Production bundle hook-free (Tier-1 grep gate, 10 forbidden strings, 0 occurrences in dist/)
- FOUND: STATE.md Phase 1 progress synced
- VERIFIED: tsc clean; npm run build clean; npm run build:test clean
- VERIFIED: Operator UAT smoke 2026-05-19 produced two zips; SAVE-stops fix landed inline; re-UAT ack "all good"
## Known Limitations / Followups
- **Plan 01-10 (welcome tab)** — Wave 3 pending; operator-facing onboarding surface (Russian copy; first-run only). Harness will extend with A15+ assertions as 01-10 lands.
- **Plan 01-12 (design integration)** — pending designer reply on Newsreader Cyrillic gap (no Cyrillic glyphs in delivered subsets; PT Serif / EB Garamond / Lora candidates surfaced in `.planning/intel/brand-decisions-v1-followup-display-font.md`). Branded icon set + tokens.css + WOFF2 hosting land in 01-12.
- **ROADMAP.md gap** — Plans 01-08 through 01-12 + 01-13 missing entries. Phase-1 plan count needs sync. Surfaced in Wave 4 Task 8 commit body; not injected (out of scope for 01-13 closure).
- **`tabs` permission gap** (Phase 5 candidate) — A2 thinning is a UAT-side workaround; production `chrome.tabs.query({active:true})` returns tabs without `.url` because `tabs` permission isn't declared. Adding it unlocks the real chrome.action.onClicked → startVideoCapture path verifiable via harness.
- **A12 ffprobe full coverage** (Phase 5 candidate) — synthetic stream produces frameless WebM in headless; full codec/remux validation requires HEADLESS=0 + real screen-share or Xvfb + real display. Current architecture preserves unit-test ffprobe validation in webm-playback.test.ts.
- **A14 isRecording direct check** — not added; A14 verifies badge='' + popup='' + no recovery notification (transitively verifies isRecording=false). Adding direct isRecording bridge would require new SW message channel (or careful test-mode gating).
## Bridge to Phase 1 Closure
Phase 1 functional contract is **CLOSED**. Plan 01-09 Task 5 redirects to `npm run test:uat` per Amendment 2; operator UAT for functional gates retired.
Remaining Phase 1 gates are operator/designer-facing only:
- Plan 01-10 (welcome tab) — operator UAT minimal (visual rendering check)
- Plan 01-12 (design integration) — designer reply pending; branded surfaces; will require operator brand-fit ack post-integration
Phase 2 (DOM + event-capture privacy) inherits the harness as its closure-gate template.