Files
Mark e035fd279d docs(01-09): Amendment 3 + 01-13 SUMMARY reversal note + STATE.md sync + debug records
Plan 01-09 Amendment 3 (2026-05-19) — atomic documentation pass for
the save-does-not-stop-recording charter reversal.

Changes:
- .planning/phases/01-stabilize-video-pipeline/01-09-PLAN.md:
  Amendment 3 block added above <success_criteria> (mirrors
  Amendment 2 placement). Describes the reversed charter,
  references the new debug record, points at the inverted
  test file + harness A14.
- .planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md:
  "Subsequent Reversal (2026-05-19)" footer added. Notes that
  npm run test:uat still 15/15 GREEN under the inverted A14
  contract; vitest baseline preserved at 98 GREEN.
- .planning/STATE.md:
  Plan 01-13 closure block extended with CHARTER REVERSAL bullet
  citing the 4 commit SHAs (6ac23fd RED, 7645765 GREEN,
  1baaf45 A14 invert, this commit docs).
- .planning/debug/resolved/01-09-save-stops-recording.md:
  SUPERSEDED 2026-05-19 footer appended (audit trail; original
  fix was technically correct against its charter, reversal is
  UX iteration not technical defect).
- .planning/debug/resolved/01-09-save-does-not-stop-recording.md:
  NEW debug record landed directly in resolved/ (no checkpoint
  cycle — orchestrator-diagnosed reversal). Documents symptom,
  charter clarification cycle, fix shape, RED→GREEN evidence
  with commit SHAs + vitest/harness output, anti-regression
  coverage at unit + E2E layers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:50:49 +02:00

209 lines
20 KiB
Markdown
Raw Permalink 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.
---
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.
## Subsequent Reversal (2026-05-19)
The save-stops-recording fix landed at commit 89f3337 was REVERSED on
2026-05-19 per operator UX iteration preferring the original "always-on
safety net" charter. See `.planning/debug/resolved/01-09-save-does-not-stop-recording.md`
+ Plan 01-09 Amendment 3. Test files inverted to lock the new charter;
harness A14 inverted. `npm run test:uat` still 15/15 GREEN under the
new contract; vitest baseline preserved at 98 GREEN (the 4 save-archive
tests inverted — same count).