Commit Graph

6 Commits

Author SHA1 Message Date
2b6c24b2d9 feat(01-13): A14 — post-SAVE state check (badge='', popup='', no new recovery notif)
Plan 01-13 Task 9 closure for operator empirical UAT bug
.planning/debug/01-09-save-stops-recording.md. Adds the harness
assertion that empirically verifies the SAVE-auto-stops-recording fix
(committed at 4f4c3e2) holds end-to-end through a real Chrome instance
+ real MediaRecorder + real chrome.action + real chrome.notifications.

A14 design (read-only post-state check):
  1. Snapshot active mokosh-recovery-* notification ids (delta baseline).
  2. Settle 500ms for the post-A13 SAVE finally block to land.
  3. Read chrome.action.getBadgeText/getPopup + getActiveNotificationIds.
  4. Assert badge='', popup='', recoveryDelta=0 — three checks total.

A14 chains off A13's SAVE_ARCHIVE (which under the new fix auto-stops
the recording per SPEC one-shot intent). A14 does NOT dispatch its own
SAVE — A13's SAVE is the event A14 observes the post-state of. This
keeps the harness wall-clock minimal (~500ms added for A14, no
additional 11s segment-settle).

Amendment to A13: now does setupFreshRecording + 11s segment-settle
BEFORE its own SAVE_ARCHIVE dispatch. Under the new fix, A12's
SAVE_ARCHIVE stopped the recording — without this A13 would dispatch
against an empty buffer and fail with EmptyVideoBufferError. The
amendment adds ~11s to harness wall-clock; acceptable given the SPEC
SAVE=stop contract is now load-bearing.

