diff --git a/tests/uat/prototype/a6.test.ts b/tests/uat/a6.test.ts similarity index 82% rename from tests/uat/prototype/a6.test.ts rename to tests/uat/a6.test.ts index 3238188..d20d800 100644 --- a/tests/uat/prototype/a6.test.ts +++ b/tests/uat/a6.test.ts @@ -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 -// architecture: extension-internal test page + chrome.runtime.sendMessage -// bridge + synthetic MediaStream. Runs ONE end-to-end assertion: A6 -// (Bug B canonical) — when the offscreen recorder fires -// RECORDING_ERROR{error: 'user-stopped-sharing'} (simulated via -// dispatchEvent('ended')), the SW state machine routes through -// setIdleMode (NOT setErrorMode), badge becomes empty, popup empties, -// isRecording=false, NO recovery notification fires. +// Puppeteer-driven single-assertion driver for A6 (Bug B canonical). +// Originally landed as the Plan 01-11 prototype at commit c647f61; +// Plan 01-13 Wave 1 promoted this file from `tests/uat/prototype/` to +// the production path without behavioral change. Wave 2 will refactor +// the launch + console-capture + result-print plumbing into reusable +// lib helpers (`tests/uat/lib/{launch,assertions,harness-page-driver} +// .ts`) and rewrite this driver against them; Wave 3 folds A6 into +// `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 -// can re-spawn 01-11 executor with new brief. FAIL = architectural -// blocker(s) remain → falls back to Option B (partial coverage) or -// Option C (operator UAT). +// Assertion contract — A6 (Bug B canonical): when the offscreen +// recorder fires RECORDING_ERROR{error: 'user-stopped-sharing'} +// (simulated via dispatchEvent('ended') on the active video track per +// 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: -// tsx tests/uat/prototype/a6.test.ts -// HEADLESS=0 tsx tests/uat/prototype/a6.test.ts # debug view +// tsx tests/uat/a6.test.ts +// HEADLESS=0 tsx tests/uat/a6.test.ts # debug view // // Pre-flight: requires `dist-test/` from `npm run build:test`. The test // 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'; +// 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 REPO_ROOT = resolvePath(HARNESS_FILE_DIR, '..', '..', '..'); +const REPO_ROOT = resolvePath(HARNESS_FILE_DIR, '..', '..'); const DIST_TEST_DIR = resolvePath(REPO_ROOT, 'dist-test'); /** Per-check record returned by the harness page. */ @@ -130,7 +144,7 @@ async function launchChrome(): Promise<{ function printResult(result: HarnessAssertionResult): void { process.stdout.write('\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`); if (result.error !== undefined) { 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. */ async function main(): Promise { - 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('='.repeat(72) + '\n'); @@ -173,7 +187,7 @@ async function main(): Promise { try { // Open the prototype harness page. The page lives at the test-build // 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`); // Open a 'victim' page first — production code calls diff --git a/tests/uat/prototype/extension-page-harness.html b/tests/uat/extension-page-harness.html similarity index 89% rename from tests/uat/prototype/extension-page-harness.html rename to tests/uat/extension-page-harness.html index 8535195..6b6e1db 100644 --- a/tests/uat/prototype/extension-page-harness.html +++ b/tests/uat/extension-page-harness.html @@ -6,7 +6,7 @@

Mokosh UAT — extension-internal page harness

-

This page lives at chrome-extension://<id>/tests/uat/prototype/extension-page-harness.html.

+

This page lives at chrome-extension://<id>/tests/uat/extension-page-harness.html.

Puppeteer navigates a tab here and drives assertions via window.__mokoshHarness.*.

