Files
mokosh/tests/uat/lib/harness-page-driver.ts
Mark eb64521321 feat(01-13): wave-2 — launchHarnessBrowser + assertions + harness-page-driver scaffolding
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>
2026-05-18 15:21:11 +02:00

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`);
}