From eb2258a88007cb9bbadaecd38d29a42e3fe95829 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 18 May 2026 15:01:58 +0200 Subject: [PATCH] =?UTF-8?q?feat(01-13):=20wave-1=20=E2=80=94=20promote=20c?= =?UTF-8?q?647f61=20prototype=20to=20production=20paths;=20A6=20GREEN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the three load-bearing prototype files from `tests/uat/prototype/` to their production paths under `tests/uat/`, leaving the architectural narrative (research findings, BLOCKER citations, falsification table references) intact. No behavioral changes — A6 still PASSES 5/5 in ~7s end-to-end from the new paths. File moves (git mv preserves history): - tests/uat/prototype/extension-page-harness.html → tests/uat/extension-page-harness.html - tests/uat/prototype/extension-page-harness.ts → tests/uat/extension-page-harness.ts - tests/uat/prototype/a6.test.ts → tests/uat/a6.test.ts The `tests/uat/prototype/` directory is now empty (git does not track empty directories; will not appear in subsequent `git status`). Path-reference updates inside the moved files: - tests/uat/extension-page-harness.html: `

` line referencing the chrome-extension:// URL updated to drop `/prototype/`. - tests/uat/extension-page-harness.ts: file-header docstring rewritten to cite Plan 01-13 / Approach B / inheritance from c647f61. The load-bearing architectural-finding comment block (MV3 SW dynamic- import block falsification, Approach-B chrome.* surface summary) is REWORDED but its semantic content + research citations are PRESERVED — every load-bearing fact survives the rename. - tests/uat/a6.test.ts: * File-header rewritten to position the file as Plan 01-13's standalone single-assertion entry point (preserves the future- proof rationale: this entry stays around forever for fast TDD iteration on A6 even after Wave 3 folds A6 into the orchestrator harness.test.ts). * REPO_ROOT resolvePath chain corrected from `..,..,..` to `..,..` — the file is now two directory levels above the repo root instead of three. Without this fix DIST_TEST_DIR would resolve to a path one level above the actual repo root and assertBundlePresent would throw. **VERIFIED by running the driver: build path resolves correctly.** * harnessUrl constant updated to drop `/prototype/` from the chrome-extension:///tests/uat/extension-page-harness.html URL — must match the rollup emission path in dist-test/. * Stdout labels updated: 'PROTOTYPE A6 result' → 'A6 result', 'Plan 01-11 PROTOTYPE — A6 ... feasibility test' → 'Plan 01-13 — A6 (Bug B canonical) standalone driver'. Inside the docstrings the historical 'originally landed as 01-11 prototype' provenance is preserved per the plan's contract. vite.test.config.ts: - `rollupOptions.input` renamed `prototype_harness` → `extension_page_harness` pointing at the new production path. crxjs emits the harness HTML to `dist-test/tests/uat/extension-page-harness.html` (verified by `ls dist-test/tests/uat/`). - The `modulePreload: { polyfill: false }` line is PRESERVED — this is the CRITICAL SW FIX per 01-11-SUMMARY (disabling the polyfill is what makes the test bundle's offscreen-side dynamic import work without crashing in non-DOM contexts that incorrectly try to call document.querySelector). - File-header comment §4 and the inline `define.__MOKOSH_UAT__` comment are PRESERVED — load-bearing rationale for the dedicated build-time token (vs `import.meta.env.MODE === 'test'` which collides with vitest). Verification (all GREEN): - `npm run build:test` — exit 0; dist-test/ emits `tests/uat/extension-page-harness.html` and `assets/extension_page_harness-*.js`. - `npx tsx tests/uat/a6.test.ts` — exits 0 with "A6 result: PASS"; 5/5 checks GREEN (SETUP: badge becomes REC; A6.1 badge==''; A6.2 popup==''; A6.3 notif delta==0; A6.4 isRecording=false). End-to-end runtime ~7s headless on this workstation. - `npx tsc --noEmit` — exit 0 (root tsconfig + tests/uat/tsconfig.json). - `npx vitest run` — 92/92 GREEN; the moves do not touch any vitest- discovered files. - `npm run build` — exit 0; Tier-1 grep gate stays GREEN (the moves do not touch production code). Wave 2 (next): build out `tests/uat/lib/{launch,assertions,harness-page- driver}.ts` around the extension-page architecture; rewrite `tests/uat/a6.test.ts` to use the shared lib (still PASSES 5/5). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/uat/{prototype => }/a6.test.ts | 52 +++++++++++------- .../extension-page-harness.html | 2 +- .../{prototype => }/extension-page-harness.ts | 55 ++++++++++++------- vite.test.config.ts | 24 +++++--- 4 files changed, 84 insertions(+), 49 deletions(-) rename tests/uat/{prototype => }/a6.test.ts (82%) rename tests/uat/{prototype => }/extension-page-harness.html (89%) rename tests/uat/{prototype => }/extension-page-harness.ts (86%) 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