Ready.
diff --git a/tests/uat/prototype/extension-page-harness.ts b/tests/uat/extension-page-harness.ts similarity index 86% rename from tests/uat/prototype/extension-page-harness.ts rename to tests/uat/extension-page-harness.ts index 14b7a84..f5cacb6 100644 --- a/tests/uat/prototype/extension-page-harness.ts +++ b/tests/uat/extension-page-harness.ts @@ -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 -// `chrome-extension:///tests/uat/prototype/extension-page-harness.html` +// `chrome-extension:///tests/uat/extension-page-harness.html` // in the test build (vite.test.config.ts adds it as a Rollup input). // -// PURPOSE: prove the orchestrator's hypothesis — that the working -// architecture for MV3 extension UAT is to drive Chrome FROM INSIDE -// (extension-internal test page + synthetic MediaStream) rather than -// FROM OUTSIDE (CDP into SW context). +// ARCHITECTURAL ANCHOR (Approach B): the working architecture for MV3 +// extension UAT is to drive Chrome FROM INSIDE (extension-internal +// test page + synthetic MediaStream) rather than FROM OUTSIDE (CDP +// 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): -// The Plan 01-11 RESEARCH §6 architecture used `await import(...)` +// IMPORTANT RESEARCH FINDING (load-bearing — DO NOT REGRESS): +// Plan 01-11 RESEARCH §6 originally architected `await import(...)` // at the top of src/background/index.ts to gate SW-side test hooks. -// EMPIRICAL: dynamic import is BLOCKED in MV3 service workers -// (Chrome 148, verified via probe). The SW silently dies — the -// chunk file is loaded but the await never resolves, so production -// listeners never register. Production sources: -// - w3c/webextensions#212 (May 2022, still open) +// EMPIRICAL FALSIFICATION (01-11 prototype, verified Chrome 148): +// dynamic import is BLOCKED in MV3 service workers. The SW silently +// dies — the chunk file is loaded but the await never resolves, so +// production listeners never register. Production sources: +// - w3c/webextensions#212 (May 2022, still open as of May 2026) // - chromium.googlesource.com es_modules.md: "Dynamic import is // currently blocked in Service Workers, but it will change in // the future." -// The prototype WORKS AROUND this by: -// 1. Removing the SW-side gated dynamic import entirely. +// Approach B WORKS AROUND this by: +// 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 -// 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 // chrome.* APIs (page has full extension permissions): // - chrome.action.getBadgeText / getPopup — read SW state // - chrome.offscreen.createDocument — create offscreen FIRST // (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 + -// fake getDisplayMedia) +// fake getDisplayMedia from offscreen-hooks.ts) // - chrome.notifications.getAll — count active notifications // (no SW hook needed) // - chrome.runtime.sendMessage __mokoshOffscreenQuery // dispatch-ended — trigger Bug B simulation via offscreen // bridge (offscreen still uses dynamic import → works) // -// The page exposes `window.__mokoshHarness` with one method: -// - `assertA6()` — runs the canonical Bug B regression assertion -// end-to-end and returns a structured pass/fail record. +// Wave 1 surface — the page exposes `window.__mokoshHarness` with one +// method (assertA6); Wave 3 extends to all 13 assertions: +// - `assertA6()` — canonical Bug B regression assertion (proven). /** * Result shape returned by harness assertions to Puppeteer. diff --git a/vite.test.config.ts b/vite.test.config.ts index 05539c5..3216ca6 100644 --- a/vite.test.config.ts +++ b/vite.test.config.ts @@ -33,10 +33,15 @@ // with it disabled, the SW initializes and chrome.runtime.onMessage // handlers respond. See Plan 01-11 PROTOTYPE research session. // -// PROTOTYPE addition: the prototype harness page at -// `tests/uat/prototype/extension-page-harness.html` is added as a -// Rollup input so the test build emits it. Production builds do NOT -// include the prototype page (vite.config.ts has no such input). +// Plan 01-13 Wave 1: the extension-internal harness page at +// `tests/uat/extension-page-harness.html` is added as a Rollup input +// so the test build emits it under that path in `dist-test/`. The +// Puppeteer driver navigates the in-browser tab to +// `chrome-extension:///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: // - Vite mergeConfig: https://vite.dev/guide/api-javascript.html#mergeconfig @@ -76,10 +81,13 @@ export default defineConfig(({ command, mode }) => modulePreload: { polyfill: false }, rollupOptions: { input: { - // Add the prototype harness page so it lands in dist-test/ - // and becomes reachable as - // chrome-extension:///tests/uat/prototype/extension-page-harness.html - prototype_harness: 'tests/uat/prototype/extension-page-harness.html', + // Plan 01-13 Wave 1: emit the extension-internal harness + // page at its production path so it becomes reachable as + // chrome-extension:///tests/uat/extension-page-harness.html + // The crxjs vite plugin will copy this HTML into dist-test/ + // and rewrite the