A14 contract notes per orchestrator simpler-design recommendation:
  - direct isRecording proxy check skipped (no bridge op exposes it;
    transitively verified via badge='' — production state machine
    pairs isRecording transitions with badge transitions atomically)
  - recovery-notif check is delta-based (A7 left a mokosh-recovery-*
    in the active set; we verify A13's SAVE did NOT add another one)

Files modified:
  - tests/uat/extension-page-harness.ts: +assertA14 (~110 lines) +
    A13 amended with setupFreshRecording + 11s settle + 1 new
    SETUP check + window.__mokoshHarness export wire
  - tests/uat/lib/harness-page-driver.ts: +driveA14 wrapper
  - tests/uat/harness.test.ts: +A14 in drivers array + header doc +
    total 14/14 → 15/15 + import line

Verification:
  - npm run test:uat: 15/15 GREEN (was 14/14)
  - npx tsc --noEmit: exit 0 (no type errors)
  - npm run build: exit 0 (production bundle clean)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:31:13 +02:00
d793c9e1e5 feat(01-13): wave-3D — A11+A12+A13 GREEN + get-segment-count bridge op; 14/14 GREEN
Lands the final three UAT-harness assertions. All 14 assertions (A0..A13)
now GREEN against the current bundle; `npm run test:uat` exits 0 in ~70s
wall-clock (35s of which is A11's mandatory continuity wait).

Assertions wired:

 - A11 — 35s buffer continuity → segments.length >= 3. Tears down any prior
   recording (STOP_RECORDING → START_RECORDING so the recorder's
   `resetBuffer` at start clears segments). Waits 35_000ms wall-clock with
   intermittent SW keepalive PINGs every 20s (belt-and-suspenders over the
   offscreen recorder's own keepalive port). Queries the new
   `get-segment-count` bridge op. Asserts count >= 3 (per D-13:
   SEGMENT_DURATION_MS=10s × MAX_SEGMENTS=3).

 - A12 — SAVE_ARCHIVE produces zip; webm passes ffprobe. Page side
   dispatches SAVE_ARCHIVE (recording from A11 still alive). Host side
   polls `downloadsDir` for the new/updated zip (overwrite-aware mtime
   delta — the CDP-routed downloads pattern OVERWRITES `download.zip`
   rather than numbering it, empirically verified during initial RED).
   Extracts `video/last_30sec.webm` via JSZip to a tmpfile. Runs
   `/usr/bin/ffprobe -v error -f matroska <path>`; asserts exit 0 + clean
   stderr. Three skip-gates: (i) ffprobe binary absent → SKIPPED; (ii)
   webm < 10_240B (synthetic-stream-limitation signature — canvas
   captureStream in `--headless=new` offscreen produces 0-frame WebM
   with only EBML/Track headers) → SKIPPED with explicit diagnostic
   pointing operators to `tests/offscreen/webm-playback.test.ts` as the
   primary defense for the codec/remux contract; (iii) happy path →
   strict ffprobe gate (will fire RED on remux/codec regressions when
   operators run HEADLESS=0 with a real screen-share grant). A12's
   role as "belt + suspenders" is documented inline + framed by Plan
   01-13 Task 7 behavior block.

 - A13 — Zip structure + meta.json shape. Second SAVE_ARCHIVE (verifies
   idempotency over A12's first save). JSZip parse via the
   `assertArchiveShape` helper (extended in this wave to read
   `extensionVersion` — the actual production SessionMetadata field
   name per src/shared/types.ts:103, vs. the earlier 01-11 prototype's
   incorrect `version` assumption). Six checks: SW dispatch ack, zip
   arrival, webm entry present, webm size > 1024B, meta.json entry
   present, meta.json.extensionVersion matches
   chrome.runtime.getManifest().version (captured once at orchestrator
   startup via the new page-side getManifestVersion helper).

Bridge op + recorder wire:

 - Adds `get-segment-count` op to the offscreen-hooks
   `__mokoshOffscreenQuery` chrome.runtime.onMessage handler — returns
   `{count: number}` via the existing segmentCountGetter closure
   (segments.length captured at recorder.ts:284 inside startRecording;
   the getter binding survives multiple START/STOP cycles via the
   module-level let segments array).

 - Adds `get-segment-count` to FORBIDDEN_HOOK_STRINGS in BOTH gate
   files: `tests/background/no-test-hooks-in-prod-bundle.test.ts`
   (Tier-1 unit gate; 9 → 10 entries; vitest 93 → 94 GREEN) and
   `tests/uat/harness.test.ts:assertA0_GrepGate` (UAT-level mirror).
   Production bundle remains hook-free (0 occurrences in dist/ after
   `npm run build` — verified).

Harness surface:

 - `tests/uat/extension-page-harness.ts` extends `window.__mokoshHarness`
   from 10 → 13 assertion methods + 1 helper:
   `assertA11, assertA12, assertA13, getManifestVersion`. Adds
   `teardownAndStartFreshRecording` helper for A11's clean-slate
   contract.

 - `tests/uat/lib/harness-page-driver.ts` retires the Wave-3 stub
   marker (no more NYI throws). Adds `driveA11` (standard wrapper),
   `driveA12` + `driveA13` (heavyweight host-side drivers with fs
   polling + JSZip + ffprobe). Adds `pollForNewOrUpdatedZip` which
   detects both new files AND overwrites via mtime delta — fixes the
   `download.zip` overwrite blindness that turned A12 RED on first run
   (driveA5's name-only filter wasn't reused).

 - `tests/uat/lib/zip.ts` updates `assertArchiveShape` to read
   `extensionVersion` (the production field name per
   src/shared/types.ts:103); adds the A13_MIN_VIDEO_BYTES=1024 floor
   constant.

 - `tests/uat/harness.test.ts` orchestrator wires the three new
   drivers + the per-run manifest-version capture for A13.

Baseline:

 - `npx tsc --noEmit`: exit 0.
 - `npm run build`: exit 0; production bundle clean of all 10 hook
   strings (verified by grep).
 - `npm run build:test`: exit 0; test bundle ships `get-segment-count`.
 - `npx vitest run`: 94/94 GREEN (was 93; +1 from the new gate string).
 - `npm run test:uat`: 14/14 GREEN; wall-clock ~70s (35s A11 wait +
   2× ~13s save settles + ~10s production rebuild + overhead).

A11 RED-on-regression demo (documented per acceptance-criteria
"at least 1 of 3"):

  Edit src/offscreen/recorder.ts:52: `SEGMENT_DURATION_MS = 10_000`
  → `SEGMENT_DURATION_MS = 30_000`. Rebuild dist-test. Re-run UAT.
  A11 FAILS (only 1 segment rotates in 35s, vs floor of 3). Revert
  the edit; A11 PASSES. The harness empirically catches regressions
  that lengthen the rotation cadence beyond the 30s ring window —
  the canonical D-13 contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:24:39 +02:00
b665919c5f feat(01-13): wave-3C — A8+A9+A10 GREEN + Bug A canonical regression rewind
Plan 01-13 Task 6 (Wave 3C). Wires the final three Wave-3 assertions
before A11+A12+A13 (Wave 3D — 35s segments / ffprobe / zip shape):

- A8 (Bug A canonical regression rewind) — invokes
  chrome.notifications.create from the harness page with the SAME options
  the production SW onStartup handler uses (iconUrl resolved via
  chrome.runtime.getURL('icons/icon128.png')). Exercises Chrome's
  imageUtil icon validation — the exact code path Bug A regressed on
  (a881bf0). 4 checks: non-empty assignedId, id-honoring, getAll delta=1,
  prefix set-membership. The SW handler invocation itself remains
  covered by tests/background/onstartup-notification.test.ts (unit
  tier); A8 covers the end-to-end imageUtil-acceptance gate (e2e tier).
  Per T-1-13-06 threat-model row: unit + e2e are intentional defense in
  depth covering both halves of the Bug A contract.

- A9 (icon file sizes meet imageUtil floors) — fetches icons/icon{16,48,
  128}.png via chrome.runtime.getURL and asserts blob.size against the
  200/500/1024-byte silent-rejection floors per assets-spec.md. Cheap
  pre-check for the Bug A class: a future icon swap that drops below
  the floor would silently break the notification flow; A9 catches it
  BEFORE the SW even tries to create.

- A10 (manifest shape contract) — chrome.runtime.getManifest() asserts:
  permissions includes 'notifications' (without it,
  chrome.notifications.create is unreachable), icons['16/48/128']
  defined + non-empty, action.default_icon['16/48/128'] same. 7 checks
  total. Catches manifest-edit regressions that would silently break A8.

Bug A canonical RED-on-regression demo cycle
============================================

Regression trigger: head -c 50 /tmp/icon128.png.backup > icons/icon128.png
(truncates the 2615-byte PNG to 50 bytes — preserves PNG magic so
manifest loads, but Chrome's imageUtil silent-rejects the create).

RED — A8 standalone driver with truncated icon128.png (50 bytes):

  A8 — BUG A canonical: chrome.notifications.create accepts startup-icon (imageUtil contract): FAIL
  Top-level error: notifications.create rejected: Unable to download all specified images.

  Diagnostics:
    - Step 1: snapshot notif count + ids BEFORE create
    - Step 1 result: 0 active; ids=[]
    - Step 2: chrome.notifications.create(id='mokosh-startup-1779124969677', iconUrl='chrome-extension://<ext-id>/icons/icon128.png')
    - THREW: notifications.create rejected: Unable to download all specified images.

GREEN — A8 standalone driver after restoring icon128.png (2615 bytes):

  A8 — BUG A canonical: chrome.notifications.create accepts startup-icon (imageUtil contract): PASS

  Checks:
    [PASS] A8.1: create callback resolves with non-empty assignedId (imageUtil acceptance)
           expected: "non-empty string"
           actual:   "mokosh-startup-1779124999809"
    [PASS] A8.2: assignedId matches input id (chrome.notifications honors caller-supplied id)
           expected: "mokosh-startup-1779124999809"
           actual:   "mokosh-startup-1779124999809"
    [PASS] A8.3: notification count delta === 1 (exactly one new startup notification)
           expected: 1
           actual:   1
    [PASS] A8.4: at least one notification id startsWith 'mokosh-startup-' (set membership)
           expected: true
           actual:   true

The RED→GREEN cycle proves the harness empirically catches Bug A
regression class (imageUtil silent rejection on undersized iconUrl PNG).
The "Unable to download all specified images." rejection is Chrome's
internal error surface for the same imageUtil validation that Bug A
originally regressed on (fix at a881bf0). Note: under the full
orchestrator order, the same truncation surfaces FIRST at A7 (recovery
notification, which shares NOTIFICATION_ICON_PATH) — orchestrator
bail-on-first-failure means A8 isn't reached in the full run. The
isolated A8 demo above (via an ephemeral local driver script, NOT
committed) confirmed A8 catches the same regression independently.

Baseline preserved
==================

- vitest: 93/93 GREEN (SKIP_BUILD=1 to dodge the pre-existing
  ~5s-default test timeout in no-test-hooks-in-prod-bundle.test.ts;
  with a fresh dist/ in place all 9 hook-string sub-tests PASS).
- tsc: clean (no diagnostics).
- npm run build: exit 0; production bundle unchanged
  (no SW/offscreen src edits — only tests/ + dist-test/).
- npm run test:uat: 11/14 GREEN (A0+A1+A2+A3+A4+A5+A6+A7+A8+A9+A10);
  bails at A11 (Wave 3D wires that).

Files touched
=============

- tests/uat/extension-page-harness.ts: +assertA8 +assertA9 +assertA10
  with 4 + 3 + 7 checks respectively; +createNotificationPromise +
  getActiveNotificationIds + STARTUP_NOTIF_PREFIX + A8_GETALL_SETTLE_MS
  + A9_ICON_SPEC helpers. window.__mokoshHarness extends 7 → 10 methods.
- tests/uat/lib/harness-page-driver.ts: replaces driveA8/driveA9/driveA10
  NYI stubs with page.evaluate wrappers.
- tests/uat/harness.test.ts: updates Wave-3C-current comment block to
  reflect A8+A9+A10 wired (expected diagnostic 11/14, bail at A11).

Approach rationale (per plan resolved-questions §A8)
====================================================

The plan resolved A8's "no SW-side handler-capture hook" challenge with
an explicit SIMPLER WORKAROUND: invoke chrome.notifications.create
DIRECTLY from the harness page with the same production options. This
sidesteps the MV3-SW-dynamic-import block (01-11-SUMMARY) while still
exercising Chrome's imageUtil validation — the exact code path Bug A
broke. Approach considered but rejected per the plan: a SW-side
static eager-import test hook + a __mokoshTriggerStartup message
handler would have required adding a new production code path (even
gated by __MOKOSH_UAT__) and a new FORBIDDEN_HOOK_STRINGS entry. The
page-direct approach adds ZERO production surface and ZERO new
forbidden strings — strictly better.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:07:47 +02:00
6a77967b6c feat(01-13): wave-3B — A5+A6+A7 GREEN + Bug B canonical regression rewind
Wave 3B lands the A5 (SAVE_ARCHIVE → zip on disk) and A7 (genuine
RECORDING_ERROR → ERR + recovery notification) assertions, completing
8/14 of the orchestrator's GREEN floor (A0+A1+A2+A3+A4+A5+A6+A7).
Bails at A8 (Wave 3C scope).

Changes per file:

  tests/uat/extension-page-harness.ts
    - assertA5: 11s settle (>= SEGMENT_DURATION_MS so first rotation
      lands a segment) + send SAVE_ARCHIVE + assert resp.success=true.
      Page-side only checks SW handler ack; host-side driver verifies
      disk-side outcome (zip presence + size floor).
    - assertA7: setupFreshRecording helper (A6 tears down; A7 needs
      REC state) → snapshot notif count → send RECORDING_ERROR with
      a non-Bug-B error code ('codec-unsupported') → 200ms settle →
      assert badge='ERR' + popup endsWith popup.html + notif delta=1
      + set-membership for 'mokosh-recovery-*' prefix.
    - setupFreshRecording: shared helper for A7 + future assertions
      that need a fresh REC state after a teardown.

  tests/uat/lib/harness-page-driver.ts
    - driveA5: page.evaluate(assertA5) THEN host-side fs polling for
      *.zip in handles.downloadsDir. The CDP Browser.setDownloadBehavior
      override renames the file to download.zip (data: URL filename
      gap), so we accept any *.zip suffix. Merges page-side check +
      host-side checks into a single AssertionRecord. Signature now
      takes downloadsDir as a second arg.
    - driveA7: standard page.evaluate wrapper (no host-side work).

  tests/uat/harness.test.ts
    - Wraps driveA5 in a closure that captures handles.downloadsDir.
    - Reordered: launchHarnessBrowser MUST run before driver list so
      the closure can read handles without a TDZ trap.

  tests/uat/lib/launch.ts
    - Victim page switched from about:blank to a file:// URL backed by
      a tmp HTML file in downloadsDir. About:blank breaks A5 because
      chrome.tabs.captureVisibleTab needs <all_urls> permission which
      matches http/https/file/ftp but NOT about: or data: URLs. The
      stub HTML satisfies <all_urls> + provides a real .url for the
      production saveArchive's chrome.tabs.query.

  src/test-hooks/offscreen-hooks.ts (test-only — tree-shaken from prod)
    - installFakeDisplayMedia: mintStream() helper called per
      fakeGetDisplayMedia invocation; each call mints a FRESH
      MediaStream from the persistent canvas. Real getDisplayMedia
      returns a new stream per call — fake now matches. Required for
      A7's setupFreshRecording where the previous recording's stream
      tracks were stopped by A6's onUserStoppedSharing teardown.
    - Added 33ms setInterval-driven drawFrame() alongside the
      existing requestAnimationFrame loop. RAF can throttle in
      headless Chrome on offscreen documents (page-visibility
      heuristics produce 0 fps), which yields zero-byte
      MediaRecorder segments that crash ts-ebml's VINT decode in
      webm-remux.extractFramesFromSegment with "Unrepresentable
      length: Infinity". The setInterval is redundant when RAF fires
      at full rate; it's a safety net for the headless-MV3 corner.

Bug B regression-catch demo (success_criteria #3 — MANDATORY per plan):

Step 1 — apply local regression patch (NOT committed):
  src/background/index.ts:792  setIdleMode() → setErrorMode()

Step 2 — npm run build:test && npm run test:uat RED snippet:

  A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: FAIL
    [PASS] SETUP: badge becomes REC after start
    [FAIL] A6.1: badge text is '' (NOT 'ERR') after user-stop
           expected: ""
           actual:   "ERR"
    [FAIL] A6.2: popup is '' (NOT manifest default) after user-stop
           expected: ""
           actual:   "chrome-extension://<id>/src/popup/index.html"
    [PASS] A6.3: NO recovery notification fired (count delta === 0)
    [PASS] A6.4: isRecording=false (via badge proxy)

  UAT harness: 6/14 assertions passed (bailed: A6 failed; see above)

Step 3 — revert local patch (git checkout -- src/background/index.ts).

Step 4 — npm run build:test && npm run test:uat GREEN snippet:

  A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: PASS
    [PASS] SETUP: badge becomes REC after start
    [PASS] A6.1: badge text is '' (NOT 'ERR') after user-stop
    [PASS] A6.2: popup is '' (NOT manifest default) after user-stop
    [PASS] A6.3: NO recovery notification fired (count delta === 0)
    [PASS] A6.4: isRecording=false (via badge proxy)

  UAT harness: 8/14 assertions passed (bailed: A8 failed — NOT YET
  IMPLEMENTED — Wave 3C wires driveA8)

The harness CORRECTLY catches the Bug B regression — the canonical
debug 01-09-recovery-flow scenario (operator-initiated stop routed
through setErrorMode locks the operator out of restart because popup
stays pinned to SAVE-only mode). Bug B is now CI-callable end-to-end.

vitest 93/93 GREEN throughout (unit-test layer unaffected). Tier-1
grep gate GREEN (9 forbidden hook strings: 0 occurrences in dist/).
npm run build exit 0; npx tsc --noEmit exit 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:01:06 +02:00
1b67b1c1d3 feat(01-13): wave-3A — A1+A2+A3+A4 GREEN + harness.test.ts orchestrator (5/14 assertions GREEN)
Wave 3A landed. `npm run test:uat` now exercises 5/14 assertions
end-to-end (A0 + A1 + A2 + A3 + A4); bails at A5 NOT YET IMPLEMENTED
(Wave 3B scope). A6 still PASSES 5/5 through the standalone
`npx tsx tests/uat/a6.test.ts` entry — the orchestrator-level A6 won't
reach in Wave 3A because the sequential loop bails at A5; once Wave 3B
wires driveA5 the loop will fall through to A6 (which uses the proven
Wave-2 driveA6 driver — no rework needed there).

Files changed:

- `tests/uat/extension-page-harness.ts` — extends `window.__mokoshHarness`
  from `{ assertA6 }` to `{ assertA1, assertA2, assertA3, assertA4,
  assertA6 }`. Per-assertion contracts:
  • A1 — chrome.action.getBadgeText({}) === '' + getPopup({}) === ''
    + isRecording=false (badge !== 'REC' proxy per state-machine atomic
    pairing). 3 CheckRecords.
  • A2 — ensureOffscreen + START_RECORDING direct-to-offscreen
    (workaround for the `tabs` manifest permission gap per
    01-11-SUMMARY + plan resolved-questions row 2) + manual
    setBadgeText('REC') + setPopup(POPUP_HTML_PATH) + waitFor
    badge==='REC'. The bypassed chrome.action.onClicked →
    startVideoCapture path is unit-tested in
    tests/background/badge-state-machine.test.ts; A2 verifies the
    contract that matters (recording reaches the REC state-machine
    row). 2 CheckRecords.
  • A3 — offscreen bridge query 'get-display-surface' (new in this
    plan via the prior commit's offscreen-hooks extension) → asserts
    === 'monitor'. 1 CheckRecord.
  • A4 — getPopup remains 'src/popup/index.html' + hasDocument()===true
    (no duplicate offscreen). Essentially a no-op verification —
    regression protection against future refactors that might unpin
    the popup during recording or spawn extra offscreens on stray
    events. 2 CheckRecords.
  • IMPORTANT: chrome.action.getPopup() returns the FULL absolute
    chrome-extension://<id>/... URL (not the manifest-relative path).
    A2.2 + A4.1 assert via .endsWith('src/popup/index.html') to stay
    extension-id independent. Empirical finding from first orchestrator
    run; documented inline.

- `tests/uat/lib/harness-page-driver.ts` — wires `driveA1/A2/A3/A4`
  (replaces the 4 NOT YET IMPLEMENTED Wave-3A stubs from
  eb64521). Each wraps a single page.evaluate(() =>
  window.__mokoshHarness.assertXX()) call per the contract laid down
  by driveA6. A5+A7..A13 remain stubbed for Waves 3B+3C+3D.

- `tests/uat/harness.test.ts` (NEW) — top-level UAT orchestrator
  driving all 14 assertions sequentially against a single Chrome +
  single harness page. A0 (Tier-1 grep gate) runs pre-flight before
  any Chrome launch — mirrors
  tests/background/no-test-hooks-in-prod-bundle.test.ts forbidden-
  string inventory (9 entries; belt-and-suspenders per
  feedback-pre-checkpoint-bundle-gates.md memory). Bail-on-first-
  failure with [SKIP] markers for unreached assertions + structured
  diagnostic dump (full SW + offscreen console tail) on each failure.
  SKIP_PROD_REBUILD=1 escape hatch skips the A0-side `npm run build`
  for developer iteration.

Verification (all GREEN):
  - npx tsc --noEmit: clean (root)
  - npx tsc --noEmit -p tests/uat: clean (UAT subtree)
  - npm run build: clean; production bundle hook-free
    (9-string grep gate in vitest unit gate)
  - npm run build:test: clean; dist-test/assets/extension_page_harness-*.js
    grew from 3.87kB → 7.67kB (A1+A2+A3+A4 added)
  - SKIP_BUILD=1 npx vitest run: 93/93 GREEN
    (Wave 0+1+2 baseline 92 + 1 from the 9th grep-gate string from
    the prior commit; this commit adds zero new vitest tests — the
    A1-A4 contracts are verified at UAT-harness time only)
  - npx tsx tests/uat/a6.test.ts (standalone): 5/5 GREEN; exit 0
    (Wave-2 A6 baseline preserved through orchestrator-adjacent
    harness page surface extension)
  - npm run test:uat (full operator entry): 5/14 GREEN
    (A0 + A1 + A2 + A3 + A4); bails at A5 NOT YET IMPLEMENTED
    (Wave 3B scope, expected). Total wall clock ~25s (~5s build +
    ~5s prod-rebuild for A0 + ~15s assertion sequence).

Operator empirical-verification deferred to orchestrator (per
feedback-pre-checkpoint-bundle-gates.md — the orchestrator runs SW
CSP-safety + Node-globals + DOM-globals grep on the built bundle
before surfacing any checkpoint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:45:25 +02:00
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