Plan 01-13 fully closed. Operator UAT acked "all good" on 2026-05-19; recovery flow (A7) + restart-after-click (A2) both harness-covered, no manual verification needed. What this commit lands: - 01-13-SUMMARY.md (full spike-pivot-then-implementation narrative; tracks all 16 commits across Plan 01-11 spike + Plan 01-13 4-wave execution + save-stops debug session; documents 15/15 npm run test:uat GREEN + 98/98 vitest GREEN + Bug A/B regression-rewind demos verified) - Save-stops debug record moved to .planning/debug/resolved/ (closure- canonical location; prior inline path tracked via git mv) - STATE.md sync: completed_plans 11→12, percent 95→96, Plan 01-13 fully-closed narrative + save-stops debug session captured Phase 1 functional contract: CLOSED via harness PASS. Remaining Phase 1 gates: Plan 01-10 (welcome tab) + Plan 01-12 (design integration; pending designer Newsreader-Cyrillic reply). Phase 2 inherits the harness as its closure-gate template. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19 KiB
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 handlerfinallyblock 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 withsetupFreshRecording+ 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
- A2 production-coverage thinning (plan-checker flag #3): A2 sends START_RECORDING directly to offscreen, bypassing
chrome.action.onClicked → startVideoCapturebecausechrome.tabs.queryreturns tabs without.url(missingtabspermission). The SW path is unit-test-covered (badge-state-machine.test.ts); harness covers state-machine + buffer + flow correctness. Flagged for Phase 5 hardening iftabspermission is added. - A12 synthetic-stream limitation: Canvas.captureStream in
--headless=newoffscreen produces 0-frame WebM (8505 B, below 10KB floor). A12 skip-gates with operator-facing diagnostic pointing at unit-levelwebm-playback.test.ts(real 1.8MB fixture). HEADLESS=0 + real screen-share grant exercises the full ffprobe path. - 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.
- 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(...)insrc/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).
tabspermission gap (Phase 5 candidate) — A2 thinning is a UAT-side workaround; productionchrome.tabs.query({active:true})returns tabs without.urlbecausetabspermission 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.