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
4 changed files with 84 additions and 49 deletions
Showing only changes of commit eb2258a880 - Show all commits

View File

@@ -1,22 +1,31 @@
// tests/uat/prototype/a6.test.ts — Plan 01-11 PROTOTYPE. // tests/uat/a6.test.ts — Plan 01-13 standalone A6 entry point.
// //
// Puppeteer-driven feasibility test for the orchestrator-proposed // Puppeteer-driven single-assertion driver for A6 (Bug B canonical).
// architecture: extension-internal test page + chrome.runtime.sendMessage // Originally landed as the Plan 01-11 prototype at commit c647f61;
// bridge + synthetic MediaStream. Runs ONE end-to-end assertion: A6 // Plan 01-13 Wave 1 promoted this file from `tests/uat/prototype/` to
// (Bug B canonical) — when the offscreen recorder fires // the production path without behavioral change. Wave 2 will refactor
// RECORDING_ERROR{error: 'user-stopped-sharing'} (simulated via // the launch + console-capture + result-print plumbing into reusable
// dispatchEvent('ended')), the SW state machine routes through // lib helpers (`tests/uat/lib/{launch,assertions,harness-page-driver}
// setIdleMode (NOT setErrorMode), badge becomes empty, popup empties, // .ts`) and rewrite this driver against them; Wave 3 folds A6 into
// isRecording=false, NO recovery notification fires. // `tests/uat/harness.test.ts` as the assertion of record for `npm run
// test:uat`. This standalone entry is RETAINED throughout for fast
// TDD iteration on the A6 contract (`npx tsx tests/uat/a6.test.ts` —
// ~7s end-to-end vs the orchestrator's ~60-90s for all 14).
// //
// VERDICT path: PASS = the prototype architecture works → orchestrator // Assertion contract — A6 (Bug B canonical): when the offscreen
// can re-spawn 01-11 executor with new brief. FAIL = architectural // recorder fires RECORDING_ERROR{error: 'user-stopped-sharing'}
// blocker(s) remain → falls back to Option B (partial coverage) or // (simulated via dispatchEvent('ended') on the active video track per
// Option C (operator UAT). // 01-11 RESEARCH §7 BLOCKER — track.stop() does NOT fire 'ended' per
// W3C spec), the SW state machine routes through setIdleMode (NOT
// setErrorMode): badge becomes empty, popup empties, isRecording=false,
// NO recovery notification fires. The prototype verified this PASSES
// 5/5 today AND FAILS on local revert of the Bug B fix at
// src/background/index.ts:776 — both halves of the RED-on-regression
// demo land in the Wave 3B commit body as the canonical TDD canon.
// //
// Usage: // Usage:
// tsx tests/uat/prototype/a6.test.ts // tsx tests/uat/a6.test.ts
// HEADLESS=0 tsx tests/uat/prototype/a6.test.ts # debug view // HEADLESS=0 tsx tests/uat/a6.test.ts # debug view
// //
// Pre-flight: requires `dist-test/` from `npm run build:test`. The test // Pre-flight: requires `dist-test/` from `npm run build:test`. The test
// will fail loudly if the bundle is missing. // will fail loudly if the bundle is missing.
@@ -34,8 +43,13 @@ import { fileURLToPath } from 'node:url';
import puppeteer, { type Browser, type Page } from 'puppeteer'; import puppeteer, { type Browser, type Page } from 'puppeteer';
// Plan 01-13 Wave 1: this file lives at `tests/uat/a6.test.ts` (was
// `tests/uat/prototype/a6.test.ts` pre-Wave-1). Repo root is two
// directory levels up — was three pre-Wave-1. The resolvePath chain
// MUST stay in sync with the on-disk location or `DIST_TEST_DIR` will
// resolve to the wrong path and `assertBundlePresent` will throw.
const HARNESS_FILE_DIR = dirname(fileURLToPath(import.meta.url)); const HARNESS_FILE_DIR = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolvePath(HARNESS_FILE_DIR, '..', '..', '..'); const REPO_ROOT = resolvePath(HARNESS_FILE_DIR, '..', '..');
const DIST_TEST_DIR = resolvePath(REPO_ROOT, 'dist-test'); const DIST_TEST_DIR = resolvePath(REPO_ROOT, 'dist-test');
/** Per-check record returned by the harness page. */ /** Per-check record returned by the harness page. */
@@ -130,7 +144,7 @@ async function launchChrome(): Promise<{
function printResult(result: HarnessAssertionResult): void { function printResult(result: HarnessAssertionResult): void {
process.stdout.write('\n'); process.stdout.write('\n');
process.stdout.write('='.repeat(72) + '\n'); process.stdout.write('='.repeat(72) + '\n');
process.stdout.write(`PROTOTYPE A6 result: ${result.passed ? 'PASS' : 'FAIL'}\n`); process.stdout.write(`A6 result: ${result.passed ? 'PASS' : 'FAIL'}\n`);
process.stdout.write(`Assertion: ${result.name}\n`); process.stdout.write(`Assertion: ${result.name}\n`);
if (result.error !== undefined) { if (result.error !== undefined) {
process.stdout.write(`Top-level error: ${result.error}\n`); process.stdout.write(`Top-level error: ${result.error}\n`);
@@ -155,7 +169,7 @@ function printResult(result: HarnessAssertionResult): void {
* @returns 0 on PASS, 1 on FAIL. * @returns 0 on PASS, 1 on FAIL.
*/ */
async function main(): Promise<number> { async function main(): Promise<number> {
process.stdout.write('\nMokosh Plan 01-11 PROTOTYPE — A6 (Bug B canonical) feasibility test\n'); process.stdout.write('\nMokosh Plan 01-13 — A6 (Bug B canonical) standalone driver\n');
process.stdout.write('Architecture: extension-internal page + bridge + synthetic stream\n'); process.stdout.write('Architecture: extension-internal page + bridge + synthetic stream\n');
process.stdout.write('='.repeat(72) + '\n'); process.stdout.write('='.repeat(72) + '\n');
@@ -173,7 +187,7 @@ async function main(): Promise<number> {
try { try {
// Open the prototype harness page. The page lives at the test-build // Open the prototype harness page. The page lives at the test-build
// path (vite.test.config.ts adds it as a rollup input). // path (vite.test.config.ts adds it as a rollup input).
const harnessUrl = `chrome-extension://${extensionId}/tests/uat/prototype/extension-page-harness.html`; const harnessUrl = `chrome-extension://${extensionId}/tests/uat/extension-page-harness.html`;
process.stdout.write(`Opening: ${harnessUrl}\n`); process.stdout.write(`Opening: ${harnessUrl}\n`);
// Open a 'victim' page first — production code calls // Open a 'victim' page first — production code calls

View File

@@ -6,7 +6,7 @@
</head> </head>
<body> <body>
<h1>Mokosh UAT — extension-internal page harness</h1> <h1>Mokosh UAT — extension-internal page harness</h1>
<p>This page lives at <code>chrome-extension://&lt;id&gt;/tests/uat/prototype/extension-page-harness.html</code>.</p> <p>This page lives at <code>chrome-extension://&lt;id&gt;/tests/uat/extension-page-harness.html</code>.</p>
<p>Puppeteer navigates a tab here and drives assertions via <code>window.__mokoshHarness.*</code>.</p> <p>Puppeteer navigates a tab here and drives assertions via <code>window.__mokoshHarness.*</code>.</p>
<pre id="status">Ready.</pre> <pre id="status">Ready.</pre>
<script type="module" src="./extension-page-harness.ts"></script> <script type="module" src="./extension-page-harness.ts"></script>

View File

@@ -1,46 +1,59 @@
// tests/uat/prototype/extension-page-harness.ts — Plan 01-11 PROTOTYPE. // tests/uat/extension-page-harness.ts — Plan 01-13 production UAT harness.
//
// Inherited from the Plan 01-11 prototype at commit c647f61 per the
// 01-11-SUMMARY.md architectural pivot (Approach B). The prototype
// proved A6 (Bug B canonical regression rewind) 5/5 GREEN in ~7s
// end-to-end, validating the architecture summarized below. Plan 01-13
// Wave 1 promoted this file from `tests/uat/prototype/` to the
// production path without behavioral change; Wave 3 will extend the
// `window.__mokoshHarness` surface with assertA1..A13 methods.
// //
// Extension-internal harness page entrypoint. Lives at // Extension-internal harness page entrypoint. Lives at
// `chrome-extension://<id>/tests/uat/prototype/extension-page-harness.html` // `chrome-extension://<id>/tests/uat/extension-page-harness.html`
// in the test build (vite.test.config.ts adds it as a Rollup input). // in the test build (vite.test.config.ts adds it as a Rollup input).
// //
// PURPOSE: prove the orchestrator's hypothesis — that the working // ARCHITECTURAL ANCHOR (Approach B): the working architecture for MV3
// architecture for MV3 extension UAT is to drive Chrome FROM INSIDE // extension UAT is to drive Chrome FROM INSIDE (extension-internal
// (extension-internal test page + synthetic MediaStream) rather than // test page + synthetic MediaStream) rather than FROM OUTSIDE (CDP
// FROM OUTSIDE (CDP into SW context). // into SW context). Falsification of the Approach-A alternatives
// (sw.evaluate + popup-bridge + SW-side dynamic-import test hooks)
// is documented in 01-11-SUMMARY.md.
// //
// IMPORTANT RESEARCH FINDING (in-flight prototype investigation): // IMPORTANT RESEARCH FINDING (load-bearing — DO NOT REGRESS):
// The Plan 01-11 RESEARCH §6 architecture used `await import(...)` // Plan 01-11 RESEARCH §6 originally architected `await import(...)`
// at the top of src/background/index.ts to gate SW-side test hooks. // at the top of src/background/index.ts to gate SW-side test hooks.
// EMPIRICAL: dynamic import is BLOCKED in MV3 service workers // EMPIRICAL FALSIFICATION (01-11 prototype, verified Chrome 148):
// (Chrome 148, verified via probe). The SW silently dies — the // dynamic import is BLOCKED in MV3 service workers. The SW silently
// chunk file is loaded but the await never resolves, so production // dies — the chunk file is loaded but the await never resolves, so
// listeners never register. Production sources: // production listeners never register. Production sources:
// - w3c/webextensions#212 (May 2022, still open) // - w3c/webextensions#212 (May 2022, still open as of May 2026)
// - chromium.googlesource.com es_modules.md: "Dynamic import is // - chromium.googlesource.com es_modules.md: "Dynamic import is
// currently blocked in Service Workers, but it will change in // currently blocked in Service Workers, but it will change in
// the future." // the future."
// The prototype WORKS AROUND this by: // Approach B WORKS AROUND this by:
// 1. Removing the SW-side gated dynamic import entirely. // 1. Removing the SW-side gated dynamic import entirely
// (src/background/index.ts has comment-only docs at lines 13-30
// explaining why no hook gate lands here).
// 2. Using only the OFFSCREEN-side test hook (offscreen IS a DOM // 2. Using only the OFFSCREEN-side test hook (offscreen IS a DOM
// document, dynamic import works there). // document, dynamic import works there — see
// src/offscreen/recorder.ts top-of-module gate).
// 3. Driving everything from this harness page using PRODUCTION // 3. Driving everything from this harness page using PRODUCTION
// chrome.* APIs (page has full extension permissions): // chrome.* APIs (page has full extension permissions):
// - chrome.action.getBadgeText / getPopup — read SW state // - chrome.action.getBadgeText / getPopup — read SW state
// - chrome.offscreen.createDocument — create offscreen FIRST // - chrome.offscreen.createDocument — create offscreen FIRST
// (the page is allowed to call this) // (the page is allowed to call this)
// - chrome.runtime.sendMessage REQUEST_PERMISSIONS — trigger // - chrome.runtime.sendMessage START_RECORDING — trigger
// production startRecording path (uses existing offscreen + // production startRecording path (uses existing offscreen +
// fake getDisplayMedia) // fake getDisplayMedia from offscreen-hooks.ts)
// - chrome.notifications.getAll — count active notifications // - chrome.notifications.getAll — count active notifications
// (no SW hook needed) // (no SW hook needed)
// - chrome.runtime.sendMessage __mokoshOffscreenQuery // - chrome.runtime.sendMessage __mokoshOffscreenQuery
// dispatch-ended — trigger Bug B simulation via offscreen // dispatch-ended — trigger Bug B simulation via offscreen
// bridge (offscreen still uses dynamic import → works) // bridge (offscreen still uses dynamic import → works)
// //
// The page exposes `window.__mokoshHarness` with one method: // Wave 1 surface — the page exposes `window.__mokoshHarness` with one
// - `assertA6()` — runs the canonical Bug B regression assertion // method (assertA6); Wave 3 extends to all 13 assertions:
// end-to-end and returns a structured pass/fail record. // - `assertA6()` — canonical Bug B regression assertion (proven).
/** /**
* Result shape returned by harness assertions to Puppeteer. * Result shape returned by harness assertions to Puppeteer.

View File

@@ -33,10 +33,15 @@
// with it disabled, the SW initializes and chrome.runtime.onMessage // with it disabled, the SW initializes and chrome.runtime.onMessage
// handlers respond. See Plan 01-11 PROTOTYPE research session. // handlers respond. See Plan 01-11 PROTOTYPE research session.
// //
// PROTOTYPE addition: the prototype harness page at // Plan 01-13 Wave 1: the extension-internal harness page at
// `tests/uat/prototype/extension-page-harness.html` is added as a // `tests/uat/extension-page-harness.html` is added as a Rollup input
// Rollup input so the test build emits it. Production builds do NOT // so the test build emits it under that path in `dist-test/`. The
// include the prototype page (vite.config.ts has no such input). // Puppeteer driver navigates the in-browser tab to
// `chrome-extension://<id>/tests/uat/extension-page-harness.html` and
// invokes `window.__mokoshHarness.*` from the host side via CDP. The
// page itself has full chrome.* extension privileges (Approach B
// architectural anchor). Production builds (vite.config.ts) do NOT
// include this input — the page ships only in the test bundle.
// //
// References: // References:
// - Vite mergeConfig: https://vite.dev/guide/api-javascript.html#mergeconfig // - Vite mergeConfig: https://vite.dev/guide/api-javascript.html#mergeconfig
@@ -76,10 +81,13 @@ export default defineConfig(({ command, mode }) =>
modulePreload: { polyfill: false }, modulePreload: { polyfill: false },
rollupOptions: { rollupOptions: {
input: { input: {
// Add the prototype harness page so it lands in dist-test/ // Plan 01-13 Wave 1: emit the extension-internal harness
// and becomes reachable as // page at its production path so it becomes reachable as
// chrome-extension://<id>/tests/uat/prototype/extension-page-harness.html // chrome-extension://<id>/tests/uat/extension-page-harness.html
prototype_harness: 'tests/uat/prototype/extension-page-harness.html', // The crxjs vite plugin will copy this HTML into dist-test/
// and rewrite the <script type="module" src> reference to
// the bundled chunk's hashed filename.
extension_page_harness: 'tests/uat/extension-page-harness.html',
}, },
}, },
}, },