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 (6ac23fdRED,7645765GREEN,1baaf45A14 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>
209 lines
20 KiB
Markdown
209 lines
20 KiB
Markdown
---
|
||
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).
|