Build out the Approach-B harness driver utilities atop the Wave 1
production paths. Three new files form the shared scaffold that
Wave 3's 13 assertion drivers (A1-A5, A7-A13) and the eventual
orchestrator (`tests/uat/harness.test.ts`) will all consume. The
standalone A6 driver (`tests/uat/a6.test.ts`) is rewritten to use
the new lib — behavior-preserving: A6 still PASSES 5/5 in ~7s.
New files:
- tests/uat/lib/launch.ts (~320 LoC)
`launchHarnessBrowser({ headless?, downloadsDir? }) → HarnessHandles`
Extracts the Chrome-launch + victim-page + harness-page + console-
attach pattern from a6.test.ts into a single reusable helper.
NEW vs prototype: CDP `Browser.setDownloadBehavior` wires
Chrome's download path to a per-run `mkdtempSync` tmp dir so A5
(SAVE_ARCHIVE) can poll a known location without colliding with
the operator's real downloads. Architectural commitments
enforced (per 01-11-SUMMARY): no `--auto-select-desktop-capture-
source` flag; victim about:blank brought to front for the
production `chrome.tabs.query({active:true})` workaround; SW
console attach best-effort with bounded poll; offscreen console
attach opportunistic via `targetcreated` listener (offscreen
target appears later, when the harness page calls
chrome.offscreen.createDocument).
- tests/uat/lib/assertions.ts (~210 LoC)
Host-side assertion primitives:
* `AssertionRecord`, `CheckRecord`, `ConsoleBuffers` types —
mirror the page-side shape returned by `assertA*` methods.
* `runAssertion(name, fn, buffers)` — try/catch wrapper that
dumps the SW + offscreen console tails (last 100 lines each)
to stderr on failure, then returns `{passed: false, error}`
if `fn` throws.
* `printAssertionResult(result)` — single source of truth for
the formatted result print. Extracted from the inline
`printResult` previously in the prototype's a6.test.ts so
Wave 3's orchestrator can reuse it across all 14 assertions.
* `assertEqual / assertGte / assertMatch / assertTrue` —
structured failure messages atop node:assert/strict.
* `waitFor(probe, predicate, timeoutMs, description)` — host-
side polling primitive; mirrors the page-side waitFor
semantics verbatim (they can't share a module: page-side is
bundled into the harness HTML, host-side runs in Node).
NO chrome.* helpers here — all chrome.* work happens inside the
extension-internal harness page. This module is host-side ONLY
by construction (no chrome global in Node anyway).
- tests/uat/lib/harness-page-driver.ts (~170 LoC)
One driver wrapper per assertion (A1..A13). Each wraps a single
`page.evaluate(() => window.__mokoshHarness.assertXX())`.
Centralizing this means adding/renaming an assertion = two-file
edit (extension-page-harness.ts impl + this file) instead of
touching every test-file caller.
Wave 2 wires `driveA6` (proven from c647f61). The 12 Wave-3
drivers (driveA1..A5, A7..A13) are stubbed as
`throw new Error('NOT YET IMPLEMENTED — Wave 3<X> wires driveXX')`
so the future orchestrator's `for (const drive of drivers)` loop
fails cleanly on the first unimplemented one (bail-on-first-
failure semantics). The `AssertionWithBytes` type is declared
for A5/A12/A13 which return `bytesBase64` payloads (zip / webm
bytes that the host side processes after the page-side
assertion completes).
Rewrite — `tests/uat/a6.test.ts`:
- Drops ~80 LoC of Chrome-launch + console-attach + result-print
plumbing now living in lib/launch.ts + lib/assertions.ts.
- Now ~70 LoC total — pure orchestration of
launchHarnessBrowser → runAssertion(driveA6) → printAssertionResult
→ browser.close() → exit code.
- Behavior-preserving: A6 still 5/5 GREEN with the same diagnostic
output (SETUP, A6.1-A6.4) and the same ~7s end-to-end runtime.
Verification (all GREEN):
- `npx tsc --noEmit` — exit 0 (root + tests/uat/tsconfig.json).
- `npx tsx tests/uat/a6.test.ts` — exits 0 with "PASS"; 5 checks
GREEN (SETUP, A6.1, A6.2, A6.3, A6.4). End-to-end runtime ~7s
headless on this workstation.
- `npm run build` — exit 0; Tier-1 grep gate GREEN (production
bundle contains zero hook strings AND zero lib symbol names —
the new lib files are test-only and not bundled into dist/).
- `npm run build:test` — exit 0; dist-test/ still emits the
extension-page-harness.html harness (lib files are host-side,
not rollup inputs).
- `npx vitest run` — 92/92 GREEN.
Wave 3 ready: harness-page-driver.ts has driveA1..A5/A7..A13 stubs
in place; extending requires only:
1. Add `assertAXX` method to window.__mokoshHarness in
tests/uat/extension-page-harness.ts.
2. Replace the corresponding stub body in this file with the
page.evaluate wrapper.
3. (Wave 3A) Create tests/uat/harness.test.ts orchestrator that
iterates over [A0 grep gate, driveA1..A13] with bail-on-fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
182 lines
7.0 KiB
TypeScript
182 lines
7.0 KiB
TypeScript
// tests/uat/lib/harness-page-driver.ts — Plan 01-13 Wave 2.
|
|
//
|
|
// Driver wrappers — one per assertion (A1..A13). Each wraps a single
|
|
// `page.evaluate(() => window.__mokoshHarness.assertXX())` call,
|
|
// returning the structured AssertionRecord (or the extended shape with
|
|
// `bytesBase64` for A5/A12/A13 which return host-side-required payloads
|
|
// like the downloaded zip bytes or the recorded webm bytes).
|
|
//
|
|
// Centralizing the page.evaluate call here means adding or renaming an
|
|
// assertion requires a two-file edit:
|
|
// 1. extension-page-harness.ts — page-side impl + window.__mokoshHarness wire
|
|
// 2. this file — host-side driver wrapper
|
|
// instead of touching every test-file that calls the assertion.
|
|
//
|
|
// Wave 2 ONLY wires `driveA6` (the proven assertion from the c647f61
|
|
// prototype). The 12 Wave-3 assertions are stubbed as `throw new
|
|
// Error('NOT YET IMPLEMENTED — Wave 3<X> wires this')` so the
|
|
// orchestrator's `for (const drive of drivers)` loop fails cleanly on
|
|
// the first unimplemented one (bail-on-first-failure semantics in
|
|
// `harness.test.ts` lands in Wave 3A).
|
|
//
|
|
// References:
|
|
// - puppeteer Page.evaluate:
|
|
// https://pptr.dev/api/puppeteer.page.evaluate
|
|
|
|
import type { Page } from 'puppeteer';
|
|
import type { AssertionRecord, CheckRecord } from './assertions';
|
|
|
|
/**
|
|
* Extended assertion-record shape for A5/A12/A13 which return
|
|
* host-side-required binary payloads:
|
|
* - A5 (SAVE_ARCHIVE): `bytesBase64` is the downloaded zip bytes
|
|
* (read by host-side from `handles.downloadsDir`); page side only
|
|
* returns the trigger ack.
|
|
* - A12 (ffprobe): `bytesBase64` is the recorded webm bytes —
|
|
* extracted from the zip by the host so ffprobe (host-side binary)
|
|
* can analyze it.
|
|
* - A13 (zip shape): `bytesBase64` is the zip bytes; `expectedVersion`
|
|
* is the manifest version the harness was built against.
|
|
*
|
|
* All Wave-3 assertions; not used in Wave 2.
|
|
*/
|
|
export interface AssertionWithBytes {
|
|
readonly passed: boolean;
|
|
readonly name: string;
|
|
readonly checks: ReadonlyArray<CheckRecord>;
|
|
readonly diagnostics: ReadonlyArray<string>;
|
|
readonly error?: string;
|
|
readonly bytesBase64?: string;
|
|
readonly expectedVersion?: string;
|
|
}
|
|
|
|
/** Marker error message for unimplemented Wave-3 drivers — orchestrator
|
|
* matches on this prefix to format the diagnostic distinctly from a
|
|
* genuine assertion failure. */
|
|
const WAVE3_STUB_PREFIX = 'NOT YET IMPLEMENTED';
|
|
|
|
/**
|
|
* Drive the A6 (Bug B canonical) assertion. The proven, prototype-
|
|
* inherited driver. Page side does all orchestration (ensureOffscreen +
|
|
* start + wait + dispatch + assert); host side just triggers + reads
|
|
* the result.
|
|
*
|
|
* @param page - The harness page (from `launchHarnessBrowser`).
|
|
* @returns Structured AssertionRecord with 5 checks (SETUP + A6.1..A6.4).
|
|
*/
|
|
export async function driveA6(page: Page): Promise<AssertionRecord> {
|
|
return await page.evaluate(async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
|
|
const harness = (window as any).__mokoshHarness;
|
|
const r: AssertionRecord = await harness.assertA6();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/* ─── Wave 3A — NOT YET IMPLEMENTED ──────────────────────────────── */
|
|
|
|
/**
|
|
* Drive A1 (SW bootstrap state). Wave 3A wires this.
|
|
* @throws Always — replace stub when Wave 3A lands.
|
|
*/
|
|
export async function driveA1(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3A wires driveA1`);
|
|
}
|
|
|
|
/**
|
|
* Drive A2 (toolbar onClicked → REC). Wave 3A wires this.
|
|
* @throws Always — replace stub when Wave 3A lands.
|
|
*/
|
|
export async function driveA2(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3A wires driveA2`);
|
|
}
|
|
|
|
/**
|
|
* Drive A3 (displaySurface monitor). Wave 3A wires this.
|
|
* @throws Always — replace stub when Wave 3A lands.
|
|
*/
|
|
export async function driveA3(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3A wires driveA3`);
|
|
}
|
|
|
|
/**
|
|
* Drive A4 (popup during recording). Wave 3A wires this.
|
|
* @throws Always — replace stub when Wave 3A lands.
|
|
*/
|
|
export async function driveA4(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3A wires driveA4`);
|
|
}
|
|
|
|
/* ─── Wave 3B — NOT YET IMPLEMENTED ──────────────────────────────── */
|
|
|
|
/**
|
|
* Drive A5 (SAVE_ARCHIVE download). Wave 3B wires this; signature will
|
|
* take a second `downloadsDir` parameter so the host side can poll
|
|
* for the dropped zip file.
|
|
*
|
|
* @throws Always — replace stub when Wave 3B lands.
|
|
*/
|
|
export async function driveA5(_page: Page): Promise<AssertionWithBytes> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3B wires driveA5`);
|
|
}
|
|
|
|
/**
|
|
* Drive A7 (genuine error → ERR + recovery notification). Wave 3B wires.
|
|
* @throws Always — replace stub when Wave 3B lands.
|
|
*/
|
|
export async function driveA7(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3B wires driveA7`);
|
|
}
|
|
|
|
/* ─── Wave 3C — NOT YET IMPLEMENTED ──────────────────────────────── */
|
|
|
|
/**
|
|
* Drive A8 (Bug A onStartup → notification creates). Wave 3C wires.
|
|
* @throws Always — replace stub when Wave 3C lands.
|
|
*/
|
|
export async function driveA8(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3C wires driveA8`);
|
|
}
|
|
|
|
/**
|
|
* Drive A9 (icon file sizes). Wave 3C wires.
|
|
* @throws Always — replace stub when Wave 3C lands.
|
|
*/
|
|
export async function driveA9(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3C wires driveA9`);
|
|
}
|
|
|
|
/**
|
|
* Drive A10 (manifest shape). Wave 3C wires.
|
|
* @throws Always — replace stub when Wave 3C lands.
|
|
*/
|
|
export async function driveA10(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3C wires driveA10`);
|
|
}
|
|
|
|
/* ─── Wave 3D — NOT YET IMPLEMENTED ──────────────────────────────── */
|
|
|
|
/**
|
|
* Drive A11 (35s → ≥3 segments). Wave 3D wires.
|
|
* @throws Always — replace stub when Wave 3D lands.
|
|
*/
|
|
export async function driveA11(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3D wires driveA11`);
|
|
}
|
|
|
|
/**
|
|
* Drive A12 (ffprobe — host-side returns webm bytes). Wave 3D wires.
|
|
* @throws Always — replace stub when Wave 3D lands.
|
|
*/
|
|
export async function driveA12(_page: Page): Promise<AssertionWithBytes> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3D wires driveA12`);
|
|
}
|
|
|
|
/**
|
|
* Drive A13 (zip structure + meta.json). Wave 3D wires.
|
|
* @throws Always — replace stub when Wave 3D lands.
|
|
*/
|
|
export async function driveA13(_page: Page): Promise<AssertionWithBytes> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3D wires driveA13`);
|
|
}
|