feat(01-13): wave-1 — promote c647f61 prototype to production paths; A6 GREEN
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: `<p>` 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://<id>/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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
@@ -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://<id>/tests/uat/prototype/extension-page-harness.html</code>.</p>
|
<p>This page lives at <code>chrome-extension://<id>/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>
|
||||||
@@ -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.
|
||||||
@@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user