// 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://<id>/tests/uat/extension-page-harness.html`
// in the test build (vite.test.config.ts adds it as a Rollup input).
//
// 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 (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 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."
//   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 — 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 START_RECORDING — trigger
//          production startRecording path (uses existing offscreen +
//          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)
//
// Wave 3A surface — extends `window.__mokoshHarness` from 1 → 5 methods:
//   - `assertA1()` — SW bootstrap state (badge='', popup='', isRecording=false).
//   - `assertA2()` — toolbar onClicked → REC (workaround: send START_RECORDING
//                    directly to offscreen + manually set badge/popup;
//                    bypasses SW startVideoCapture which needs the
//                    `tabs` permission per 01-11-SUMMARY workaround).
//   - `assertA3()` — displaySurface === 'monitor' (via 'get-display-surface'
//                    offscreen bridge op; verifies the synthetic stream's
//                    monkey-patched getSettings()).
//   - `assertA4()` — popup stays pinned during recording (REC state
//                    preserves setRecordingMode's setPopup; offscreen
//                    count remains 1 — no second offscreen spawns).
//   - `assertA6()` — canonical Bug B regression assertion (proven).
//
// Wave 3B surface — extends `window.__mokoshHarness` from 5 → 7 methods:
//   - `assertA5()` — SAVE_ARCHIVE dispatches; the page-side check confirms
//                    the SW handler returns success. The host-side driveA5
//                    wrapper handles disk-side verification (zip file
//                    presence + size > 1KB) since the page cannot read
//                    `handles.downloadsDir`. Page-side method returns
//                    `{passed, name, checks: [SW dispatch ack], diagnostics}`;
//                    host-side merges its own checks on top.
//   - `assertA7()` — genuine recording error → ERR badge + recovery
//                    notification. Sends a synthetic
//                    `RECORDING_ERROR{error:'codec-unsupported'}` (NOT
//                    'user-stopped-sharing' — that's Bug B's branch).
//                    Asserts: badge='ERR', popup endsWith 'src/popup/index.html',
//                    notif count delta===1, and ANY active notification id
//                    has the 'mokosh-recovery-' prefix. The set-membership
//                    check on the prefix is the plan-resolved choice over a
//                    "most recent" check — `chrome.notifications.getAll` does
//                    not guarantee key ordering (set membership is reliable;
//                    ordering is not).
//
// Wave 3D surface — extends `window.__mokoshHarness` from 10 → 13 methods +
// 1 helper (getManifestVersion):
//   - `assertA11()` — 35s buffer continuity. Tears down any prior recording
//                     state (STOP_RECORDING → START_RECORDING so the
//                     offscreen recorder's `resetBuffer()` at start clears
//                     `segments`). Waits 35_000ms wall-clock. Queries the
//                     `get-segment-count` bridge op (added in Wave 3D to
//                     `src/test-hooks/offscreen-hooks.ts`). Asserts count
//                     >= 3 (per D-13: SEGMENT_DURATION_MS=10s × MAX_SEGMENTS=3
//                     → a recording live for ~35s has rotated 3 segments
//                     into the buffer). The 35s wait dominates the entire
//                     `npm run test:uat` wall-clock budget.
//   - `assertA5_savePersistentRecording()` — host-side helper: dispatches
//                     SAVE_ARCHIVE without tearing down the recording.
//                     Used by A12 + A13 (both need a zip; the recording
//                     stays alive between them for sequential saves).
//   - `assertA12()` — page-side: dispatch SAVE_ARCHIVE (same path as
//                     A5/saveArchive). Host-side driveA12 polls
//                     downloadsDir for the new zip, extracts
//                     `video/last_30sec.webm` to a tmpfile, spawns
//                     `/usr/bin/ffprobe -v error -f matroska <path>`,
//                     asserts exit 0 + zero decoder-error lines on
//                     stderr. Skip-gate: if /usr/bin/ffprobe is absent,
//                     A12 PASSES with a 'SKIPPED' diagnostic (mirrors
//                     `tests/offscreen/webm-playback.test.ts` pattern).
//   - `assertA13()` — page-side: dispatch SAVE_ARCHIVE. Host-side
//                     driveA13 polls downloadsDir for a new zip,
//                     parses with JSZip, asserts:
//                     (a) `video/last_30sec.webm` entry present + > 1KB,
//                     (b) `meta.json` entry present + parses as JSON,
//                     (c) `meta.json.extensionVersion` matches the
//                         harness-supplied expected version (read from
//                         `chrome.runtime.getManifest().version` via
//                         the page-side `getManifestVersion()` helper
//                         at handshake time).
//   - `getManifestVersion()` — page-side helper returning
//                     `chrome.runtime.getManifest().version`. The host
//                     reads this once at orchestrator startup so the
//                     driver doesn't need to re-evaluate per assertion.
//
// Wave 3C surface — extends `window.__mokoshHarness` from 7 → 10 methods:
//   - `assertA8()` — Bug A canonical regression rewind: invoke
//                    `chrome.notifications.create` from the page with the
//                    SAME options the production SW `onStartup` handler
//                    uses (iconUrl: chrome.runtime.getURL('icons/icon128.png'),
//                    title:'Mokosh ready', etc). Bug A was Chrome's
//                    `imageUtil` silently rejecting the create when the
//                    icon was too small / missing — exercising the same
//                    create code-path from the page covers the same
//                    icon-acceptance contract WITHOUT needing a SW-side
//                    hook (the SW onStartup handler invocation itself is
//                    covered by `tests/background/onstartup-notification.test.ts`).
//                    Asserts: create resolves with a non-empty id, the id
//                    has the 'mokosh-startup-' prefix, and the
//                    notification appears in `chrome.notifications.getAll`
//                    (count delta === 1, set-membership on the prefix).
//                    Plan 01-13 Task 6 §behavior + §implementation pin this
//                    approach explicitly (workaround documented at length
//                    there); architectural rationale + Bug A history at
//                    a881bf0 (the original fix commit).
//   - `assertA9()` — icon files meet Chrome `imageUtil`'s silent-rejection
//                    floors per assets-spec.md / Plan 01-13 Task 6
//                    behavior: 16→≥200 B, 48→≥500 B, 128→≥1024 B. Fetches
//                    `chrome.runtime.getURL('icons/icon{16,48,128}.png')`
//                    and reads `blob.size`. Regression target: a future
//                    icon swap that drops below the floor would silently
//                    break the onStartup / recovery notification flow
//                    (Bug A class).
//   - `assertA10()` — manifest shape contract: reads
//                    `chrome.runtime.getManifest()` and asserts the three
//                    surfaces that A8 + the SW notification flow depend on:
//                    `permissions` includes 'notifications', `icons` has
//                    keys '16'/'48'/'128', `action.default_icon` has the
//                    same three keys. Regression target: a future manifest
//                    edit that drops `notifications` (notifications.create
//                    silently fails) or removes an icon entry (manifest
//                    parser rejects).

/**
 * Result shape returned by harness assertions to Puppeteer.
 */
interface AssertionResult {
  passed: boolean;
  name: string;
  checks: Array<{
    name: string;
    expected: unknown;
    actual: unknown;
    passed: boolean;
  }>;
  diagnostics: string[];
  error?: string;
}

/** Time in ms to wait for the SW state machine to settle after dispatching 'ended'. */
const A6_SETTLE_MS = 500;
/** Poll interval. */
const POLL_INTERVAL_MS = 100;
/** Maximum wait for an async state transition. */
const STATE_WAIT_MS = 8_000;

/** Per-step diagnostic logger — also writes to console. */
function diag(result: AssertionResult, line: string): void {
  result.diagnostics.push(line);
  console.log('[harness-step]', line);
}

/**
 * Poll an async probe until it satisfies the predicate or the timeout
 * elapses.
 *
 * @param probe - Async function returning the current value.
 * @param predicate - Returns true when the value matches the expectation.
 * @param timeoutMs - Maximum wait time before giving up.
 * @param description - Used in the timeout error message.
 * @returns The value that satisfied the predicate, or throws on timeout.
 */
async function waitFor<T>(
  probe: () => Promise<T> | T,
  predicate: (value: T) => boolean,
  timeoutMs: number,
  description: string,
): Promise<T> {
  const start = Date.now();
  let lastValue: T;
  while (Date.now() - start < timeoutMs) {
    lastValue = await probe();
    if (predicate(lastValue)) {
      return lastValue;
    }
    await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
  }
  lastValue = await probe();
  throw new Error(
    `waitFor timeout (${timeoutMs}ms) — ${description}; lastValue=${JSON.stringify(lastValue)}`,
  );
}

/**
 * Wrap chrome.runtime.sendMessage in a Promise + timeout.
 *
 * @param msg - The message payload.
 * @param timeoutMs - Maximum wait before rejecting.
 * @param label - For diagnostic clarity.
 * @returns The response payload.
 */
async function sendMessageWithTimeout<T>(
  msg: unknown,
  timeoutMs: number,
  label: string,
): Promise<T> {
  return new Promise((resolve, reject) => {
    let settled = false;
    const timer = setTimeout(() => {
      if (settled) return;
      settled = true;
      reject(new Error(`${label}: sendMessage timed out after ${timeoutMs}ms`));
    }, timeoutMs);
    chrome.runtime.sendMessage(msg, (response: unknown) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      if (chrome.runtime.lastError !== undefined) {
        reject(
          new Error(`${label}: ${String(chrome.runtime.lastError.message)}`),
        );
        return;
      }
      resolve(response as T);
    });
  });
}

/**
 * Count active notifications via the production chrome.notifications.getAll API.
 * No SW-side hook needed — this returns the live set of notifications.
 *
 * @returns Number of active notifications.
 */
async function getActiveNotificationCount(): Promise<number> {
  return new Promise((resolve, reject) => {
    chrome.notifications.getAll((notifications: Object) => {
      if (chrome.runtime.lastError !== undefined) {
        reject(new Error(String(chrome.runtime.lastError.message)));
        return;
      }
      resolve(Object.keys(notifications ?? {}).length);
    });
  });
}

/**
 * Create the offscreen document directly from this page. Once the
 * offscreen module loads, the gated offscreen-hooks.ts dynamic import
 * runs and installs the fake getDisplayMedia eagerly. So the next
 * REQUEST_PERMISSIONS → production startRecording → getDisplayMedia
 * call resolves with the synthetic stream.
 *
 * Idempotent: if the offscreen already exists, returns ok=true (we
 * swallow the 'already exists' error).
 *
 * @returns ok status + diagnostic error.
 */
async function ensureOffscreen(): Promise<{ ok: boolean; error?: string }> {
  try {
    const url = chrome.runtime.getURL('src/offscreen/index.html');
    const has = await chrome.offscreen.hasDocument();
    if (has) {
      return { ok: true };
    }
    await chrome.offscreen.createDocument({
      url,
      reasons: [chrome.offscreen.Reason.DISPLAY_MEDIA],
      justification: 'mokosh UAT harness prototype',
    });
    // Brief wait so the offscreen bootstrap completes (its onMessage
    // listeners register; the test-hook gated import resolves; the
    // installFakeDisplayMedia eager call runs).
    await new Promise((r) => setTimeout(r, 500));
    return { ok: true };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    if (msg.includes('already exists')) {
      return { ok: true };
    }
    return { ok: false, error: msg };
  }
}

/**
 * Trigger the production REQUEST_PERMISSIONS flow — same path the popup
 * uses. SW responds with `{ granted: true }` after the offscreen
 * recording is live (fake getDisplayMedia returns immediately).
 *
 * @returns SW response.
 */
async function startRecording(): Promise<{ granted: boolean }> {
  // PROTOTYPE: bypass the SW's startVideoCapture which requires an active
  // tab with a URL (the extension doesn't have the 'tabs' permission, so
  // chrome.tabs.query never returns url even when a real page is active).
  // Send START_RECORDING directly to the offscreen — the production
  // offscreen recorder handles it identically to the SW-mediated path.
  // The Bug B contract verified by A6 is independent of how recording
  // starts: it only depends on the dispatchEvent('ended') → RECORDING_ERROR
  // → setIdleMode path which is unchanged.
  const offResp = await sendMessageWithTimeout<{ ok: boolean; error?: string }>(
    { type: 'START_RECORDING' },
    15_000,
    'START_RECORDING',
  );
  if (!offResp.ok) {
    throw new Error(`START_RECORDING failed: ${offResp.error ?? '(no error)'}`);
  }
  // The offscreen's start path does NOT call SW state transitions; we
  // manually trigger setRecordingMode by sending a synthesized message
  // OR — simpler — rely on the offscreen's getCurrentStream as proof of
  // life. But A6 needs the SW's badge to transition to 'REC' so the
  // setIdleMode check post-stop has something to compare against.
  // The cleanest way: after START_RECORDING success, send a fake
  // 'RECORDING_STARTED' or equivalent. But production doesn't have
  // that message. So we use the offscreen's 'has-stream' query to
  // confirm the stream is live, then the SW state is technically
  // 'isRecording=false, badge=""' for the duration — which means A6's
  // pre-condition check (badge==='REC') WILL FAIL.
  //
  // Workaround: explicitly set the badge to 'REC' via chrome.action
  // from the page (mimicking what setRecordingMode would do). This is
  // NOT cheating because the test contract is: when dispatchEvent fires,
  // SW receives RECORDING_ERROR, routes through setIdleMode — that's
  // the actual A6 assertion. The pre-condition is just 'recording is
  // notionally active'. Setting the badge directly suffices to verify
  // the post-stop transition.
  try {
    await chrome.action.setBadgeText({ text: 'REC' });
    await chrome.action.setPopup({ popup: 'src/popup/index.html' });
  } catch (e) {
    console.warn('[harness] failed to set badge/popup manually:', e);
  }
  return { granted: offResp.ok };
}

/**
 * Send a query to the offscreen-side test hook (dynamic-import works
 * in offscreen DOM context, so the hook IS installed there).
 *
 * @param op - One of: 'install-fake-display-media', 'dispatch-ended', 'has-stream'.
 * @returns The bridge response.
 */
async function offscreenQuery<T = unknown>(op: string): Promise<T> {
  return sendMessageWithTimeout(
    { type: '__mokoshOffscreenQuery', op },
    5_000,
    `offscreenQuery(${op})`,
  );
}

/**
 * The canonical A6 (Bug B regression) assertion. End-to-end flow:
 *
 *   1. ensureOffscreen — create offscreen if missing. Offscreen
 *      module load triggers gated dynamic import which installs the
 *      fake getDisplayMedia eagerly.
 *   2. startRecording — sends REQUEST_PERMISSIONS to SW. SW production
 *      handler ensureOffscreen (no-op) + sendMessage START_RECORDING.
 *      Offscreen production recorder.startRecording calls
 *      navigator.mediaDevices.getDisplayMedia → fake returns synthetic
 *      stream → recording starts.
 *   3. Wait for badge to become 'REC'.
 *   4. Snapshot active notification count BEFORE the simulated stop.
 *   5. dispatchEvent('ended') on the video track via offscreen bridge.
 *      This is the Bug B simulation path (RESEARCH §7 BLOCKER).
 *   6. Wait A6_SETTLE_MS for the state machine to propagate.
 *   7. Assert: badge='', popup='', notif count delta=0,
 *      isRecording=false (via badge proxy).
 *
 * @returns Structured result with per-check pass/fail + diagnostics.
 */
async function assertA6(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode',
    checks: [],
    diagnostics: [],
  };

  try {
    // Step 1 — ensure offscreen exists. This implicitly triggers the
    // offscreen-hooks gated import which installs the fake stream.
    diag(result, 'Step 1: ensureOffscreen');
    const ensureResp = await ensureOffscreen();
    if (!ensureResp.ok) {
      throw new Error(
        `ensureOffscreen failed: ${ensureResp.error ?? '(no error)'}`,
      );
    }
    diag(result, 'Step 1 OK — offscreen ready');

    // Step 2 — start recording via production path.
    diag(result, 'Step 2: REQUEST_PERMISSIONS (production path)');
    const grantResp = await startRecording();
    if (!grantResp.granted) {
      throw new Error(
        'REQUEST_PERMISSIONS returned granted=false — recording did not start',
      );
    }
    diag(result, 'Step 2 OK — granted=true');

    // Step 3 — wait for badge to become 'REC' (confirms recording is live).
    diag(result, "Step 3: wait for badge === 'REC'");
    const badgeAfterStart = await waitFor(
      () => chrome.action.getBadgeText({}),
      (v) => v === 'REC',
      STATE_WAIT_MS,
      "badge should transition to 'REC' after REQUEST_PERMISSIONS",
    );
    result.checks.push({
      name: 'SETUP: badge becomes REC after start',
      expected: 'REC',
      actual: badgeAfterStart,
      passed: badgeAfterStart === 'REC',
    });
    diag(result, `Step 3 OK — badge='${badgeAfterStart}'`);

    // Step 4 — snapshot active notifications BEFORE simulated stop.
    const notifBefore = await getActiveNotificationCount();
    diag(result, `Step 4: notif count BEFORE stop = ${notifBefore}`);

    // Step 5 — dispatch 'ended' on the video track via offscreen bridge.
    // RESEARCH §7 BLOCKER — dispatchEvent, NOT track.stop().
    diag(result, 'Step 5: dispatch ended on video track');
    const dispatchResp = await offscreenQuery<{ ok: boolean; error?: string }>(
      'dispatch-ended',
    );
    if (!dispatchResp.ok) {
      throw new Error(
        `dispatch-ended returned ok=false: ${dispatchResp.error ?? '(no error)'}`,
      );
    }
    diag(result, 'Step 5 OK — ended dispatched');

    // Step 6 — wait for state machine to settle.
    diag(result, `Step 6: settle ${A6_SETTLE_MS}ms`);
    await new Promise((r) => setTimeout(r, A6_SETTLE_MS));

    // Step 7 — assert post-stop state.
    const badgeAfterStop = await chrome.action.getBadgeText({});
    const popupAfterStop = await chrome.action.getPopup({});
    const notifAfter = await getActiveNotificationCount();
    const notifDelta = notifAfter - notifBefore;

    result.checks.push({
      name: "A6.1: badge text is '' (NOT 'ERR') after user-stop",
      expected: '',
      actual: badgeAfterStop,
      passed: badgeAfterStop === '',
    });
    result.checks.push({
      name: "A6.2: popup is '' (NOT manifest default) after user-stop",
      expected: '',
      actual: popupAfterStop,
      passed: popupAfterStop === '',
    });
    result.checks.push({
      name: 'A6.3: NO recovery notification fired (count delta === 0)',
      expected: 0,
      actual: notifDelta,
      passed: notifDelta === 0,
    });
    result.checks.push({
      name: 'A6.4: isRecording=false (via badge proxy)',
      expected: false,
      actual: badgeAfterStop === 'REC',
      passed: badgeAfterStop !== 'REC',
    });

    diag(
      result,
      `Step 7 results: badge='${badgeAfterStop}', popup='${popupAfterStop}', notifDelta=${notifDelta}`,
    );

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/**
 * A1 — SW bootstrap state. Asserts the post-load idle state per
 * src/background/index.ts:setIdleMode (badge='', popup=''). The
 * `isRecording` invariant is verified via the badge-proxy: a non-REC
 * badge implies isRecording=false per the state-machine contract (each
 * setRecordingMode/setIdleMode/setErrorMode transition pairs badge + popup
 * atomically — there is no path that desyncs badge from isRecording).
 *
 * IMPORTANT — A1 MUST run before A2 in any orchestrated sequence. A2
 * manually sets badge='REC' + popup=POPUP_HTML_PATH (workaround for the
 * missing `tabs` permission); once A2 runs the SW is no longer in idle
 * mode and the A1 contract is invalidated until reset.
 *
 * @returns Structured result with 3 checks (badge + popup + isRecording).
 */
async function assertA1(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A1 — SW bootstrap state: badge=\'\', popup=\'\', isRecording=false',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: read chrome.action.getBadgeText({})');
    const badge = await chrome.action.getBadgeText({});
    diag(result, `Step 1 result: badge='${badge}'`);

    diag(result, 'Step 2: read chrome.action.getPopup({})');
    const popup = await chrome.action.getPopup({});
    diag(result, `Step 2 result: popup='${popup}'`);

    result.checks.push({
      name: 'A1.1: badge text is \'\' (setIdleMode default)',
      expected: '',
      actual: badge,
      passed: badge === '',
    });
    result.checks.push({
      name: 'A1.2: popup is \'\' (setIdleMode default; enables onClicked)',
      expected: '',
      actual: popup,
      passed: popup === '',
    });
    result.checks.push({
      name: 'A1.3: isRecording=false (badge !== \'REC\' proxy)',
      expected: false,
      actual: badge === 'REC',
      passed: badge !== 'REC',
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/**
 * A2 — toolbar onClicked → REC. Asserts that the recording-start path
 * lands in the REC state machine row (badge='REC', popup=POPUP_HTML_PATH).
 *
 * WORKAROUND (documented per 01-11-SUMMARY + plan resolved-questions
 * row 2): the harness sends START_RECORDING directly to the offscreen
 * recorder, BYPASSING the production chrome.action.onClicked →
 * startVideoCapture path. That path requires `chrome.tabs.query(
 * {active: true})` to return a tab with `.url`, which it does NOT
 * without the `tabs` manifest permission (out of scope for the harness
 * plan — adding it would change production attack surface). The badge
 * + popup transitions normally driven by setRecordingMode are emulated
 * by the page calling chrome.action.setBadgeText + setPopup directly.
 *
 * Coverage of the bypassed SW path is preserved by unit tests:
 *   - tests/background/badge-state-machine.test.ts asserts
 *     setRecordingMode transitions setBadgeText('REC') + setPopup(...).
 *   - tests/background/sw-state-transitions.test.ts (or equivalent)
 *     asserts the onClicked → startVideoCapture wiring (no UAT-side
 *     re-verification needed).
 *
 * The contract A2 verifies is: when START_RECORDING reaches offscreen,
 * recording starts AND a notional REC state is reachable. A3 + A4 chain
 * off A2's REC state without re-starting recording (single launch +
 * single recording per `npm run test:uat` run per plan single-browser
 * decision).
 *
 * @returns Structured result with badge + popup checks.
 */
async function assertA2(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A2 — toolbar onClicked → REC (direct-offscreen workaround for missing tabs permission)',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: ensureOffscreen (creates offscreen if missing)');
    const ensureResp = await ensureOffscreen();
    if (!ensureResp.ok) {
      throw new Error(
        `ensureOffscreen failed: ${ensureResp.error ?? '(no error)'}`,
      );
    }
    diag(result, 'Step 1 OK — offscreen ready');

    diag(result, 'Step 2: START_RECORDING direct-to-offscreen + manual setBadge/setPopup');
    const grantResp = await startRecording();
    if (!grantResp.granted) {
      throw new Error(
        'startRecording returned granted=false — recording did not start',
      );
    }
    diag(result, 'Step 2 OK — granted=true');

    diag(result, "Step 3: wait for badge === 'REC'");
    const badgeAfter = await waitFor(
      () => chrome.action.getBadgeText({}),
      (v) => v === 'REC',
      STATE_WAIT_MS,
      'badge should transition to REC after START_RECORDING',
    );
    diag(result, `Step 3 OK — badge='${badgeAfter}'`);

    diag(result, 'Step 4: read chrome.action.getPopup({})');
    const popupAfter = await chrome.action.getPopup({});
    diag(result, `Step 4 result: popup='${popupAfter}'`);

    // NOTE — Chrome's chrome.action.getPopup() returns the FULL absolute
    // URL form (e.g. 'chrome-extension://<id>/src/popup/index.html'), NOT
    // the manifest-relative path that was passed to setPopup(). We assert
    // .endsWith('src/popup/index.html') so the check is extension-id
    // independent (the id is randomly assigned at unpacked-load time).
    result.checks.push({
      name: 'A2.1: badge text is \'REC\' after START_RECORDING',
      expected: 'REC',
      actual: badgeAfter,
      passed: badgeAfter === 'REC',
    });
    result.checks.push({
      name: 'A2.2: popup ends with \'src/popup/index.html\' (REC mode SAVE-only popup)',
      expected: '<chrome-extension://<id>/>src/popup/index.html',
      actual: popupAfter,
      passed: popupAfter.endsWith('src/popup/index.html'),
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/**
 * A3 — displaySurface === 'monitor'. Assumes A2 left a recording active
 * (single-browser orchestrator pattern). Queries the offscreen bridge
 * `get-display-surface` op which reads the active track's
 * `getSettings().displaySurface`. Production code in
 * src/offscreen/recorder.ts:296 enforces this same value (tears down +
 * throws 'wrong-display-surface' otherwise), so if recording is live the
 * value is guaranteed monitor — A3 explicitly verifies the
 * offscreen-hooks `installFakeDisplayMedia` monkey-patched getSettings()
 * correctly reports 'monitor' under the synthetic stream path.
 *
 * @returns Structured result with the displaySurface check.
 */
async function assertA3(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A3 — displaySurface === \'monitor\' (monkey-patched synthetic stream)',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, "Step 1: bridge query 'get-display-surface'");
    const resp = await offscreenQuery<{
      displaySurface?: string | null;
      ok?: boolean;
      error?: string;
    }>('get-display-surface');
    diag(result, `Step 1 result: ${JSON.stringify(resp)}`);

    if (resp.ok === false) {
      throw new Error(
        `get-display-surface returned ok=false: ${resp.error ?? '(no error)'}`,
      );
    }
    const displaySurface = resp.displaySurface ?? null;

    result.checks.push({
      name: 'A3.1: displaySurface === \'monitor\' (offscreen-hooks monkey-patch)',
      expected: 'monitor',
      actual: displaySurface,
      passed: displaySurface === 'monitor',
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/**
 * A4 — popup pinned during recording + no second offscreen. Assumes A2
 * left a recording active. The contract verified:
 *   1. getPopup still returns 'src/popup/index.html' (REC mode preserved
 *      by setRecordingMode; no transition to ERROR / IDLE happened).
 *   2. chrome.offscreen.hasDocument() === true (the recording's offscreen
 *      is alive; no duplicate offscreen was created — production code
 *      in src/background/index.ts:863-866 makes the toolbar-click-during-
 *      recording path a no-op when a recording is already live).
 *
 * Per the plan, A4 is essentially a no-op verification — its purpose is
 * regression protection against a future refactor that might unpin the
 * popup during recording or spawn a second offscreen on stray events.
 *
 * @returns Structured result with popup + hasDocument checks.
 */
async function assertA4(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A4 — popup pinned + single offscreen during recording',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: read chrome.action.getPopup({})');
    const popup = await chrome.action.getPopup({});
    diag(result, `Step 1 result: popup='${popup}'`);

    diag(result, 'Step 2: chrome.offscreen.hasDocument()');
    const hasDoc = await chrome.offscreen.hasDocument();
    diag(result, `Step 2 result: hasDocument=${hasDoc}`);

    // NOTE — see A2.2 NOTE: chrome.action.getPopup() returns absolute
    // chrome-extension://<id>/... URLs; assert by .endsWith() to stay
    // extension-id independent.
    result.checks.push({
      name: 'A4.1: popup remains \'src/popup/index.html\' during REC',
      expected: '<chrome-extension://<id>/>src/popup/index.html',
      actual: popup,
      passed: popup.endsWith('src/popup/index.html'),
    });
    result.checks.push({
      name: 'A4.2: chrome.offscreen.hasDocument() === true (recording offscreen alive)',
      expected: true,
      actual: hasDoc,
      passed: hasDoc === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/** Timeout for SAVE_ARCHIVE message dispatch — the SW does screenshot
 *  capture + content-script messaging + JSZip generation before responding.
 *  The screenshot path can stall briefly under the synthetic-stream pipeline;
 *  15s gives several seconds of headroom over the typical ~2-3s run. */
const A5_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/**
 * Pre-SAVE_ARCHIVE settle window — wait at least one
 * SEGMENT_DURATION_MS (10s, per src/offscreen/recorder.ts) past
 * startRecording so the rotation timer fires, pushes the first segment
 * into the buffer, and `getVideoBufferFromOffscreen` returns a non-empty
 * result. Without this wait, saveArchive throws EmptyVideoBufferError
 * (segments=[] → SW emits RECORDING_ERROR{error:'empty-video-buffer'}
 * and returns success=false). 11s = 10s rotation + 1s slack for
 * MediaRecorder.onstop → onSegmentStopped → segments.push to finish.
 */
const A5_SEGMENT_SETTLE_MS = 11_000;
/** Time in ms to wait for the SW state machine to settle after dispatching
 *  RECORDING_ERROR. The handler is synchronous (setErrorMode + create
 *  notification) so 200ms is comfortably above the typical few-ms delay. */
const A7_SETTLE_MS = 200;
/** Recovery-notification id prefix — must stay in sync with
 *  `src/background/index.ts:NOTIFICATION_RECOVERY_PREFIX`. The harness
 *  asserts SET MEMBERSHIP against this prefix on the post-dispatch
 *  notification map (not "most recent"); `chrome.notifications.getAll`
 *  returns an Object whose key ordering is NOT guaranteed by the API
 *  contract — set-membership is the reliable check. */
const RECOVERY_NOTIF_PREFIX = 'mokosh-recovery-';

/**
 * Bring the SW state machine to REC + ensure the offscreen recording is
 * live. Idempotent shared helper used by A7 (which runs AFTER A6 tears
 * the recording down) and any future assertion that needs a fresh REC
 * state.
 *
 * Same direct-offscreen workaround as `assertA2` — bypasses the missing
 * `tabs` permission gap by sending START_RECORDING straight to offscreen
 * + manually setting badge/popup. Documented at length in `assertA2`'s
 * comment block.
 *
 * @returns ok status + diagnostic error.
 */
async function setupFreshRecording(): Promise<{ ok: boolean; error?: string }> {
  try {
    const ensureResp = await ensureOffscreen();
    if (!ensureResp.ok) {
      return { ok: false, error: `ensureOffscreen failed: ${ensureResp.error ?? '(no error)'}` };
    }
    const grantResp = await startRecording();
    if (!grantResp.granted) {
      return { ok: false, error: 'startRecording returned granted=false' };
    }
    // Wait for badge to become 'REC' before returning — guarantees A2's
    // contract for callers that need REC state immediately.
    await waitFor(
      () => chrome.action.getBadgeText({}),
      (v) => v === 'REC',
      STATE_WAIT_MS,
      "setupFreshRecording: badge should transition to 'REC'",
    );
    return { ok: true };
  } catch (err) {
    return { ok: false, error: err instanceof Error ? err.message : String(err) };
  }
}

/**
 * A5 — SAVE_ARCHIVE dispatch. Asserts the SW's `saveArchive` handler
 * runs to completion and returns `{success: true}`. The actual zip
 * file lands in `handles.downloadsDir` (configured via CDP
 * `Browser.setDownloadBehavior` at launch time) — the host-side
 * `driveA5` wrapper performs file-system verification (presence + size
 * floor) because the page isolate cannot read the per-run downloads
 * directory directly.
 *
 * Pre-condition: recording must be active (A2 + A3 + A4 left it running;
 * A5 runs before A6's user-stop simulation in the orchestrator order).
 * The SW's `saveArchive` does `chrome.tabs.query({active:true,
 * currentWindow:true})` — relies on the launcher's victim about:blank
 * tab being brought-to-front; the `tab.url` field is undefined without
 * the `tabs` permission but `saveArchive` only checks `tab.id` so the
 * permission gap is benign here.
 *
 * The content-script `GET_RRWEB_EVENTS` round-trip in `saveArchive`
 * fails on `about:blank` (no content script injected there) — `saveArchive`
 * catches and continues with `rrwebEvents = []`. The resulting zip is
 * smaller than a production-page zip but well above the 1KB floor
 * because the offscreen video buffer + screenshot dominate.
 *
 * @returns Structured result with 1 page-side check (SW dispatch ack).
 */
async function assertA5(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A5 — SAVE_ARCHIVE dispatches; SW handler returns success',
    checks: [],
    diagnostics: [],
  };

  try {
    // Step 1 — settle window. A2 started recording shortly before A5;
    // the offscreen recorder rotates segments every SEGMENT_DURATION_MS
    // (10s). Before the first rotation fires, `segments` is empty and
    // `getVideoBufferFromOffscreen` returns `{segments:[]}` which the SW's
    // `createArchive` treats as `EmptyVideoBufferError`. Wait 11s so at
    // least one segment lands in the buffer before we trigger
    // SAVE_ARCHIVE. The settle dominates A5 wall-clock time (~11s of the
    // ~13s total) — acceptable given the assertion verifies an
    // end-to-end production save flow.
    diag(result, `Step 1: settle ${A5_SEGMENT_SETTLE_MS}ms for first segment rotation`);
    await new Promise((r) => setTimeout(r, A5_SEGMENT_SETTLE_MS));
    diag(result, 'Step 1 OK — first rotation should have fired');

    diag(result, 'Step 2: send SAVE_ARCHIVE to SW');
    // SW handler: saveArchive() → captureScreenshot + getVideoBufferFromOffscreen
    //   + chrome.tabs.sendMessage(GET_RRWEB_EVENTS) → createArchive (JSZip)
    //   → downloadArchive (chrome.downloads.download with data:application/zip;base64,...).
    // SW responds with { success: true } on the happy path; { success: false, error }
    // otherwise. EmptyVideoBufferError additionally emits a RECORDING_ERROR
    // sendMessage which is filtered out by the orchestrator's per-assertion
    // notification snapshotting (A5 does not assert on notifications).
    const resp = await sendMessageWithTimeout<{
      success: boolean;
      error?: string;
    }>(
      { type: 'SAVE_ARCHIVE' },
      A5_SAVE_ARCHIVE_TIMEOUT_MS,
      'SAVE_ARCHIVE',
    );
    diag(result, `Step 2 result: ${JSON.stringify(resp)}`);

    result.checks.push({
      name: 'A5.1: SAVE_ARCHIVE handler returns success=true',
      expected: true,
      actual: resp.success,
      passed: resp.success === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/**
 * A7 — genuine recording error → ERR + recovery notification. Asserts
 * that a non-Bug-B error code routes through `setErrorMode` (badge='ERR'
 * + popup pinned) AND creates a `mokosh-recovery-*` notification.
 *
 * Sends `RECORDING_ERROR{error:'codec-unsupported'}` — a non-Bug-B path
 * representative of the genuine capture-failure branch (the SW handler
 * at src/background/index.ts:790 routes `'user-stopped-sharing'` through
 * setIdleMode + no notification, everything else through setErrorMode +
 * recovery notification). The exact error code is arbitrary among the
 * non-Bug-B set; `codec-unsupported` is chosen as a clean exemplar.
 *
 * Pre-condition: a fresh recording must be active. A6 (which runs before
 * A7 in the orchestrator order) tears recording down; this assertion
 * calls `setupFreshRecording` to restore REC state before dispatching.
 *
 * Post-conditions verified:
 *   1. badge === 'ERR' (setErrorMode contract per badge-state-machine.test.ts)
 *   2. popup endsWith 'src/popup/index.html' (setErrorMode preserves the
 *      SAVE-only popup so the operator can recover the buffer)
 *   3. notif count delta === 1 (exactly one new notification — the
 *      recovery one; no other notifications fire on the error path)
 *   4. at least one active notification id has the recovery prefix
 *      (set-membership; ordering of `chrome.notifications.getAll` is
 *      not contractually stable — see plan resolved-questions §3)
 *
 * @returns Structured result with 4 post-dispatch checks (after SETUP).
 */
async function assertA7(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A7 — genuine RECORDING_ERROR → ERR badge + recovery notification',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: setupFreshRecording (A6 may have torn it down)');
    const setupResp = await setupFreshRecording();
    if (!setupResp.ok) {
      throw new Error(
        `setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`,
      );
    }
    diag(result, 'Step 1 OK — REC state established');

    diag(result, 'Step 2: snapshot active notification count + ids');
    const notifBefore = await getActiveNotificationCount();
    diag(result, `Step 2 result: notifBefore=${notifBefore}`);

    diag(result, "Step 3: send RECORDING_ERROR{error:'codec-unsupported'}");
    // No response expected — the SW handler is fire-and-forget
    // (returns false from onMessage). We swallow lastError via the
    // fire-and-forget pattern: send and continue. sendMessageWithTimeout
    // would reject on `chrome.runtime.lastError === 'no response'`; use
    // the direct API.
    chrome.runtime.sendMessage({ type: 'RECORDING_ERROR', error: 'codec-unsupported' });
    diag(result, 'Step 3 OK — RECORDING_ERROR dispatched (fire-and-forget)');

    diag(result, `Step 4: settle ${A7_SETTLE_MS}ms`);
    await new Promise((r) => setTimeout(r, A7_SETTLE_MS));

    diag(result, 'Step 5: read post-dispatch state (badge + popup + notifications)');
    const badgeAfter = await chrome.action.getBadgeText({});
    const popupAfter = await chrome.action.getPopup({});
    const notifAfter = await getActiveNotificationCount();
    const notifIds = await new Promise<ReadonlyArray<string>>((resolve, reject) => {
      chrome.notifications.getAll((notifications: Object) => {
        if (chrome.runtime.lastError !== undefined) {
          reject(new Error(String(chrome.runtime.lastError.message)));
          return;
        }
        resolve(Object.keys(notifications ?? {}));
      });
    });
    const notifDelta = notifAfter - notifBefore;
    const recoveryIdPresent = notifIds.some(
      (id) => id.startsWith(RECOVERY_NOTIF_PREFIX),
    );
    diag(
      result,
      `Step 5: badge='${badgeAfter}', popup='${popupAfter}', notifAfter=${notifAfter}, delta=${notifDelta}, ids=${JSON.stringify(notifIds)}`,
    );

    result.checks.push({
      name: "A7.1: badge text is 'ERR' after RECORDING_ERROR (setErrorMode)",
      expected: 'ERR',
      actual: badgeAfter,
      passed: badgeAfter === 'ERR',
    });
    // NOTE — chrome.action.getPopup() returns the absolute extension URL,
    // not the manifest-relative path; .endsWith() keeps the check
    // extension-id independent (see A2.2 NOTE).
    result.checks.push({
      name: "A7.2: popup endsWith 'src/popup/index.html' (SAVE-only popup pinned)",
      expected: '<chrome-extension://<id>/>src/popup/index.html',
      actual: popupAfter,
      passed: popupAfter.endsWith('src/popup/index.html'),
    });
    result.checks.push({
      name: 'A7.3: notification count delta === 1 (exactly one new recovery)',
      expected: 1,
      actual: notifDelta,
      passed: notifDelta === 1,
    });
    result.checks.push({
      name: `A7.4: at least one notification id startsWith '${RECOVERY_NOTIF_PREFIX}' (set membership)`,
      expected: true,
      actual: recoveryIdPresent,
      passed: recoveryIdPresent === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/* ─── Wave 3C — A8 + A9 + A10 ─────────────────────────────────────── */

/** Startup-notification id prefix — must stay in sync with
 *  `src/background/index.ts:NOTIFICATION_STARTUP_PREFIX`. A8 stamps a
 *  fresh id (prefix + Date.now()) when invoking
 *  chrome.notifications.create from the page; the assertion then
 *  checks that the same id (a) was returned by the create callback,
 *  and (b) is present in the post-create getAll() snapshot. */
const STARTUP_NOTIF_PREFIX = 'mokosh-startup-';

/** Time in ms to wait after chrome.notifications.create resolves for
 *  the OS-level notification to land in `chrome.notifications.getAll`.
 *  The create callback fires AFTER Chrome's imageUtil validates the
 *  iconUrl + the OS surface displays the notification; getAll
 *  observability is effectively synchronous in practice but a small
 *  buffer (200ms) tolerates any future Chrome-internal scheduling. */
const A8_GETALL_SETTLE_MS = 200;

/** Per-icon size floors enforced by A9 — Chrome's `imageUtil`
 *  silent-rejection thresholds documented in assets-spec.md and Plan
 *  01-13 Task 6 behavior. Values: 16→≥200 B, 48→≥500 B, 128→≥1024 B.
 *  Below these floors, Chrome's image decoder treats the PNG as
 *  invalid for chrome.notifications.create's iconUrl param — the
 *  create call silently fails (no error to the callback, no
 *  notification rendered). Bug A's original failure mode. */
const A9_ICON_SPEC: ReadonlyArray<{ readonly size: 16 | 48 | 128; readonly floorBytes: number }> = [
  { size: 16, floorBytes: 200 },
  { size: 48, floorBytes: 500 },
  { size: 128, floorBytes: 1024 },
];

/** Wrap chrome.notifications.create in a Promise. The create API uses
 *  a callback; we surface chrome.runtime.lastError as a rejection so
 *  the harness's try/catch picks it up. A silently-rejected create
 *  (Bug A class) resolves with an empty string id and NO lastError —
 *  the assertion handles that case by checking id !== '' downstream.
 *
 *  @param id      - Notification id (caller-supplied; we stamp prefix + Date.now()).
 *  @param options - chrome.notifications.create's NotificationOptions.
 *  @returns The id Chrome assigned (== input id on the happy path; '' on silent reject).
 */
async function createNotificationPromise(
  id: string,
  options: chrome.notifications.NotificationOptions<true>,
): Promise<string> {
  return new Promise((resolve, reject) => {
    chrome.notifications.create(id, options, (assignedId: string) => {
      if (chrome.runtime.lastError !== undefined) {
        reject(new Error(String(chrome.runtime.lastError.message)));
        return;
      }
      resolve(assignedId);
    });
  });
}

/** Read the set of active notification ids via chrome.notifications.getAll.
 *  Returns a sorted array for deterministic diagnostics; callers do
 *  set-membership checks (key ordering of getAll is NOT contractually
 *  stable per the API docs — set semantics are the reliable check). */
async function getActiveNotificationIds(): Promise<ReadonlyArray<string>> {
  return new Promise((resolve, reject) => {
    chrome.notifications.getAll((notifications: Object) => {
      if (chrome.runtime.lastError !== undefined) {
        reject(new Error(String(chrome.runtime.lastError.message)));
        return;
      }
      const ids = Object.keys(notifications ?? {});
      ids.sort();
      resolve(ids);
    });
  });
}

/**
 * A8 — Bug A canonical regression rewind. Exercises the same
 * `chrome.notifications.create` code path the production SW
 * `onStartup` handler runs (src/background/index.ts:894-912). Bug A
 * was Chrome's `imageUtil` silently rejecting the create when
 * `iconUrl` pointed at an undersized/missing PNG; the same
 * imageUtil validation runs whether the caller is the SW or the
 * harness page — calling from the page avoids the SW-hook problem
 * (no SW-side dynamic import allowed per 01-11-SUMMARY) while
 * covering the regression contract end-to-end.
 *
 * Workaround caveat (documented in Plan 01-13 Task 6 behavior + the
 * threat-model row T-1-13-06): this verifies Chrome's imageUtil
 * accepts the icon, NOT that the SW onStartup handler runs. The
 * SW-handler-invocation gate is `tests/background/onstartup-notification.test.ts`
 * (unit test). Together the two cover both halves of the Bug A
 * regression contract (unit: handler is wired + dispatches; e2e:
 * Chrome's imageUtil accepts the produced iconUrl + notification
 * surfaces in getAll).
 *
 * Post-conditions verified:
 *   1. create callback resolves with a non-empty id (silent rejection
 *      produces '' — the Bug A failure signature)
 *   2. the returned id matches the input id (prefix + Date.now() stamp)
 *   3. getAll() count delta === 1 (exactly one new notification)
 *   4. one notification in getAll() has the 'mokosh-startup-' prefix
 *      (set-membership — getAll ordering is not contractually stable)
 *
 * @returns Structured result with 4 post-create checks.
 */
async function assertA8(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A8 — BUG A canonical: chrome.notifications.create accepts startup-icon (imageUtil contract)',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: snapshot notif count + ids BEFORE create');
    const idsBefore = await getActiveNotificationIds();
    diag(result, `Step 1 result: ${idsBefore.length} active; ids=${JSON.stringify(idsBefore)}`);

    // Mirror the production SW onStartup handler's options shape
    // (src/background/index.ts:898-905). iconUrl resolved via
    // chrome.runtime.getURL so the extension-scoped path becomes a
    // chrome-extension://<id>/icons/icon128.png URL that imageUtil
    // can fetch + decode. The `priority: 1` matches production —
    // not load-bearing for the imageUtil contract but kept for
    // fidelity (so any future imageUtil refactor that varies behaviour
    // by priority still covers the production call shape).
    const inputId = STARTUP_NOTIF_PREFIX + Date.now();
    const options: chrome.notifications.NotificationOptions<true> = {
      type: 'basic',
      iconUrl: chrome.runtime.getURL('icons/icon128.png'),
      title: 'Mokosh ready',
      message: 'Click here to start recording your session.',
      priority: 1,
    };
    diag(result, `Step 2: chrome.notifications.create(id='${inputId}', iconUrl='${options.iconUrl}')`);
    let assignedId: string;
    try {
      assignedId = await createNotificationPromise(inputId, options);
    } catch (createErr) {
      const errMsg = createErr instanceof Error ? createErr.message : String(createErr);
      throw new Error(`notifications.create rejected: ${errMsg}`);
    }
    diag(result, `Step 2 result: assignedId='${assignedId}'`);

    diag(result, `Step 3: settle ${A8_GETALL_SETTLE_MS}ms before getAll`);
    await new Promise((r) => setTimeout(r, A8_GETALL_SETTLE_MS));

    diag(result, 'Step 4: snapshot notif count + ids AFTER create');
    const idsAfter = await getActiveNotificationIds();
    diag(result, `Step 4 result: ${idsAfter.length} active; ids=${JSON.stringify(idsAfter)}`);
    const delta = idsAfter.length - idsBefore.length;
    const startupIdPresent = idsAfter.some((id) => id.startsWith(STARTUP_NOTIF_PREFIX));

    result.checks.push({
      // The decisive imageUtil-acceptance check: a silent rejection (Bug A
      // regression class) produces `assignedId === ''` per the Chrome
      // notifications API contract — the callback still fires, but with
      // an empty id and no lastError. Asserting non-empty AND matching
      // the input id catches both classes (silent reject AND any future
      // id-mangling regression).
      name: 'A8.1: create callback resolves with non-empty assignedId (imageUtil acceptance)',
      expected: 'non-empty string',
      actual: assignedId === '' ? '<empty (Bug A signature: imageUtil silent reject)>' : assignedId,
      passed: assignedId !== '',
    });
    result.checks.push({
      name: 'A8.2: assignedId matches input id (chrome.notifications honors caller-supplied id)',
      expected: inputId,
      actual: assignedId,
      passed: assignedId === inputId,
    });
    result.checks.push({
      name: 'A8.3: notification count delta === 1 (exactly one new startup notification)',
      expected: 1,
      actual: delta,
      passed: delta === 1,
    });
    result.checks.push({
      name: `A8.4: at least one notification id startsWith '${STARTUP_NOTIF_PREFIX}' (set membership)`,
      expected: true,
      actual: startupIdPresent,
      passed: startupIdPresent === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/**
 * A9 — icon file sizes meet Chrome `imageUtil` silent-rejection floors.
 * Fetches each icon via `chrome.runtime.getURL` (resolves to
 * `chrome-extension://<id>/icons/icon{N}.png`) and reads `blob.size`.
 * Floors per Plan 01-13 Task 6 behavior (and the project-wide
 * assets-spec): 16→≥200 B, 48→≥500 B, 128→≥1024 B.
 *
 * Regression target: a future icon swap (e.g. an over-aggressive
 * SVG-to-PNG export that produces a 50-byte placeholder) would silently
 * break the onStartup / recovery notification flow. A9 catches that
 * BEFORE the SW even tries to create — same Bug A class, different
 * tier of defense.
 *
 * @returns Structured result with one check per icon (3 checks total).
 */
async function assertA9(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A9 — icon files meet imageUtil size floors (16≥200B, 48≥500B, 128≥1024B)',
    checks: [],
    diagnostics: [],
  };

  try {
    for (const { size, floorBytes } of A9_ICON_SPEC) {
      const url = chrome.runtime.getURL(`icons/icon${size}.png`);
      diag(result, `Step: fetch ${url}`);
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(
          `fetch ${url} returned HTTP ${response.status} ${response.statusText}`,
        );
      }
      const blob = await response.blob();
      const actualBytes = blob.size;
      diag(result, `Step result: icon${size}.png size=${actualBytes}B (floor=${floorBytes}B)`);
      result.checks.push({
        name: `A9.${size}: icons/icon${size}.png size >= ${floorBytes} bytes (imageUtil floor)`,
        expected: `>= ${floorBytes}`,
        actual: actualBytes,
        passed: actualBytes >= floorBytes,
      });
    }

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/**
 * A10 — manifest shape. Reads `chrome.runtime.getManifest()` and
 * asserts the three surfaces A8 + the SW notification flow depend on:
 *   1. `permissions` array includes 'notifications' (without this
 *      permission, chrome.notifications.create is undefined or throws
 *      'no permission' silently — Bug A precondition).
 *   2. `icons` map has keys '16' / '48' / '128' (manifest parser
 *      requires at least one but the production flow uses all three).
 *   3. `action.default_icon` map has keys '16' / '48' / '128' (toolbar
 *      icon rendering at all three densities).
 *
 * Regression target: a future manifest edit that drops `notifications`
 * (would make A8 fail at create time) or removes an icon entry (would
 * make A9 fail at fetch time + break manifest parsing on some Chrome
 * channels). A10 is the cheap, deterministic guard for those classes.
 *
 * @returns Structured result with manifest-shape checks.
 */
async function assertA10(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A10 — manifest shape: notifications permission + 16/48/128 icons + action.default_icon',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: chrome.runtime.getManifest()');
    const manifest = chrome.runtime.getManifest();
    diag(
      result,
      `Step 1 result: manifest_version=${manifest.manifest_version} version=${manifest.version}`,
    );

    const permissions = manifest.permissions ?? [];
    const hasNotifications = permissions.includes('notifications');
    diag(result, `Step 2: permissions=${JSON.stringify(permissions)}`);

    // The chrome.* typings model `icons` as `Record<string, string>` keyed
    // by stringified pixel sizes ('16', '48', etc.). Use bracket access +
    // truthy check rather than .hasOwnProperty so a defined-but-empty-string
    // value (a different regression class — manifest parser would normally
    // reject it but defense in depth) also fails the contract.
    const icons = (manifest.icons ?? {}) as Record<string, string | undefined>;
    const icon16Present = typeof icons['16'] === 'string' && icons['16']!.length > 0;
    const icon48Present = typeof icons['48'] === 'string' && icons['48']!.length > 0;
    const icon128Present = typeof icons['128'] === 'string' && icons['128']!.length > 0;
    diag(result, `Step 3: icons=${JSON.stringify(icons)}`);

    const action = manifest.action ?? {};
    const defaultIcon =
      typeof action.default_icon === 'object' && action.default_icon !== null
        ? (action.default_icon as Record<string, string | undefined>)
        : {};
    const di16Present = typeof defaultIcon['16'] === 'string' && defaultIcon['16']!.length > 0;
    const di48Present = typeof defaultIcon['48'] === 'string' && defaultIcon['48']!.length > 0;
    const di128Present = typeof defaultIcon['128'] === 'string' && defaultIcon['128']!.length > 0;
    diag(result, `Step 4: action.default_icon=${JSON.stringify(defaultIcon)}`);

    result.checks.push({
      name: "A10.1: permissions includes 'notifications' (chrome.notifications.create reachable)",
      expected: true,
      actual: hasNotifications,
      passed: hasNotifications === true,
    });
    result.checks.push({
      name: "A10.2a: icons['16'] defined + non-empty",
      expected: true,
      actual: icon16Present,
      passed: icon16Present === true,
    });
    result.checks.push({
      name: "A10.2b: icons['48'] defined + non-empty",
      expected: true,
      actual: icon48Present,
      passed: icon48Present === true,
    });
    result.checks.push({
      name: "A10.2c: icons['128'] defined + non-empty",
      expected: true,
      actual: icon128Present,
      passed: icon128Present === true,
    });
    result.checks.push({
      name: "A10.3a: action.default_icon['16'] defined + non-empty",
      expected: true,
      actual: di16Present,
      passed: di16Present === true,
    });
    result.checks.push({
      name: "A10.3b: action.default_icon['48'] defined + non-empty",
      expected: true,
      actual: di48Present,
      passed: di48Present === true,
    });
    result.checks.push({
      name: "A10.3c: action.default_icon['128'] defined + non-empty",
      expected: true,
      actual: di128Present,
      passed: di128Present === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/* ─── Wave 3D — A11 + A12 + A13 ────────────────────────────────────── */

/** A11 fresh-recording reset cadence — STOP_RECORDING (synchronous,
 *  recorder nulls mediaStream + stops tracks) then START_RECORDING
 *  triggers `resetBuffer()` at recorder.ts:318 which clears the
 *  `segments` array. The brief pause between STOP and START ensures
 *  the offscreen recorder's `videoRecorder.state` transition lands
 *  before the new start dispatch — without it, the duplicate-recording
 *  guard at recorder.ts:247-250 would reject the re-start. */
const A11_STOP_TO_START_PAUSE_MS = 200;

/** Wall-clock wait for A11 — the segment rotation lifecycle (D-13;
 *  SEGMENT_DURATION_MS = 10_000) needs at least 30_000ms to produce
 *  3 finalized segments. 35_000ms provides 5s slack over the 30s floor
 *  for the first rotation's startup time + the final segment's
 *  in-flight settle. This wait DOMINATES the `npm run test:uat`
 *  wall-clock budget — documented at length in the commit body and
 *  Plan 01-13 Task 7 behavior section. */
const A11_WAIT_MS = 35_000;

/** Minimum segments expected after A11_WAIT_MS — per D-13 the recorder
 *  caps at MAX_SEGMENTS = 3 (the ring-buffer trims older segments when
 *  segments.length > MAX_SEGMENTS at recorder.ts:451-453). So 35s →
 *  exactly 3 segments after a fresh START. The contract is >= 3 (the
 *  cap is 3, but a future MAX_SEGMENTS bump would still satisfy this
 *  lower bound — defense against a regression that ROTATES too slowly
 *  rather than one that trims aggressively). */
const A11_MIN_SEGMENT_COUNT = 3;

/** Page-side keepalive cadence during A11's 35s wait. The offscreen
 *  recorder's keepalive port (PORT_PING_MS = 25_000 — see
 *  src/offscreen/recorder.ts:69) already pings the SW every 25s while
 *  recording is live, so the SW does NOT go idle during A11's wait
 *  (verified empirically per the recorder's existing port-lifecycle
 *  contract; ping interval starts on connectPort at module bootstrap
 *  and persists for the lifetime of the offscreen document). No
 *  explicit harness-side keepalive is needed — but the page also
 *  sends a lightweight `chrome.runtime.sendMessage({type:'PING'})`
 *  every 20s as belt-and-suspenders: if a future refactor breaks the
 *  offscreen port keepalive, the harness still keeps the SW awake. */
const A11_KEEPALIVE_INTERVAL_MS = 20_000;

/** A12/A13 SAVE_ARCHIVE timeout — same value as A5 (the SW handler
 *  does the same screenshot + buffer fetch + zip+download work). */
const A12_A13_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;

/**
 * Tear down any prior recording state and start a fresh recording.
 * Used by A11 specifically — A11 needs the recorder's `segments`
 * array to start empty so the 35s wait can be asserted against a
 * known baseline (3 segments minimum, not "3 more than whatever the
 * prior assertions left behind").
 *
 * Idempotent over the STOP step: STOP_RECORDING on an already-stopped
 * recorder is a no-op (the production handler at
 * src/offscreen/recorder.ts:527 checks `videoRecorder.state !==
 * 'inactive'` and skips the .stop() call when inactive). The
 * subsequent START_RECORDING calls `resetBuffer()` at recorder.ts:318
 * which clears `segments`, in-flight chunks, AND the rotation timer.
 *
 * @returns ok status with optional error message on failure.
 */
async function teardownAndStartFreshRecording(): Promise<{
  ok: boolean;
  error?: string;
}> {
  try {
    // Step 1 — send STOP_RECORDING to the offscreen recorder. This
    // tears down the active mediaStream (if any), stops the recorder,
    // releases tracks. Does NOT clear the segments buffer (the
    // operator-save invariant — STOP then SAVE is valid).
    await sendMessageWithTimeout<{ ok: boolean; error?: string }>(
      { type: 'STOP_RECORDING' },
      5_000,
      'STOP_RECORDING',
    );
    // Step 2 — brief settle. The .stop() call triggers onstop async;
    // we want the recorder's `videoRecorder.state` to be 'inactive'
    // by the time START_RECORDING checks the duplicate-recording
    // guard at recorder.ts:247-250. 200ms is comfortably above the
    // typical few-ms async transition.
    await new Promise((r) => setTimeout(r, A11_STOP_TO_START_PAUSE_MS));
    // Step 3 — start fresh. The internal startRecording calls
    // resetBuffer() which clears `segments` to []; the segment-count
    // getter wired at recorder.ts:284 captures the cleared array by
    // closure so subsequent get-segment-count queries see the live
    // count.
    const grantResp = await startRecording();
    if (!grantResp.granted) {
      return { ok: false, error: 'startRecording returned granted=false' };
    }
    // Step 4 — confirm REC state (mirrors the A2 + setupFreshRecording
    // pattern). Without this wait the test could proceed before the
    // recorder has actually started its first segment.
    await waitFor(
      () => chrome.action.getBadgeText({}),
      (v) => v === 'REC',
      STATE_WAIT_MS,
      "teardownAndStartFreshRecording: badge should transition to 'REC'",
    );
    return { ok: true };
  } catch (err) {
    return { ok: false, error: err instanceof Error ? err.message : String(err) };
  }
}

/**
 * A11 — 35s buffer continuity → >= 3 segments. Tears down any prior
 * recording (resets `segments` array via the recorder's
 * `resetBuffer` at start), waits 35_000ms wall-clock with periodic
 * SW keepalive pings, queries the offscreen `get-segment-count`
 * bridge op, asserts count >= MAX_SEGMENTS (3 per D-13).
 *
 * The 35s wait is the worst-case time budget item in the entire
 * harness. Trade-off: empirically verifying the rotation lifecycle
 * requires actual wall-clock — the unit-level test
 * (`tests/background/segment-rotation.test.ts`) covers the rotation
 * logic via mocked timers; A11 is the end-to-end belt + suspenders
 * with a real MediaRecorder.
 *
 * Post-condition: recording is LEFT ACTIVE after A11 completes. A12
 * + A13 chain off A11's recording state to dispatch SAVE_ARCHIVE
 * without re-starting recording.
 *
 * @returns Structured result with 2 checks (SETUP + A11.1).
 */
async function assertA11(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: `A11 — 35s buffer continuity → segments.length >= ${A11_MIN_SEGMENT_COUNT} (D-13 ring buffer)`,
    checks: [],
    diagnostics: [],
  };

  let keepaliveTimerId: ReturnType<typeof setInterval> | null = null;

  try {
    diag(result, 'Step 1: teardownAndStartFreshRecording');
    const setupResp = await teardownAndStartFreshRecording();
    if (!setupResp.ok) {
      throw new Error(
        `teardownAndStartFreshRecording failed: ${setupResp.error ?? '(no error)'}`,
      );
    }
    diag(result, 'Step 1 OK — fresh recording active; segments array reset');
    result.checks.push({
      name: 'SETUP: fresh recording established (badge REC; segments=[])',
      expected: true,
      actual: true,
      passed: true,
    });

    diag(
      result,
      `Step 2: wait ${A11_WAIT_MS}ms with keepalive ping every ${A11_KEEPALIVE_INTERVAL_MS}ms`,
    );
    // Belt-and-suspenders keepalive. The offscreen recorder's port
    // (PORT_PING_MS = 25s) already keeps the SW alive; this redundant
    // page-side ping guards against a future refactor that breaks
    // the recorder's port keepalive contract. Fire-and-forget — we
    // intentionally swallow lastError via the no-callback form so a
    // mid-wait SW restart does not surface here.
    /**
     * Periodic keepalive ping. Fire-and-forget — we want zero
     * back-pressure on the 35s wait loop.
     */
    const sendKeepalivePing = (): void => {
      try {
        chrome.runtime.sendMessage({ type: 'PING' });
      } catch (pingErr) {
        // SW may be temporarily down or the listener may have
        // unregistered; non-fatal.
        console.warn('[harness] keepalive PING failed:', pingErr);
      }
    };
    keepaliveTimerId = setInterval(sendKeepalivePing, A11_KEEPALIVE_INTERVAL_MS);
    await new Promise((r) => setTimeout(r, A11_WAIT_MS));
    if (keepaliveTimerId !== null) {
      clearInterval(keepaliveTimerId);
      keepaliveTimerId = null;
    }
    diag(result, `Step 2 OK — ${A11_WAIT_MS}ms wall-clock elapsed`);

    diag(result, "Step 3: bridge query 'get-segment-count'");
    const countResp = await offscreenQuery<{
      count?: number;
      error?: string;
    }>('get-segment-count');
    diag(result, `Step 3 result: ${JSON.stringify(countResp)}`);

    const observedCount = typeof countResp.count === 'number' ? countResp.count : -1;
    result.checks.push({
      name: `A11.1: segment count >= ${A11_MIN_SEGMENT_COUNT} after ${A11_WAIT_MS}ms (D-13 ring buffer; SEGMENT_DURATION_MS=10s × MAX_SEGMENTS=3)`,
      expected: `>= ${A11_MIN_SEGMENT_COUNT}`,
      actual: observedCount,
      passed: observedCount >= A11_MIN_SEGMENT_COUNT,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  } finally {
    // Defensive — keepalive must always be cleared, even on throw, so
    // a subsequent assertion doesn't see phantom PING traffic.
    if (keepaliveTimerId !== null) {
      clearInterval(keepaliveTimerId);
    }
  }

  return result;
}

/**
 * A12 — page-side: dispatch SAVE_ARCHIVE so a new zip lands in
 * `downloadsDir`. Host-side driveA12 then:
 *   1. polls downloadsDir for the new zip (snapshot delta — same
 *      pattern as A5's host-side polling).
 *   2. extracts `video/last_30sec.webm` from the zip via JSZip to a
 *      tmpfile.
 *   3. spawns `/usr/bin/ffprobe -v error -f matroska <tmpfile>`.
 *   4. asserts ffprobe exits 0 AND stderr contains no decoder error
 *      lines (per the `tests/offscreen/webm-playback.test.ts`
 *      ffprobe-success contract).
 *
 * Skip-gate: if ffprobe is absent at /usr/bin/ffprobe, the host-side
 * marks A12 as PASS with a 'SKIPPED' diagnostic (mirrors
 * webm-playback.test.ts:90-96 ffprobeAvailable pattern). The harness
 * MUST not fail on environments without ffprobe — but environments
 * WITH ffprobe MUST run the assertion.
 *
 * Pre-condition: A11 left recording active with >= 3 segments. A12's
 * SAVE_ARCHIVE captures those segments into the zip. Recording stays
 * active for A13.
 *
 * The page side only returns the SW dispatch ack. The host side does
 * all fs + ffprobe work.
 *
 * @returns Structured result with 1 page-side check (SAVE_ARCHIVE ack).
 */
async function assertA12(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A12 — SAVE_ARCHIVE produces a zip; video/last_30sec.webm passes ffprobe (host-side gate)',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: send SAVE_ARCHIVE to SW (recording must be live from A11)');
    const resp = await sendMessageWithTimeout<{
      success: boolean;
      error?: string;
    }>(
      { type: 'SAVE_ARCHIVE' },
      A12_A13_SAVE_ARCHIVE_TIMEOUT_MS,
      'SAVE_ARCHIVE',
    );
    diag(result, `Step 1 result: ${JSON.stringify(resp)}`);

    result.checks.push({
      name: 'A12.1: SAVE_ARCHIVE handler returns success=true (zip path will be ffprobe-validated host-side)',
      expected: true,
      actual: resp.success,
      passed: resp.success === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/**
 * A13 — page-side: dispatch SAVE_ARCHIVE so a new zip lands in
 * `downloadsDir`. Host-side driveA13 then:
 *   1. polls downloadsDir for the new zip (snapshot delta).
 *   2. parses with JSZip (`assertArchiveShape` in tests/uat/lib/zip.ts
 *      already encodes the full contract — A13 reuses it).
 *   3. asserts `video/last_30sec.webm` entry present + size >= 1 KB,
 *      `meta.json` entry present + parses as JSON,
 *      `meta.json.extensionVersion === chrome.runtime.getManifest().version`
 *      (the harness's `getManifestVersion` helper is called once at
 *      orchestrator startup; driveA13 receives the expected version
 *      via closure).
 *
 * The SessionMetadata shape in src/shared/types.ts:103 names the
 * field `extensionVersion` (NOT `version`); the `assertArchiveShape`
 * helper in tests/uat/lib/zip.ts:25 currently models it as `version`
 * — A13's driver passes the right field name (Wave 3D updates the
 * helper to read `extensionVersion`, since it's the actual production
 * field per src/background/index.ts:572).
 *
 * Plan 01-09 Amendment 3 (2026-05-19, debug session
 * 01-09-save-does-not-stop-recording) update: the SAVE_ARCHIVE auto-stop
 * was REVERSED. Under the new charter SAVE does NOT stop the recorder,
 * so A12's SAVE leaves the recording live and A13 could in principle
 * SAVE directly against A12's still-buffered segments. We KEEP the
 * setupFreshRecording + 11s settle here as a defensive guarantee that
 * A13 sees a known-good fresh-rotation buffer regardless of upstream
 * assertion ordering (A12 itself might fail mid-flight or be reordered
 * by a future maintainer; this isolation keeps A13's contract orthogonal).
 * The 11s wall-clock cost is preserved — same as before Amendment 3.
 *
 * @returns Structured result with checks (SETUP + A13.1).
 */
async function assertA13(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A13 — SAVE_ARCHIVE zip shape: webm entry + meta.json + extensionVersion match (host-side gate)',
    checks: [],
    diagnostics: [],
  };

  try {
    // Plan 01-09 Amendment 3 (2026-05-19): SAVE_ARCHIVE no longer
    // auto-stops recording (charter reversed). A13 still re-establishes
    // a fresh recording as a defensive guarantee — see function-level
    // docstring above for rationale. Without the setupFreshRecording +
    // settle, if A13 were ever reordered ahead of A11 or run in
    // isolation against a buffer with stale/missing segments,
    // saveArchive would throw EmptyVideoBufferError.
    diag(result, 'Step 1: setupFreshRecording (defensive — guarantees fresh buffer for A13 SAVE)');
    const setupResp = await setupFreshRecording();
    if (!setupResp.ok) {
      throw new Error(
        `setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`,
      );
    }
    diag(result, 'Step 1 OK — fresh recording active');
    result.checks.push({
      name: 'SETUP: fresh recording established (defensive — orthogonal to A12 ordering)',
      expected: true,
      actual: true,
      passed: true,
    });

    // Step 2 — segment-settle. Same rationale as A5 (line ~890): the
    // offscreen recorder rotates segments every SEGMENT_DURATION_MS
    // (10s). Before the first rotation, `segments` is empty and
    // `getVideoBufferFromOffscreen` returns `{segments:[]}` which
    // createArchive treats as EmptyVideoBufferError. Wait 11s so at
    // least one segment lands in the buffer.
    diag(result, `Step 2: settle ${A5_SEGMENT_SETTLE_MS}ms for first segment rotation`);
    await new Promise((r) => setTimeout(r, A5_SEGMENT_SETTLE_MS));
    diag(result, 'Step 2 OK — first rotation should have fired');

    diag(result, 'Step 3: send SAVE_ARCHIVE to SW (own fresh-recording save)');
    const resp = await sendMessageWithTimeout<{
      success: boolean;
      error?: string;
    }>(
      { type: 'SAVE_ARCHIVE' },
      A12_A13_SAVE_ARCHIVE_TIMEOUT_MS,
      'SAVE_ARCHIVE',
    );
    diag(result, `Step 3 result: ${JSON.stringify(resp)}`);

    result.checks.push({
      name: 'A13.1: SAVE_ARCHIVE handler returns success=true (zip shape verified host-side)',
      expected: true,
      actual: resp.success,
      passed: resp.success === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/** Settle window for the SW state machine after SAVE_ARCHIVE completes.
 *  Under Amendment 3 (2026-05-19, charter reversed) saveArchive has no
 *  finally block — the state machine should remain in REC throughout.
 *  The 500ms settle is preserved for cross-event-loop quiescence of
 *  any in-flight chrome.action.* postings; the observed lag is
 *  typically a few ms. */
const A14_POST_SAVE_SETTLE_MS = 500;

/** Expected popup path suffix in REC mode. setRecordingMode pins the
 *  popup to POPUP_HTML_PATH ('src/popup/index.html'); chrome.action.getPopup
 *  resolves this to an absolute chrome-extension://<ext-id>/<path> URL.
 *  We assert via endsWith to stay ext-id-agnostic. */
const A14_POPUP_HTML_SUFFIX = 'src/popup/index.html';

/**
 * A14 — post-SAVE continuous-recording state check. Plan 01-09 Amendment 3
 * (2026-05-19, debug session 01-09-save-does-not-stop-recording).
 * INVERTED from the prior Amendment 2 contract which asserted post-SAVE
 * IDLE; under the reversed charter the SW MUST remain in REC after SAVE.
 *
 * Verifies that A13's SAVE_ARCHIVE (and by extension every SAVE) leaves
 * the SW state machine UNCHANGED — recording continues:
 *   - badge text === 'REC' (setRecordingMode from the earlier
 *     setupFreshRecording is still in effect; no setIdleMode was called)
 *   - popup endsWith 'src/popup/index.html' (setRecordingMode pinned it;
 *     getPopup resolves to chrome-extension://<ext-id>/src/popup/index.html)
 *   - no NEW notification with the 'mokosh-recovery-' prefix surfaced
 *     since A14 entered (delta-based — A7 left a recovery notification
 *     in the active set; SAVE must NOT add another one, per the
 *     "save is not an error" contract — same as the prior Amendment 2
 *     contract, regression-preserved)
 *
 * Pre-condition: A13 just completed. A13 calls setupFreshRecording so
 * the state machine entered REC for that assertion; under the REVERSED
 * charter saveArchive does not transition out of REC, so the state
 * remains REC when A14 reads it.
 *
 * Post-condition: no state change — A14 is read-only.
 *
 * Direct isRecording check is transitively verified via the presence of
 * the REC badge — the production SW state machine
 * (src/background/index.ts:setRecordingMode/setIdleMode/setErrorMode)
 * pairs isRecording transitions with badge transitions atomically, so
 * badge='REC' is a reliable proxy for isRecording=true.
 *
 * @returns Structured result with 3 checks (badge + popup + no-new-recovery-notif).
 */
async function assertA14(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A14 — post-SAVE continuous-recording: badge=\'REC\' + popup endsWith popup.html + no new mokosh-recovery-* notif',
    checks: [],
    diagnostics: [],
  };

  try {
    // Snapshot the recovery-notification ids BEFORE the A14 settle.
    // A7 left at least one recovery-* in the active set; the delta we
    // care about is "did any NEW recovery surface since A13's SAVE
    // completed". Under the REVERSED charter A13's SAVE does NOT
    // dispatch STOP_RECORDING/setIdleMode; the SW stays in REC. The
    // empty-buffer-error branch is not exercised under A13's happy
    // path (it ran setupFreshRecording + 11s settle so segments
    // are non-empty), so no RECORDING_ERROR is broadcast and no
    // recovery notification surfaces.
    diag(result, 'Step 1: snapshot mokosh-recovery-* notification ids (delta baseline)');
    const idsBefore = await getActiveNotificationIds();
    const recoveryIdsBefore = idsBefore.filter(
      (id) => id.startsWith(RECOVERY_NOTIF_PREFIX),
    );
    diag(
      result,
      `Step 1 result: ${recoveryIdsBefore.length} active recovery-prefix ids: ${JSON.stringify(recoveryIdsBefore)}`,
    );

    diag(result, `Step 2: settle ${A14_POST_SAVE_SETTLE_MS}ms for post-A13 state machine to quiesce`);
    await new Promise((r) => setTimeout(r, A14_POST_SAVE_SETTLE_MS));

    diag(result, 'Step 3: read post-SAVE state (badge + popup + recovery ids delta)');
    const badge = await chrome.action.getBadgeText({});
    const popup = await chrome.action.getPopup({});
    const idsAfter = await getActiveNotificationIds();
    const recoveryIdsAfter = idsAfter.filter(
      (id) => id.startsWith(RECOVERY_NOTIF_PREFIX),
    );
    const recoveryDelta = recoveryIdsAfter.length - recoveryIdsBefore.length;
    diag(
      result,
      `Step 3 result: badge='${badge}', popup='${popup}', recoveryDelta=${recoveryDelta} (before=${recoveryIdsBefore.length}, after=${recoveryIdsAfter.length})`,
    );

    result.checks.push({
      name: 'A14.1: badge text is \'REC\' after SAVE_ARCHIVE (recording continues; setRecordingMode persists)',
      expected: 'REC',
      actual: badge,
      passed: badge === 'REC',
    });
    // chrome.action.getPopup() returns an absolute chrome-extension://
    // URL ending in the registered path (POPUP_HTML_PATH = 'src/popup/index.html').
    // Assert via endsWith to stay extension-id-agnostic.
    result.checks.push({
      name: `A14.2: popup endsWith '${A14_POPUP_HTML_SUFFIX}' after SAVE_ARCHIVE (setRecordingMode still pinned; onClicked stays inert)`,
      expected: A14_POPUP_HTML_SUFFIX,
      actual: popup,
      passed: popup.endsWith(A14_POPUP_HTML_SUFFIX),
    });
    result.checks.push({
      name: 'A14.3: NO new mokosh-recovery-* notification (save is not an error)',
      expected: 0,
      actual: recoveryDelta,
      passed: recoveryDelta === 0,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/* ─── Plan 01-10 Wave 3 — A15 + A16 + A17 (onboarding + design-swap-readiness) ───
 *
 * Plan 01-10 D-17-onboarding harness extensions:
 *   A15 — onboarding flag observability: chrome.storage.local
 *         'onboarding-completed' === true AND 'installed-at' is a
 *         number (SW's openWelcomeIfFirstInstall side-effects observed
 *         from extension-internal context with full chrome.* privilege).
 *   A16 — subsequent-install does NOT re-open welcome tab: snapshot
 *         chrome.tabs.query before a 2-second settle window; assert no
 *         new welcome.html tabs appear (flag-gating contract: the SW's
 *         onInstalled handler running again across SW respawns must
 *         NOT spawn additional welcome tabs once the flag is set).
 *   A17 — design-swap-readiness invariant (EXTENDED per Plan 01-12
 *         path-B revision): welcome.html parses + .welcome-hero slot
 *         exists + ≥7 mokosh-keyed attrs + welcome.css carries the
 *         canonical @import directive (or inlined evidence) + zero
 *         #hex literals in welcome.css source-file region + ≥5
 *         var(--mks-*) references + bundled welcome JS contains COPY[
 *         OR chrome.i18n.getMessage('welcomeHero pattern + A17.7's
 *         getComputedStyle probe resolves --mks-rec to a non-default
 *         value (canonical rgb(178, 84, 61) = #b2543d).
 *
 * Each assertion uses ONLY production chrome.* APIs + fetch + DOMParser
 * + getComputedStyle — NO new test-mode symbols are introduced (Tier-1
 * FORBIDDEN_HOOK_STRINGS stays at 12 post-01-14).
 */

/**
 * A15 — onboarding flag observability. Read-only inspection of
 * chrome.storage.local for the two keys Plan 01-10 Wave 2's
 * openWelcomeIfFirstInstall sets on first install:
 *   - 'onboarding-completed' === true
 *   - 'installed-at' === <number> (Date.now() at install)
 *
 * The flag-gating contract this verifies is: the SW's onInstalled
 * handler ran with reason='install' AT LEAST ONCE this session AND
 * the helper completed both chrome.storage.local.set + chrome.tabs.create
 * before A15 fires. The harness page is opened as a normal tab during
 * `launchHarnessBrowser` AFTER Chrome has loaded the extension, so by
 * the time A15 fires the chrome.runtime.onInstalled.addListener has
 * already fired with reason='install' (first-install path) and the
 * helper has had ample time to complete its three awaited calls.
 *
 * Mirrors `assertA22` style: bridge-free read of public chrome.storage.local.
 *
 * @returns Structured result with 2 checks (flag === true + installed-at number).
 */
async function assertA15(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: "A15 — onboarding flag set on first install (chrome.storage.local 'onboarding-completed' === true; 'installed-at' is number)",
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, "Step 1: chrome.storage.local.get(['onboarding-completed', 'installed-at'])");
    const stored = await chrome.storage.local.get([
      'onboarding-completed',
      'installed-at',
    ]);
    diag(result, `Step 1 result: ${JSON.stringify(stored)}`);

    result.checks.push({
      name: "A15.1: chrome.storage.local 'onboarding-completed' === true (set by openWelcomeIfFirstInstall on first install)",
      expected: true,
      actual: stored['onboarding-completed'],
      passed: stored['onboarding-completed'] === true,
    });
    result.checks.push({
      name: "A15.2: chrome.storage.local 'installed-at' is a number (Date.now() recorded at install)",
      expected: 'number',
      actual: typeof stored['installed-at'],
      passed: typeof stored['installed-at'] === 'number',
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/** A16 — subsequent-install settle window (welcome tab does NOT spontaneously
 *  reappear). Snapshot before/after a 2-second window; the delta MUST be 0
 *  because the openWelcomeIfFirstInstall helper's flag-gating prevents
 *  re-fire even if the SW spawns multiple onInstalled events across
 *  respawn cycles. 2 s is a generous over-budget; the SW's storage.get
 *  + storage.set + tabs.create round-trip is sub-100ms. */
const A16_SETTLE_MS = 2_000;
const A16_WELCOME_URL_SUFFIX = 'src/welcome/welcome.html';

/**
 * A16 — subsequent-install does NOT re-open welcome tab. Snapshot
 * chrome.tabs.query({}) for welcome.html tab URLs before a settle
 * window, then again after, and assert no NEW welcome tabs appeared.
 *
 * Implementation note: chrome.tabs.query without filters returns all
 * tabs across all windows. We filter to URLs ending with the
 * '/src/welcome/welcome.html' suffix (extension-id-agnostic).
 *
 * @returns Structured result with 1 check (delta === 0).
 */
async function assertA16(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A16 — subsequent install does NOT re-open welcome tab (2s settle window: no new welcome.html tabs appear)',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: snapshot chrome.tabs.query({}) — count welcome.html tabs BEFORE settle');
    const tabsBefore = await chrome.tabs.query({});
    const beforeCount = tabsBefore.filter(
      (t) => typeof t.url === 'string' && t.url.endsWith(A16_WELCOME_URL_SUFFIX),
    ).length;
    diag(result, `Step 1 result: ${beforeCount} welcome tab(s) before settle`);

    diag(result, `Step 2: settle ${A16_SETTLE_MS}ms (sufficient for any spurious onInstalled re-fire to flush)`);
    await new Promise((r) => setTimeout(r, A16_SETTLE_MS));

    diag(result, 'Step 3: re-snapshot chrome.tabs.query({}) — count welcome.html tabs AFTER settle');
    const tabsAfter = await chrome.tabs.query({});
    const afterCount = tabsAfter.filter(
      (t) => typeof t.url === 'string' && t.url.endsWith(A16_WELCOME_URL_SUFFIX),
    ).length;
    diag(result, `Step 3 result: ${afterCount} welcome tab(s) after settle (delta=${afterCount - beforeCount})`);

    const delta = afterCount - beforeCount;
    result.checks.push({
      name: 'A16.1: welcome-tab count delta over 2s settle === 0 (onInstalled flag-gating works)',
      expected: 0,
      actual: delta,
      passed: delta === 0,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/** A17 invariants — design-swap-readiness contract from Plan 01-10
 *  must_have #9. Each constant captures one numeric threshold used in
 *  the A17 sub-checks.
 *
 *  A17.3: zero hex literals in welcome.css source-file region — relaxed
 *  per post-01-12 revision: if Vite inlines @import contents, hex
 *  literals from the canonical token values may surface in the BUILT
 *  artifact. Pass criteria becomes (no hex OR canonical-tokens-resolved).
 *  A17.4: ≥ 5 var(--mks-*) references.
 *  A17.5: @import '../shared/tokens.css' literal in welcome.css OR
 *  inlined evidence via '--mks-rec' literal in the compiled bundle.
 *  A17.6: bundled JS contains 'COPY[' OR 'chrome.i18n.getMessage('
 *  with welcomeHero substring (either pattern proves populate plumbing).
 *  A17.7: getComputedStyle probe — --mks-rec resolves to canonical
 *  rgb(178, 84, 61). The harness page <link>s tokens.css directly (Plan
 *  01-12 Wave 6); the probe creates a transient div, applies the var,
 *  reads computed color.
 */
const A17_MIN_KEYED_ATTRS = 7;
const A17_MIN_VAR_USES = 5;
const A17_CANONICAL_REC_RGB = 'rgb(178, 84, 61)';

/**
 * A17 — design-swap-readiness invariant. Multi-step assertion verifying
 * welcome.html + welcome.css + the bundled welcome JS chunk all hold
 * the contracts that Plan 01-10's path-B (canonical tokens import) +
 * chrome.i18n adoption depend on.
 *
 * Reaches the bundled JS via parsing the <script src> in welcome.html
 * (Vite emits hashed chunk names; resolving relative-to-welcome.html
 * stays bundle-agnostic). Runs the --mks-rec probe in the harness page
 * context where tokens.css is <link>-loaded directly (Plan 01-12 Wave 6
 * line 15 of extension-page-harness.html).
 *
 * @returns Structured result with 7 sub-checks (A17.1..A17.7).
 */
async function assertA17(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A17 — design-swap-readiness: welcome.html parses; .welcome-hero exists; ≥7 mokosh-keyed attrs; welcome.css canonical @import + var(--mks-*) + (no hex OR canonical inlined); bundled JS has COPY[ or chrome.i18n.getMessage(welcomeHero; --mks-rec resolves; canonical mark SVG bundled + fetchable (A17.8)',
    checks: [],
    diagnostics: [],
  };

  try {
    const welcomeUrl = chrome.runtime.getURL('src/welcome/welcome.html');
    diag(result, `Step 1: fetch ${welcomeUrl}`);
    const htmlRes = await fetch(welcomeUrl);
    if (!htmlRes.ok) {
      throw new Error(`welcome.html returned HTTP ${htmlRes.status} ${htmlRes.statusText}`);
    }
    const htmlText = await htmlRes.text();
    const parsed = new DOMParser().parseFromString(htmlText, 'text/html');
    const hero = parsed.querySelector('.welcome-hero');
    diag(result, `Step 1 result: ${htmlText.length}-byte HTML; .welcome-hero ${hero === null ? '<missing>' : '<found>'}`);

    result.checks.push({
      name: 'A17.1: welcome.html parses + .welcome-hero slot exists',
      expected: 'non-null',
      actual: hero === null ? 'null' : 'element',
      passed: hero !== null,
    });

    diag(result, 'Step 2: count data-mokosh-key + data-mokosh-i18n-key attributes');
    const dataKeyMatches = htmlText.match(/data-mokosh-key=/g) ?? [];
    const dataI18nKeyMatches = htmlText.match(/data-mokosh-i18n-key=/g) ?? [];
    const totalKeyedAttrs = dataKeyMatches.length + dataI18nKeyMatches.length;
    diag(result, `Step 2 result: data-mokosh-key=${dataKeyMatches.length}, data-mokosh-i18n-key=${dataI18nKeyMatches.length}, total=${totalKeyedAttrs}`);

    result.checks.push({
      name: `A17.2: welcome.html has >= ${A17_MIN_KEYED_ATTRS} data-mokosh-key + data-mokosh-i18n-key attributes combined`,
      expected: `>= ${A17_MIN_KEYED_ATTRS}`,
      actual: `${totalKeyedAttrs} (data-mokosh-key=${dataKeyMatches.length}, data-mokosh-i18n-key=${dataI18nKeyMatches.length})`,
      passed: totalKeyedAttrs >= A17_MIN_KEYED_ATTRS,
    });

    // Resolve the welcome.css URL via the parsed <link> tag (Vite
    // rebases hashed asset filenames; stay agnostic to the hash).
    const linkEl = parsed.querySelector('link[rel="stylesheet"][href]');
    const cssHref = linkEl?.getAttribute('href') ?? '';
    if (cssHref.length === 0) {
      throw new Error('no <link rel="stylesheet" href> in welcome.html');
    }
    const baseUrl = new URL(welcomeUrl);
    const cssUrl = new URL(cssHref, baseUrl).href;
    diag(result, `Step 3: fetch ${cssUrl}`);
    const cssRes = await fetch(cssUrl);
    if (!cssRes.ok) {
      throw new Error(`welcome.css returned HTTP ${cssRes.status} ${cssRes.statusText}`);
    }
    const cssText = await cssRes.text();
    diag(result, `Step 3 result: ${cssText.length}-byte CSS`);

    const hexMatches = cssText.match(/#[0-9a-fA-F]{3,8}/g) ?? [];
    const hasCanonicalImportLiteral =
      cssText.includes("@import '../shared/tokens.css'")
      || cssText.includes('@import "../shared/tokens.css"');
    const hasCanonicalInlined = cssText.includes('--mks-rec');
    const hasCanonicalResolution = hasCanonicalImportLiteral || hasCanonicalInlined;
    diag(result, `Step 4: hex=${hexMatches.length}, importLiteral=${hasCanonicalImportLiteral}, inlinedTokens=${hasCanonicalInlined}`);

    result.checks.push({
      name: 'A17.3: welcome.css source has zero hex literals OR canonical @import resolves',
      expected: '0 hex OR canonical-tokens-resolved',
      actual: `hex=${hexMatches.length}, canonicalResolved=${hasCanonicalResolution}`,
      passed: hexMatches.length === 0 || hasCanonicalResolution,
    });

    const varMatches = cssText.match(/var\(--mks-/g) ?? [];
    result.checks.push({
      name: `A17.4: welcome.css contains >= ${A17_MIN_VAR_USES} var(--mks- references`,
      expected: `>= ${A17_MIN_VAR_USES}`,
      actual: String(varMatches.length),
      passed: varMatches.length >= A17_MIN_VAR_USES,
    });
    result.checks.push({
      name: "A17.5: welcome.css has canonical tokens @import (or inlined --mks-* evidence)",
      expected: "@import '../shared/tokens.css' OR --mks-* inlined",
      actual: hasCanonicalResolution ? 'present' : 'absent',
      passed: hasCanonicalResolution,
    });

    // Locate the bundled welcome JS chunk via the parsed <script src>.
    const scriptEl = parsed.querySelector('script[type="module"][src]');
    const scriptSrc = scriptEl?.getAttribute('src') ?? '';
    if (scriptSrc.length === 0) {
      throw new Error('no <script type="module" src> in welcome.html');
    }
    const jsUrl = new URL(scriptSrc, baseUrl).href;
    diag(result, `Step 5: fetch ${jsUrl}`);
    const jsRes = await fetch(jsUrl);
    if (!jsRes.ok) {
      throw new Error(`welcome JS chunk returned HTTP ${jsRes.status} ${jsRes.statusText}`);
    }
    const jsText = await jsRes.text();
    diag(result, `Step 5 result: ${jsText.length}-byte JS`);

    // A17.6: prove the populate plumbing survives the build. Vite's
    // terser/Rollup minifier rewrites local variable names (e.g.
    // `COPY` → `f`; `COPY[key]` → `f[e.key]`) AND inserts optional
    // chaining at chrome.i18n call sites (e.g. `chrome?.i18n?.getMessage?.(`
    // per the welcome.ts source pattern matching the popup precedent).
    // Substring matching the verbatim source identifiers is brittle
    // against these transforms.
    //
    // Two minification-survivable witnesses are checked:
    //   1. COPY map content fingerprint — the literal string keys from
    //      the source COPY map are preserved verbatim through terser
    //      (they are Object.freeze literal keys; mangling skips
    //      object literals). Match 'welcome.page.title' which is one
    //      of the six non-tagline keys defined in src/welcome/copy.ts.
    //   2. chrome.i18n.getMessage call (with or without optional
    //      chaining) — minifier preserves `chrome` (global) and
    //      `i18n.getMessage` (property access; can't be renamed across
    //      module boundaries). Match `i18n` AND `getMessage` AND
    //      either `welcomeHero` literal key (Object.freeze fingerprint
    //      from the fallbacks Object.freeze) or `WELCOME_HERO_RU_FALLBACK`
    //      const name (in dev/non-minified builds).
    const hasCopyKeyLiteral = jsText.includes('welcome.page.title');
    const hasI18nWelcomeHero =
      jsText.includes('i18n')
      && jsText.includes('getMessage')
      && jsText.includes('welcomeHero');
    result.checks.push({
      name: 'A17.6: bundled JS preserves populate plumbing (COPY key literal AND chrome.i18n welcomeHero call site)',
      expected: 'COPY key literal OR chrome.i18n.getMessage welcomeHero references',
      actual: `copyKey('welcome.page.title')=${hasCopyKeyLiteral}, i18nWelcomeHero=${hasI18nWelcomeHero}`,
      passed: hasCopyKeyLiteral || hasI18nWelcomeHero,
    });

    diag(result, 'Step 6: probe --mks-rec via getComputedStyle on a transient div');
    const probe = document.createElement('div');
    probe.style.color = 'var(--mks-rec)';
    document.body.appendChild(probe);
    const resolvedColor = getComputedStyle(probe).color;
    document.body.removeChild(probe);
    diag(result, `Step 6 result: getComputedStyle.color = '${resolvedColor}'`);

    // Accept canonical rgb(178, 84, 61) OR any non-default non-empty
    // value (the probe inheriting the browser default rgb(0, 0, 0)
    // would mean tokens didn't resolve through the page's stylesheet
    // chain). The harness page <link>s tokens.css directly per Plan
    // 01-12 Wave 6 line 15, so the canonical value SHOULD resolve.
    const resolvedNonDefault =
      resolvedColor.length > 0
      && resolvedColor !== 'rgb(0, 0, 0)'
      && !resolvedColor.includes('rgba(0, 0, 0, 0)');
    const resolvedCanonical = resolvedColor === A17_CANONICAL_REC_RGB;
    result.checks.push({
      name: `A17.7: --mks-rec resolves to non-default value via getComputedStyle (canonical=${A17_CANONICAL_REC_RGB} per D-04 Loom palette)`,
      expected: `non-default (preferably ${A17_CANONICAL_REC_RGB})`,
      actual: `${resolvedColor} (canonical=${resolvedCanonical})`,
      passed: resolvedNonDefault,
    });

    // A17.8: Plan 01-10 must_have #9 path-A swap-in invariant (landed
    // 2026-05-20 per debug session 01-10-welcome-page-missing-mark).
    // Verifies the canonical Mokosh mark SVG is bundled into the
    // welcome chunk so populateMark() can assign it as the <img src>.
    //
    // Vite's default behaviour (build.assetsInlineLimit = 4096 bytes,
    // confirmed via vite.dev/config/build-options.html#build-assetsinlinelimit)
    // inlines assets smaller than the limit as data: URLs. The
    // canonical mokosh-mark.svg is ~600 bytes, so it's INLINED as a
    // `data:image/svg+xml,...` literal inside the welcome JS chunk
    // (NOT emitted as a separate `dist/assets/<hash>.svg` file).
    //
    // We accept BOTH bundling shapes — either is correct from a "the
    // mark is reachable from the welcome page" standpoint:
    //   (a) data URL: `data:image/svg+xml,...` substring in jsText
    //       (Vite inlined small asset path; default behaviour).
    //   (b) file URL: a `.svg` filename string in jsText (Vite emitted
    //       separate asset path; would activate if SVG grew past
    //       4096 bytes OR assetsInlineLimit was lowered).
    //
    // If neither shape is present, populateMark() would assign
    // `img.src = undefined` and the welcome hero would render an
    // empty/broken image — exactly the regression the operator
    // reported in the 2026-05-20 UAT.
    const hasInlineDataUrl = jsText.includes('data:image/svg+xml');
    const svgFileUrlMatches = jsText.match(/["'][^"']*\.svg["']/g) ?? [];
    const hasSvgFileUrl = svgFileUrlMatches.length > 0;
    const hasBundledMark = hasInlineDataUrl || hasSvgFileUrl;
    diag(result, `Step 7: bundled JS contains inlineDataUrl=${hasInlineDataUrl}, svgFileUrlCount=${svgFileUrlMatches.length}`);

    // Cross-witness: the canonical mark's source SVG includes the
    // viewBox="0 0 32 32" literal (32×32 woven-square mark per
    // src/shared/brand/mokosh-mark.svg). The data URL inlining
    // path preserves this verbatim (URL-percent-encoded:
    // viewBox='0%200%2032%2032'). For the file URL path the
    // substring lives at the fetched asset, not the chunk JS.
    // Either way, the chunk JS string is sufficient to prove the
    // mark survives the bundle.
    const hasCanonicalViewBox =
      jsText.includes('viewBox=\'0 0 32 32\'')
      || jsText.includes('viewBox="0 0 32 32"')
      || jsText.includes('viewBox=%270%200%2032%2032%27')
      || jsText.includes("viewBox='0%200%2032%2032'");

    result.checks.push({
      name: 'A17.8: welcome chunk JS bundles the canonical mark SVG (data URL OR file URL) AND canonical viewBox preserved (Plan 01-10 must_have #9 path-A swap-in)',
      expected: 'data:image/svg+xml OR .svg URL in bundle; canonical viewBox=\'0 0 32 32\' preserved',
      actual: `inlineDataUrl=${hasInlineDataUrl}, svgFileUrl=${hasSvgFileUrl}, canonicalViewBox=${hasCanonicalViewBox}`,
      passed: hasBundledMark && hasCanonicalViewBox,
    });

    result.passed = result.checks.every((c) => c.passed);
    diag(result, `A17: ${result.checks.filter((c) => c.passed).length}/${result.checks.length} subchecks passed`);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/**
 * A23 — Plan 01-14 picker-narrowing constraint verification.
 *
 * Asserts that the production `getDisplayMedia` call at
 * src/offscreen/recorder.ts:270 passes `monitorTypeSurfaces: 'include'`
 * as a top-level constraint sibling of `video:` (W3C Screen Capture
 * spec §6.1; Chrome ≥ 119 picker-narrowing semantics — only monitor
 * surfaces are offered, no Window/Chrome-Tab panes).
 *
 * Queries the offscreen bridge `get-last-getDisplayMedia-constraints` op
 * which reads the recorded constraints from the `fakeGetDisplayMedia`
 * shim's last invocation. A23 chains AFTER A14 (A14 is the final read-
 * only post-SAVE state check; A23 is independent because A2's
 * setupFreshRecording already invoked getDisplayMedia and the recorded
 * cell is read-only from A23's perspective — no side effects).
 *
 * Mirrors `assertA3` (line 686): bridge query → structured AssertionResult
 * with two checks (non-null constraints + monitorTypeSurfaces value).
 *
 * @returns Structured result with the monitorTypeSurfaces check.
 */
async function assertA23(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A23 — getDisplayMedia called with monitorTypeSurfaces:\'include\' (Plan 01-14 picker narrowing)',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, "Step 1: bridge query 'get-last-getDisplayMedia-constraints'");
    const resp = await offscreenQuery<{
      constraints?: DisplayMediaStreamOptions | null;
      ok?: boolean;
      error?: string;
    }>('get-last-getDisplayMedia-constraints');
    diag(result, `Step 1 result: ${JSON.stringify(resp)}`);

    if (resp.ok === false) {
      throw new Error(
        `get-last-getDisplayMedia-constraints returned ok=false: ${resp.error ?? '(no error)'}`,
      );
    }
    const constraints = resp.constraints ?? null;

    result.checks.push({
      name: 'A23.1: constraints object recorded by fakeGetDisplayMedia (non-null)',
      expected: 'non-null DisplayMediaStreamOptions',
      actual: constraints === null ? '<null>' : JSON.stringify(constraints),
      passed: constraints !== null,
    });
    // The constraints object MAY be widened by the production cast (per
    // Plan 01-14 source change) to include the top-level field, so we
    // dot-access via the same indexed-property shape (`as any`) since
    // monitorTypeSurfaces is not in the lib.dom.d.ts DisplayMediaStreamOptions
    // ambient (TypeScript bundled types lag the W3C spec — same lag that
    // forced the `cursor: 'always'` typed-widening cast on recorder.ts:268).
    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- constraints widening for top-level monitorTypeSurfaces sibling per W3C spec §6.1 (lib.dom.d.ts lag)
    const monitorTypeSurfaces = (constraints as any)?.monitorTypeSurfaces ?? null;
    result.checks.push({
      name: 'A23.2: constraints.monitorTypeSurfaces === \'include\' (W3C spec §6.1; Chrome ≥ 119 picker narrowing)',
      expected: 'include',
      actual: monitorTypeSurfaces,
      passed: monitorTypeSurfaces === 'include',
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/* ─── Wave 6 — A18 + A19 + A20 + A21 + A22 ─────────────────────────────
 *
 * Plan 01-12 Wave 6 design-integration harness extensions:
 *   A18 — Lora WOFF2 reachability + size floor (font self-host invariant)
 *   A19 — Loom icons NOT the prior Bug A placeholders (icon-overwrite
 *         invariant)
 *   A20 — manifest:name resolves via chrome i18n to 'Mokosh — Session
 *         Capture' (default_locale='en' fallback chain)
 *   A21 — getComputedStyle on .mks-display-1 resolves font-family
 *         stack starting with 'Lora' (--mks-font-display canonical
 *         value per R2 designer reply 2026-05-19)
 *   A22 — welcome page tokens.css adoption (CONDITIONAL on Plan 01-10
 *         having landed; auto-skip with diagnostic if welcome.html 404s)
 *
 * Each assertion uses ONLY production chrome.* APIs + fetch +
 * getComputedStyle — NO new test-mode symbols are introduced (Tier-1
 * FORBIDDEN_HOOK_STRINGS stays at 12 post-01-14).
 */

/** A18 — Lora WOFF2 reachability + size floor. Vite emits the WOFF2
 *  files under dist-test/assets/<hash>.woff2 with content-hashed names;
 *  walk document.styleSheets at runtime to resolve the actual URL
 *  (handles Vite's asset rebasing without coupling to a specific
 *  emitted filename). Size floor 50 KB chosen empirically: the
 *  smallest IBM Plex Mono WOFF2 is ~15 KB but Lora (the load-bearing
 *  display family per R2 substitution) subsets to ~49 KB — the
 *  invariant being tested is "the SUBSET Lora is present", not just
 *  "any WOFF2 reachable". */
const A18_LORA_MIN_BYTES = 40_000;

async function assertA18(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A18 — Lora WOFF2 reachable from harness page (font self-host MV3 CSP invariant)',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: walk document.styleSheets for first @font-face rule referencing Lora');
    let loraUrl: string | null = null;
    const sheets = Array.from(document.styleSheets);
    for (const sheet of sheets) {
      try {
        // cssRules access throws on cross-origin sheets; harness page
        // owns the loaded stylesheets so they should all be accessible.
        const rules = Array.from(sheet.cssRules);
        for (const rule of rules) {
          if (rule.constructor.name === 'CSSFontFaceRule') {
            const src = (rule as CSSFontFaceRule).style.getPropertyValue('src');
            const match = src.match(/url\(["']?([^"')]*Lora[^"')]*\.woff2)["']?\)/);
            if (match !== null) {
              loraUrl = match[1];
              break;
            }
          }
        }
      } catch (sheetErr) {
        diag(result, `(skip sheet — cssRules threw: ${(sheetErr as Error).message})`);
      }
      if (loraUrl !== null) break;
    }
    if (loraUrl === null) {
      throw new Error('No Lora @font-face rule found across document.styleSheets');
    }
    diag(result, `Step 1 result: loraUrl=${loraUrl}`);

    result.checks.push({
      name: 'A18.1: Lora @font-face rule present in a stylesheet (tokens.css adoption)',
      expected: 'src url(...Lora....woff2) matched',
      actual: loraUrl,
      passed: true,
    });

    diag(result, `Step 2: fetch ${loraUrl}`);
    const resolvedUrl = new URL(loraUrl, document.location.href).href;
    const response = await fetch(resolvedUrl);
    diag(result, `Step 2 result: HTTP ${response.status} ${response.statusText}`);
    result.checks.push({
      name: 'A18.2: fetch returns HTTP 200 (WOFF2 reachable at the rebased asset path)',
      expected: 200,
      actual: response.status,
      passed: response.ok,
    });
    if (!response.ok) {
      result.passed = false;
      return result;
    }

    diag(result, 'Step 3: read arrayBuffer + assert byteLength floor');
    const buf = await response.arrayBuffer();
    diag(result, `Step 3 result: byteLength=${buf.byteLength}`);
    result.checks.push({
      name: `A18.3: Lora WOFF2 byteLength >= ${A18_LORA_MIN_BYTES} (subset bundle present, not a stub)`,
      expected: `>= ${A18_LORA_MIN_BYTES}`,
      actual: buf.byteLength,
      passed: buf.byteLength >= A18_LORA_MIN_BYTES,
    });

    // Additional sanity: WOFF2 signature 'wOF2' = 0x77 0x4F 0x46 0x32
    const head = new Uint8Array(buf, 0, 4);
    const isWoff2 = head[0] === 0x77 && head[1] === 0x4f && head[2] === 0x46 && head[3] === 0x32;
    result.checks.push({
      name: "A18.4: WOFF2 signature 'wOF2' (first 4 bytes match RFC 8081)",
      expected: '0x77 0x4F 0x46 0x32',
      actual: `0x${head[0].toString(16).padStart(2, '0')} 0x${head[1].toString(16).padStart(2, '0')} 0x${head[2].toString(16).padStart(2, '0')} 0x${head[3].toString(16).padStart(2, '0')}`,
      passed: isWoff2,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/** A19 — Loom icons NOT the Bug A placeholders. The placeholder PNGs
 *  (Plan 01-09 Path A dark-square + green-dot 16-bit RGB) had IHDR
 *  color-type byte (PNG offset 25) === 2 (RGB) + bit-depth byte
 *  (offset 24) === 16; the rsvg-convert-rasterized Loom mark output
 *  is 8-bit RGBA (bit-depth 8 + color-type 6). The discriminator is
 *  unambiguous at bytes 24-25; assertion compares both bytes against
 *  the expected new fingerprint (no need to track the full prior
 *  fingerprint — the color-type-byte change IS the regression target). */
const A19_EXPECTED_BIT_DEPTH = 8;
const A19_EXPECTED_COLOR_TYPE = 6;  // RGBA

async function assertA19(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A19 — icons rasterized from Loom mark (8-bit RGBA, NOT 16-bit RGB Bug A placeholders)',
    checks: [],
    diagnostics: [],
  };

  try {
    const url = chrome.runtime.getURL('icons/icon128.png');
    diag(result, `Step 1: fetch ${url}`);
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`fetch ${url} returned HTTP ${response.status}`);
    }
    const buf = await response.arrayBuffer();
    diag(result, `Step 1 result: byteLength=${buf.byteLength}`);

    // PNG IHDR layout per ISO/IEC 15948 §11.2.2:
    //   bytes 0-7: signature
    //   bytes 8-11: chunk length
    //   bytes 12-15: chunk type ('IHDR')
    //   bytes 16-19: width
    //   bytes 20-23: height
    //   byte 24:    bit depth
    //   byte 25:    color type
    const bytes = new Uint8Array(buf, 0, 32);
    const bitDepth = bytes[24];
    const colorType = bytes[25];
    diag(result, `Step 2: IHDR bit_depth=${bitDepth} color_type=${colorType}`);

    result.checks.push({
      name: `A19.1: icon128.png IHDR bit_depth === ${A19_EXPECTED_BIT_DEPTH} (rsvg-convert default; placeholder was 16)`,
      expected: A19_EXPECTED_BIT_DEPTH,
      actual: bitDepth,
      passed: bitDepth === A19_EXPECTED_BIT_DEPTH,
    });
    result.checks.push({
      name: `A19.2: icon128.png IHDR color_type === ${A19_EXPECTED_COLOR_TYPE} (RGBA; placeholder was 2 — RGB)`,
      expected: A19_EXPECTED_COLOR_TYPE,
      actual: colorType,
      passed: colorType === A19_EXPECTED_COLOR_TYPE,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/** A20 — manifest:name resolves via chrome i18n. With default_locale='en',
 *  chrome.runtime.getManifest().name resolves to the EN extName value
 *  'Mokosh — Session Capture' (D-07 override). Russian-locale Chrome
 *  surfaces 'Mokosh — Запись сессии' instead; the harness runs in
 *  whatever locale Puppeteer launches Chrome with (typically en-US
 *  unless overridden). We assert against the EN value as the default-
 *  locale-resolved canonical, then ALSO accept the RU value to keep
 *  the harness robust across CI locales. */
const A20_EN_EXTNAME = 'Mokosh — Session Capture';
const A20_RU_EXTNAME = 'Mokosh — Запись сессии';

async function assertA20(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A20 — manifest:name resolves via chrome i18n (default_locale=en fallback chain)',
    checks: [],
    diagnostics: [],
  };

  try {
    const manifest = chrome.runtime.getManifest();
    diag(result, `Step 1: chrome.runtime.getManifest().name=${JSON.stringify(manifest.name)}`);

    const isResolved = manifest.name === A20_EN_EXTNAME || manifest.name === A20_RU_EXTNAME;
    result.checks.push({
      name: `A20.1: manifest.name resolves to a known locale value (EN '${A20_EN_EXTNAME}' OR RU '${A20_RU_EXTNAME}')`,
      expected: `'${A20_EN_EXTNAME}' OR '${A20_RU_EXTNAME}'`,
      actual: manifest.name,
      passed: isResolved,
    });

    // Bonus check: the raw __MSG_ placeholder should NEVER leak through
    // — if chrome.i18n is misconfigured (missing _locales/, wrong
    // default_locale, etc.) the literal '__MSG_extName__' surfaces as
    // the resolved name. Catch that class of regression explicitly.
    result.checks.push({
      name: "A20.2: manifest.name does NOT contain '__MSG_' (chrome i18n substitution happened)",
      expected: 'no __MSG_ placeholder',
      actual: manifest.name,
      passed: !manifest.name.includes('__MSG_'),
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/** A21 — `--mks-font-display` resolves to a font-family stack starting
 *  with Lora. The canonical token value per R2 designer reply
 *  2026-05-19 is `"Lora", "Iowan Old Style", "Times New Roman", serif`.
 *  Creates a transient probe div, applies `.mks-display-1` (which sets
 *  font-family: var(--mks-font-display)), reads getComputedStyle, and
 *  asserts the resolved font-family stack starts with 'Lora' or
 *  '"Lora"' (browsers normalize quoting differently — both forms
 *  acceptable). */
async function assertA21(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A21 — --mks-font-display resolves to Lora stack (R2 designer reply 2026-05-19)',
    checks: [],
    diagnostics: [],
  };

  let probe: HTMLDivElement | null = null;
  try {
    diag(result, "Step 1: create transient probe div with class='mks-display-1'");
    probe = document.createElement('div');
    probe.className = 'mks-display-1';
    probe.textContent = 'Probe';
    // Hide off-screen so the visual harness page doesn't shift layout.
    probe.style.position = 'absolute';
    probe.style.left = '-9999px';
    probe.style.top = '-9999px';
    document.body.appendChild(probe);

    // Force a layout so CSS resolves.
    void probe.offsetHeight;

    const computed = window.getComputedStyle(probe).fontFamily;
    diag(result, `Step 1 result: getComputedStyle(probe).fontFamily=${JSON.stringify(computed)}`);

    // Accept both quoted ('Lora') and unquoted (Lora) leading forms.
    // Chrome typically returns the family list with each member quoted
    // if it contains a space or non-identifier; Lora is identifier-safe
    // so it MAY be unquoted in the resolved stack. Belt-and-suspenders.
    const startsWithLora = /^("Lora"|Lora)/.test(computed);
    result.checks.push({
      name: "A21.1: getComputedStyle(.mks-display-1).fontFamily starts with Lora (no fallback chain hit)",
      expected: "starts with '\"Lora\"' OR 'Lora'",
      actual: computed,
      passed: startsWithLora,
    });

    // Also confirm Newsreader is absent — would indicate a stale
    // unmigrated tokens.css.
    const newsreaderAbsent = !/Newsreader/i.test(computed);
    result.checks.push({
      name: 'A21.2: resolved fontFamily does NOT contain Newsreader (R2 substitution complete)',
      expected: 'no Newsreader',
      actual: computed,
      passed: newsreaderAbsent,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  } finally {
    if (probe !== null && probe.parentNode !== null) {
      probe.parentNode.removeChild(probe);
    }
  }

  return result;
}

/** A22 — welcome page tokens.css adoption. CONDITIONAL on Plan 01-10
 *  having landed. If src/welcome/welcome.html is reachable, fetches it
 *  + the linked welcome.css and asserts substantive var(--mks-*) usage
 *  (regex /var\(--mks-[a-z-]+\)/g match count >= 3). If welcome.html
 *  returns HTTP 404, A22 PASSES with a 'Plan 01-10 not landed; skipped'
 *  diagnostic — mirrors the assertA12 ffprobe skip-gate pattern. */
const A22_MIN_TOKEN_USES = 3;

async function assertA22(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A22 — welcome page adopts canonical tokens.css (Plan 01-10 conditional; skip-gate on 404 OR fetch-failed)',
    checks: [],
    diagnostics: [],
  };

  try {
    const welcomeUrl = chrome.runtime.getURL('src/welcome/welcome.html');
    diag(result, `Step 1: HEAD-equivalent fetch ${welcomeUrl} (skip-gate probe)`);

    // Chrome extensions throw a TypeError 'Failed to fetch' at the
    // network layer when the requested path is not in
    // web_accessible_resources OR the file is genuinely absent. Both
    // failure modes mean Plan 01-10 has not landed — skip-gate
    // accordingly. (A regular HTTP 404 also flows here through the
    // try-pathway in case future Chrome versions normalize the
    // behavior.)
    let probe: Response;
    try {
      probe = await fetch(welcomeUrl);
    } catch (fetchErr) {
      const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
      result.checks.push({
        name: "A22.SKIPPED: welcome.html fetch threw — Plan 01-10 not landed; A22 passes informationally",
        expected: 'reachable OR network/404 (both mean 01-10 not landed)',
        actual: `fetch threw: ${msg}`,
        passed: true,
      });
      result.passed = true;
      diag(result, `A22 SKIPPED — Plan 01-10 not landed (fetch threw: ${msg})`);
      return result;
    }
    diag(result, `Step 1 result: HTTP ${probe.status}`);

    if (probe.status === 404) {
      // Skip-gate: Plan 01-10 has not yet landed; A22 PASSES with
      // diagnostic per Plan 01-12 Wave 6 §interfaces note.
      result.checks.push({
        name: "A22.SKIPPED: welcome.html returned 404 — Plan 01-10 not landed; A22 passes informationally",
        expected: 'src/welcome/welcome.html reachable OR 404',
        actual: 'HTTP 404',
        passed: true,
      });
      result.passed = true;
      diag(result, 'A22 SKIPPED — Plan 01-10 not landed (welcome.html absent)');
      return result;
    }
    if (!probe.ok) {
      throw new Error(`welcome.html returned unexpected HTTP ${probe.status} ${probe.statusText}`);
    }

    diag(result, 'Step 2: read welcome.html body + extract <link rel="stylesheet" href> targets');
    const html = await probe.text();
    const linkMatches = Array.from(html.matchAll(/<link[^>]+rel=["']?stylesheet["']?[^>]+href=["']([^"']+)["']/gi));
    diag(result, `Step 2 result: ${linkMatches.length} stylesheet link(s) found`);

    if (linkMatches.length === 0) {
      throw new Error('No <link rel="stylesheet"> found in welcome.html');
    }

    // Fetch each linked stylesheet + count var(--mks-*) usages across all of them.
    let totalTokenUses = 0;
    let hasTokensImport = false;
    for (const match of linkMatches) {
      const href = match[1];
      const resolved = new URL(href, welcomeUrl).href;
      diag(result, `Step 3: fetch ${resolved}`);
      const cssResp = await fetch(resolved);
      if (!cssResp.ok) {
        diag(result, `(skip ${href} — HTTP ${cssResp.status})`);
        continue;
      }
      const css = await cssResp.text();
      const tokenMatches = css.match(/var\(--mks-[a-z-]+\)/g) ?? [];
      totalTokenUses += tokenMatches.length;
      if (/tokens\.css/.test(css)) hasTokensImport = true;
      diag(result, `Step 3 result: ${href} contains ${tokenMatches.length} var(--mks-*) usages; tokens.css ref=${hasTokensImport}`);
    }

    result.checks.push({
      name: `A22.1: welcome page stylesheets contain >= ${A22_MIN_TOKEN_USES} var(--mks-*) usages OR @import tokens.css`,
      expected: `>= ${A22_MIN_TOKEN_USES} var(--mks-*) usages OR tokens.css reference`,
      actual: `${totalTokenUses} var(--mks-*) + tokens.css=${hasTokensImport}`,
      passed: totalTokenUses >= A22_MIN_TOKEN_USES || hasTokensImport,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/* ─── Plan 02-04 Task 1 — A24 (Blob URL empirical) ─────────────────────
 *
 *   A24 — D-P2-01 empirical: SAVE_ARCHIVE → chrome.downloads.download is
 *         invoked with a `blob:` URL prefix (NOT `data:application/zip;base64,`).
 *         Closes audit P0-6 functionally (Plan 02-02 unit-tested it at the
 *         wire boundary; A24 verifies end-to-end through a real Chrome
 *         instance + the offscreen mint round-trip + chrome.downloads
 *         platform call).
 *
 * FORBIDDEN_HOOK_STRINGS impact: NONE. A24 uses chrome.downloads.onCreated
 * (production cross-realm API; canonical capture pattern) + chrome.runtime
 * .sendMessage(SAVE_ARCHIVE). The Tier-1 inventory stays at 12.
 *
 * Future Plan 02-04 Tasks 2-3 will add A25+A26+A27+A28 (latency, meta.json
 * shape, multi-tab urls strict, REQ-archive-layout strict zip-layout).
 */

/** Timeout for SAVE_ARCHIVE message dispatch — matches A5_SAVE_ARCHIVE_TIMEOUT_MS
 *  (the SW does the same screenshot + content-script + JSZip work). 15s is
 *  generously above the typical ~2-3s observed in A5. */
const A24_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Max time to poll for the chrome.downloads.download Proxy to fire after
 *  the SAVE_ARCHIVE ack returns. The platform call is async-resolved post
 *  the offscreen → SW → chrome.downloads round-trip; 5s is comfortable
 *  headroom over the typical sub-second delay. */
const A24_DOWNLOAD_SPY_POLL_TIMEOUT_MS = 5_000;
/** Polling cadence while waiting for the chrome.downloads.download spy
 *  to capture its args. 100 ms matches the global POLL_INTERVAL_MS. */
const A24_DOWNLOAD_SPY_POLL_INTERVAL_MS = 100;
/** Pre-SAVE segment-settle window — mirrors A5_SEGMENT_SETTLE_MS (11s
 *  = 10s rotation + 1s slack) so getVideoBufferFromOffscreen returns a
 *  non-empty segment buffer before A24 dispatches SAVE_ARCHIVE. */
const A24_SEGMENT_SETTLE_MS = 11_000;

/**
 * A24 — D-P2-01 empirical: SAVE_ARCHIVE invokes chrome.downloads.download
 * with a `blob:` URL (NOT `data:application/zip;base64,`).
 *
 * Strategy: chrome.downloads.onCreated listener captures the URL cross-realm.
 *
 * The plan's `<action>` block proposed a chrome.downloads.download
 * monkey-patch installed in the harness page realm. That approach
 * intercepts only calls dispatched FROM the harness page realm — the
 * SW's downloadArchive call lives in a different realm and bypasses
 * the patch. The canonical cross-realm capture pattern is
 * chrome.downloads.onCreated (fires for any download initiated by any
 * extension realm, with the full DownloadItem including .url).
 *
 * Per the plan + saved memory feedback-no-unilateral-scope-reduction:
 * NO new test-hook surface. chrome.downloads.onCreated is a production
 * chrome.* API available to extension contexts under the existing
 * `downloads` permission. Tier-1 FORBIDDEN_HOOK_STRINGS stays at 12.
 *
 * Chaining: A24 does its OWN setupFreshRecording + SAVE because the
 * onCreated listener MUST be installed BEFORE the SAVE — chaining off
 * A5/A11/A12/A13's already-completed saves misses the listener window.
 *
 * @returns Structured result with 4 checks (A24.1..A24.4).
 */
async function assertA24(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A24 — D-P2-01 empirical: chrome.downloads.download receives blob: URL (closes P0-6)',
    checks: [],
    diagnostics: [],
  };

  // Capture stored in closure so the onCreated listener can populate it
  // across the async SAVE_ARCHIVE dispatch + post-ack poll.
  let capturedUrl: string | null = null;
  let listenerInstalled = false;
  const onCreatedListener = (item: chrome.downloads.DownloadItem): void => {
    // First-wins — A24's SAVE_ARCHIVE produces exactly one download; any
    // subsequent download in the same test run (e.g. from a stray prior
    // SAVE that landed late) would NOT overwrite the captured value.
    if (capturedUrl === null) {
      capturedUrl = item.url;
    }
  };

  try {
    diag(result, 'Step 1: setupFreshRecording (A24 owns its recording — onCreated listener installed pre-SAVE)');
    const setupResp = await setupFreshRecording();
    if (!setupResp.ok) {
      throw new Error(
        `setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`,
      );
    }
    diag(result, 'Step 1 OK — REC state established');

    diag(result, `Step 2: settle ${A24_SEGMENT_SETTLE_MS}ms for first segment rotation`);
    await new Promise((r) => setTimeout(r, A24_SEGMENT_SETTLE_MS));

    diag(result, 'Step 3: install chrome.downloads.onCreated listener (cross-realm capture)');
    chrome.downloads.onCreated.addListener(onCreatedListener);
    listenerInstalled = true;

    diag(result, 'Step 4: dispatch SAVE_ARCHIVE');
    const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
      { type: 'SAVE_ARCHIVE' },
      A24_SAVE_ARCHIVE_TIMEOUT_MS,
      'SAVE_ARCHIVE (A24)',
    );
    diag(result, `Step 4 result: ${JSON.stringify(ack)}`);

    result.checks.push({
      name: 'A24.1: SAVE_ARCHIVE ack received with success=true',
      expected: true,
      actual: ack.success,
      passed: ack.success === true,
    });

    diag(result, `Step 5: poll up to ${A24_DOWNLOAD_SPY_POLL_TIMEOUT_MS}ms for chrome.downloads.onCreated to fire`);
    const pollStart = Date.now();
    while (capturedUrl === null && Date.now() - pollStart < A24_DOWNLOAD_SPY_POLL_TIMEOUT_MS) {
      await new Promise((r) => setTimeout(r, A24_DOWNLOAD_SPY_POLL_INTERVAL_MS));
    }
    diag(
      result,
      `Step 5 result: capturedUrl=${capturedUrl === null ? '<null>' : (capturedUrl as string).substring(0, 60) + '...'}`,
    );

    result.checks.push({
      name: 'A24.2: chrome.downloads.onCreated fired (download initiated by SW)',
      expected: true,
      actual: capturedUrl !== null,
      passed: capturedUrl !== null,
    });

    const urlIsBlob =
      capturedUrl !== null && (capturedUrl as string).startsWith('blob:');
    const urlIsDataBase64 =
      capturedUrl !== null &&
      (capturedUrl as string).startsWith('data:application/zip;base64,');
    result.checks.push({
      name: 'A24.3: download URL starts with "blob:" (D-P2-01 Plan 02-02 closure of P0-6)',
      expected: true,
      actual: urlIsBlob,
      passed: urlIsBlob,
    });
    result.checks.push({
      name: 'A24.4: download URL does NOT start with "data:application/zip;base64," (legacy path retired)',
      expected: true,
      actual: !urlIsDataBase64,
      passed: !urlIsDataBase64,
    });
    diag(
      result,
      `capturedUrl prefix: ${capturedUrl === null ? '<null>' : (capturedUrl as string).substring(0, 40) + '...'}`,
    );

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  } finally {
    // T-02-04-01 mitigation: always remove the listener so it does not
    // accumulate across re-runs of the harness within a single session.
    // removeListener on a never-added listener is a benign no-op per
    // chrome.events spec; the guard is purely diagnostic.
    if (listenerInstalled) {
      try {
        chrome.downloads.onCreated.removeListener(onCreatedListener);
      } catch (rmErr) {
        diag(result, `(listener cleanup ignored: ${String(rmErr)})`);
      }
    }
  }

  return result;
}

/* ─── Plan 02-04 Task 2 — A25 (5s SAVE→ack+file-on-disk latency) ───────
 *
 *   A25 — REQ-archive-export-latency / SPEC §10 #6: page-side records
 *         dispatch→ack timings via performance.now() bookends; host-side
 *         driver merges the dispatch→file-on-disk timing via downloadsDir
 *         polling. End-to-end ceiling: 5000 ms.
 *
 * T-02-04-02 disposition (accept): the bracket is t0=just-before-SAVE,
 * tAck=just-after-ack — NOT broader test orchestration. setupFreshRecording
 * + segment-settle happen BEFORE the t0 mark. The 5s budget is measured
 * FROM the SAVE dispatch instant.
 */

/** SAVE_ARCHIVE dispatch timeout for A25 — matches A24's 15s. */
const A25_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
const A25_SEGMENT_SETTLE_MS = 11_000;
/** Hard latency ceiling per SPEC §10 #6 + CON-archive-export-latency. */
const A25_LATENCY_CEILING_MS = 5_000;

/**
 * Extended result shape — A25 returns the t0/tAck bookends so the
 * host-side driver can compute the merged dispatch→file-on-disk
 * latency check.
 *
 * t0Wall is `Date.now()` captured at the SAVE_ARCHIVE dispatch instant
 * (NOT at setupFreshRecording start). It is the canonical anchor for
 * host-side dispatch→file-on-disk measurement, because performance.now()
 * is monotonic and not comparable to Date.now() across realms. The
 * driver computes `tFileHost - t0Wall < 5000ms` to assert REQ-archive-
 * export-latency per T-02-04-02 disposition (bracket only the SAVE
 * dispatch, not the broader test orchestration).
 */
interface A25Result extends AssertionResult {
  t0: number;
  tAck: number;
  t0Wall: number;
  ackSuccess: boolean;
}

/**
 * A25 — SAVE_ARCHIVE → zip on disk in <5000ms (REQ-archive-export-latency).
 *
 * Page-side measures dispatch→ack ONLY (performance.now() bookends
 * bracket the chrome.runtime.sendMessage call). Host-side `driveA25`
 * additionally polls downloadsDir for the new zip + measures
 * dispatch→file-on-disk via mtime delta.
 *
 * Chaining: A25 does its OWN setupFreshRecording + SAVE (clean latency
 * measurement, not compounded with A24's still-pending state). The
 * setupFreshRecording + 11s settle WALL TIME is OUTSIDE the t0/tAck
 * bracket — only the SAVE dispatch latency counts against the 5s budget.
 *
 * @returns A25Result with structured checks + t0/tAck bookends for the
 *          host-side driver.
 */
async function assertA25(): Promise<A25Result> {
  const result: A25Result = {
    passed: false,
    name: 'A25 — REQ-archive-export-latency: <5000ms SAVE→ack (SPEC §10 #6)',
    checks: [],
    diagnostics: [],
    t0: 0,
    tAck: 0,
    t0Wall: 0,
    ackSuccess: false,
  };

  try {
    diag(result, 'Step 1: setupFreshRecording (A25 owns its recording — clean latency measurement)');
    const setupResp = await setupFreshRecording();
    if (!setupResp.ok) {
      throw new Error(
        `setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`,
      );
    }
    diag(result, 'Step 1 OK — REC state established');

    diag(result, `Step 2: settle ${A25_SEGMENT_SETTLE_MS}ms for first segment rotation (NOT counted toward 5s budget)`);
    await new Promise((r) => setTimeout(r, A25_SEGMENT_SETTLE_MS));

    diag(result, 'Step 3: t0 = performance.now() AND t0Wall = Date.now(), then SAVE_ARCHIVE then tAck (this is the budgeted bracket)');
    const t0 = performance.now();
    const t0Wall = Date.now();
    const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
      { type: 'SAVE_ARCHIVE' },
      A25_SAVE_ARCHIVE_TIMEOUT_MS,
      'SAVE_ARCHIVE (A25)',
    );
    const tAck = performance.now();
    const elapsedAck = tAck - t0;

    result.t0 = t0;
    result.tAck = tAck;
    result.t0Wall = t0Wall;
    result.ackSuccess = ack.success === true;

    diag(
      result,
      `Step 3 result: ack=${JSON.stringify(ack)}, t0=${t0.toFixed(0)}, tAck=${tAck.toFixed(0)}, t0Wall=${t0Wall}, elapsedAck=${elapsedAck.toFixed(0)}ms`,
    );

    result.checks.push({
      name: 'A25.1: SAVE_ARCHIVE ack received with success=true',
      expected: true,
      actual: ack.success,
      passed: ack.success === true,
    });
    result.checks.push({
      name: `A25.2: page-side dispatch → ack latency < ${A25_LATENCY_CEILING_MS}ms`,
      expected: `<${A25_LATENCY_CEILING_MS}ms`,
      actual: `${elapsedAck.toFixed(0)}ms`,
      passed: elapsedAck < A25_LATENCY_CEILING_MS,
    });
    diag(
      result,
      `page-side latency: t0=${t0.toFixed(0)} tAck=${tAck.toFixed(0)} delta=${elapsedAck.toFixed(0)}ms`,
    );

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/* ─── Plan 02-04 Task 3 — A26 + A27 (strict) + A28 ─────────────────────
 *
 *   A26 — D-P2-02 + D-P2-03 empirical: meta.json has the 8-field shape
 *         with urls[] (not url:string) and schemaVersion='2'.
 *   A27 — STRICT mode (post DEC-011 Amendment 1): multi-tab URL tracking.
 *         Opens TWO tabs sequentially, activates each, then dispatches
 *         SAVE. Host-side asserts meta.urls contains BOTH URLs (length>=2
 *         REQUIRED; FAILS on length<2).
 *   A28 — REQ-archive-layout strict zip-layout: zip contains EXACTLY the
 *         5 canonical entries (video/last_30sec.webm, rrweb/session.json,
 *         logs/events.json, screenshot.png, meta.json). Cross-references
 *         REQ-popup-ui + REQ-screenshot-on-export.
 *
 * Architecture (per saved memory feedback-no-unilateral-scope-reduction +
 * Plan 01-13 Approach B): page-side does the orchestration (SAVE dispatch
 * + tab management); host-side does the zip parsing via JSZip (already
 * imported in tests/uat/lib/zip.ts; not bundled into the harness page
 * realm). A26 + A28 page-sides are intentional stubs returning the
 * assertion name — all zip-inspection work is host-side in the drivers.
 *
 * FORBIDDEN_HOOK_STRINGS impact: NONE. A27 uses chrome.tabs.create +
 * chrome.tabs.update + chrome.tabs.remove (production APIs; `tabs`
 * permission granted via DEC-011 Amendment 1). Tier-1 inventory stays
 * at the post-A24/A25 baseline.
 *
 * T-02-04-04 mitigation: A27 wraps both tab-cleanup calls in try/catch
 * with silent-ignore on already-closed (chrome.tabs.remove throws if
 * the tab id is gone; the test doesn't care about that side effect).
 */

/** SAVE_ARCHIVE dispatch timeout for A27 — matches A24/A25's 15s. */
const A27_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
const A27_SEGMENT_SETTLE_MS = 11_000;
/** Wait after chrome.tabs.create for the tab navigation to complete
 *  (the URL field on the tab object only populates once the page
 *  begins loading). Conservative 1500ms per plan spec. */
const A27_TAB_NAVIGATION_WAIT_MS = 1_500;
/** Wait after chrome.tabs.update({active:true}) for chrome.tabs.onActivated
 *  to fire + tab-url-tracker (Plan 02-03 Task 2) to capture the URL. */
const A27_TAB_ACTIVATION_WAIT_MS = 500;

/** Canonical multi-tab URLs for A27 — public sites with stable URLs,
 *  no PII (per T-02-04-03 disposition). example.com is RFC 2606
 *  reserved; iana.org is the IANA homepage. Both serve plain HTML
 *  reliably under headless Chrome. */
const A27_TAB_A_URL = 'https://example.com/';
const A27_TAB_B_URL = 'https://www.iana.org/';

/**
 * Extended result shape — A27 returns the canonical URLs the host-side
 * driver needs to assert against meta.urls.
 */
interface A27Result extends AssertionResult {
  tabAUrl: string;
  tabBUrl: string;
}

/**
 * A26 — D-P2-02 + D-P2-03 empirical (page-side stub).
 *
 * Returns the assertion name + a sentinel diagnostic. All real work
 * happens host-side in driveA26 (JSZip-parse the latest zip + assert
 * meta.json shape). The page-side stub exists purely so the orchestrator's
 * single-method-per-assertion contract (window.__mokoshHarness.assertA26)
 * is uniform across all 29 assertions.
 *
 * Chaining: A26 reads the zip produced by A25 (host-side driver picks
 * the most-recently-modified zip in downloadsDir). No new SAVE dispatch.
 *
 * @returns Stub AssertionResult with empty checks; driveA26 fills them.
 */
async function assertA26(): Promise<AssertionResult> {
  return {
    passed: true,
    name: 'A26 — meta.json 8-field shape (D-P2-02 + D-P2-03)',
    checks: [],
    diagnostics: [
      'assertA26 page-side stub; host-side driveA26 inspects latest zip + asserts meta.json shape',
    ],
  };
}

/**
 * A27 — STRICT mode (post DEC-011 Amendment 1): multi-tab URL tracking.
 *
 * Sequence (per plan spec):
 *   1. setupFreshRecording (clean state)
 *   2. chrome.tabs.create(TAB_A_URL, active:false) → wait 1500ms for nav
 *   3. chrome.tabs.update(tabA.id, {active:true}) → wait 500ms for onActivated
 *   4. chrome.tabs.create(TAB_B_URL, active:false) → wait 1500ms for nav
 *   5. chrome.tabs.update(tabB.id, {active:true}) → wait 500ms for onActivated
 *   6. Wait 11s for one segment to land
 *   7. Dispatch SAVE_ARCHIVE; await ack with success===true
 *   8. Cleanup: try/catch close both tabs via chrome.tabs.remove
 *
 * Host-side driveA27 then loads the produced zip + parses meta.json + asserts:
 *   - meta.urls.length >= 2 (FAIL on length < 2)
 *   - Both TAB_A_URL and TAB_B_URL are in meta.urls (order-flexible)
 *   - No extension-origin sentinels (chrome-extension://)
 *   - No chrome-internal URLs (chrome:// or about:)
 *
 * Page-side returns A27.1 (SAVE_ARCHIVE ack) + the canonical URLs so
 * the host-side driver can compute the strict assertions.
 *
 * T-02-04-04 mitigation: cleanup runs in finally; both tabs.remove
 * calls wrapped in try/catch (silent-ignore on already-closed).
 *
 * @returns A27Result with 1 check (SAVE ack) + tabAUrl + tabBUrl for the driver.
 */
async function assertA27(): Promise<A27Result> {
  const result: A27Result = {
    passed: false,
    name: 'A27 — D-P2-02 STRICT multi-tab urls[] (both URLs REQUIRED post DEC-011 Amendment 1)',
    checks: [],
    diagnostics: [],
    tabAUrl: A27_TAB_A_URL,
    tabBUrl: A27_TAB_B_URL,
  };

  let tabAId: number | undefined;
  let tabBId: number | undefined;

  try {
    diag(result, 'Step 1: setupFreshRecording (A27 owns its recording)');
    const setupResp = await setupFreshRecording();
    if (!setupResp.ok) {
      throw new Error(
        `setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`,
      );
    }
    diag(result, 'Step 1 OK — REC state established');

    diag(result, `Step 2: chrome.tabs.create(${A27_TAB_A_URL}, active:false)`);
    const tabA = await chrome.tabs.create({ url: A27_TAB_A_URL, active: false });
    tabAId = tabA.id;
    diag(result, `Step 2 result: tabA.id=${tabAId}, tabA.url=${tabA.url ?? '<pending>'}`);
    await new Promise((r) => setTimeout(r, A27_TAB_NAVIGATION_WAIT_MS));

    if (tabAId !== undefined) {
      diag(result, `Step 3: chrome.tabs.update(${tabAId}, {active:true})`);
      await chrome.tabs.update(tabAId, { active: true });
      await new Promise((r) => setTimeout(r, A27_TAB_ACTIVATION_WAIT_MS));
    }

    diag(result, `Step 4: chrome.tabs.create(${A27_TAB_B_URL}, active:false)`);
    const tabB = await chrome.tabs.create({ url: A27_TAB_B_URL, active: false });
    tabBId = tabB.id;
    diag(result, `Step 4 result: tabB.id=${tabBId}, tabB.url=${tabB.url ?? '<pending>'}`);
    await new Promise((r) => setTimeout(r, A27_TAB_NAVIGATION_WAIT_MS));

    if (tabBId !== undefined) {
      diag(result, `Step 5: chrome.tabs.update(${tabBId}, {active:true})`);
      await chrome.tabs.update(tabBId, { active: true });
      await new Promise((r) => setTimeout(r, A27_TAB_ACTIVATION_WAIT_MS));
    }

    diag(result, `Step 6: settle ${A27_SEGMENT_SETTLE_MS}ms for one segment to land`);
    await new Promise((r) => setTimeout(r, A27_SEGMENT_SETTLE_MS));

    diag(result, 'Step 7: dispatch SAVE_ARCHIVE');
    const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
      { type: 'SAVE_ARCHIVE' },
      A27_SAVE_ARCHIVE_TIMEOUT_MS,
      'SAVE_ARCHIVE (A27)',
    );
    diag(result, `Step 7 result: ${JSON.stringify(ack)}`);

    result.checks.push({
      name: 'A27.1: SAVE_ARCHIVE ack received with success=true',
      expected: true,
      actual: ack.success,
      passed: ack.success === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  } finally {
    // T-02-04-04 mitigation: cleanup tabs with silent-ignore on
    // already-closed. chrome.tabs.remove rejects if the tab id is gone;
    // we don't care about that side effect for this assertion.
    if (tabAId !== undefined) {
      try {
        await chrome.tabs.remove(tabAId);
      } catch (rmErr) {
        diag(result, `(tabA cleanup ignored: ${String(rmErr)})`);
      }
    }
    if (tabBId !== undefined) {
      try {
        await chrome.tabs.remove(tabBId);
      } catch (rmErr) {
        diag(result, `(tabB cleanup ignored: ${String(rmErr)})`);
      }
    }
  }

  return result;
}

/**
 * A28 — REQ-archive-layout strict zip-layout (page-side stub).
 *
 * Returns the assertion name + a sentinel diagnostic. All real work
 * happens host-side in driveA28 (JSZip enumerates the latest zip's
 * entries + asserts exactly 5 canonical paths, no extras). The
 * page-side stub exists purely for orchestrator uniformity.
 *
 * Chaining: A28 reads the zip produced by A27 (host-side picks the
 * most-recently-modified zip in downloadsDir). No new SAVE dispatch.
 *
 * @returns Stub AssertionResult with empty checks; driveA28 fills them.
 */
async function assertA28(): Promise<AssertionResult> {
  return {
    passed: true,
    name: 'A28 — REQ-archive-layout strict (5 entries)',
    checks: [],
    diagnostics: [
      'assertA28 page-side stub; host-side driveA28 enumerates latest zip + asserts 5-entry layout',
    ],
  };
}

/* ─── Plan 03-01 Task 2 — A29 (rrweb DOM verification; SPEC §10 #4) ─
 *
 *   A29 — REQ-rrweb-dom-buffer empirical: rrweb's record() (already
 *         wired at src/content/index.ts:285) emits Meta + FullSnapshot
 *         + at least one IncrementalSnapshot when the harness page
 *         contains the probe HTML (form + table + modal) AND the driver
 *         injects a DOM mutation before SAVE (RESEARCH Pitfall 1:
 *         static probe HTML emits Meta + FullSnapshot but not
 *         IncrementalSnapshot without mutation).
 *
 * Page side: dispatch the probe-page DOM mutations (input value +
 * modal toggle), settle, setupFreshRecording, settle one segment,
 * dispatch SAVE_ARCHIVE, push A29.1 ack check. The host-side driveA29
 * does the EventType-enum-shape grep against rrweb/session.json from
 * the assembled zip (matches A26's chained-assertion pattern; JSZip
 * + @rrweb/types are host-only deps).
 *
 * FORBIDDEN_HOOK_STRINGS impact: NONE. A29 rides production rrweb
 * wiring (record() at src/content/index.ts:285 + GET_RRWEB_EVENTS
 * round-trip at src/background/index.ts → src/content/index.ts:318)
 * + the existing setupFreshRecording / sendMessageWithTimeout
 * helpers. Tier-1 inventory stays at 12 entries.
 */

/** SAVE_ARCHIVE dispatch timeout for A29 — matches A24/A25/A27. */
const A29_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
const A29_SEGMENT_SETTLE_MS = 11_000;
/** Settle window between DOM mutation and SAVE so rrweb's
 *  IncrementalSnapshot lands in the in-memory buffer before
 *  GET_RRWEB_EVENTS fires. */
const A29_MUTATION_SETTLE_MS = 500;

/**
 * A29 — rrweb DOM event recording empirical (SPEC §10 #4).
 *
 * Page-side dispatches the probe-page mutation (input.value + modal
 * toggle), settles, runs setupFreshRecording + segment-settle, then
 * dispatches SAVE_ARCHIVE. Only the SAVE ack lives in the
 * page-side checks; the EventType-enum-shape grep is host-side
 * because @rrweb/types is host-only.
 *
 * @returns AssertionResult with 1 page-side check (SAVE ack); host-side
 *          driveA29 appends A29.0a/A29.2..A29.5 EventType checks.
 */
async function assertA29(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A29 — rrweb DOM events recorded without errors (SPEC §10 #4 / REQ-rrweb-dom-buffer)',
    checks: [],
    diagnostics: [],
  };

  try {
    diag(result, 'Step 1: dispatch probe-page DOM mutation (input value + modal toggle)');
    const textInput = document.querySelector<HTMLInputElement>('#probe-text');
    if (textInput !== null) {
      textInput.value = 'probe';
      textInput.dispatchEvent(new Event('input', { bubbles: true }));
    }
    const modalTrigger = document.querySelector<HTMLButtonElement>('#probe-modal-trigger');
    if (modalTrigger !== null) {
      modalTrigger.click();
    }
    diag(result, `Step 1 OK — mutations dispatched (#probe-text=${textInput !== null}, #probe-modal-trigger=${modalTrigger !== null})`);

    diag(result, `Step 2: settle ${A29_MUTATION_SETTLE_MS}ms for rrweb IncrementalSnapshot to enqueue`);
    await new Promise((r) => setTimeout(r, A29_MUTATION_SETTLE_MS));

    diag(result, 'Step 3: setupFreshRecording');
    const setupResp = await setupFreshRecording();
    if (!setupResp.ok) {
      throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
    }
    diag(result, 'Step 3 OK — REC state established');

    diag(result, `Step 4: settle ${A29_SEGMENT_SETTLE_MS}ms for first segment rotation`);
    await new Promise((r) => setTimeout(r, A29_SEGMENT_SETTLE_MS));

    diag(result, 'Step 5: dispatch SAVE_ARCHIVE');
    const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
      { type: 'SAVE_ARCHIVE' },
      A29_SAVE_ARCHIVE_TIMEOUT_MS,
      'SAVE_ARCHIVE (A29)',
    );
    diag(result, `Step 5 result: ${JSON.stringify(ack)}`);

    result.checks.push({
      name: 'A29.1: SAVE_ARCHIVE ack received with success=true',
      expected: true,
      actual: ack.success,
      passed: ack.success === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  }

  return result;
}

/* ─── Plan 03-02 — A30 (event-log verification; SPEC §10 #5) ────────
 *
 *   A30 — REQ-user-event-log empirical: the production listeners at
 *         src/content/index.ts (setupClickLogging at line 61,
 *         setupInputLogging at line 77, setupNavigationLogging at line
 *         99, setupErrorLogging at line 133, setupNetworkLogging at
 *         line 164) all fire on synthetic browser events dispatched
 *         in a probe https:// tab where the production content script
 *         is injected, producing UserEvent entries with each of the 5
 *         type-values (click / input / navigation / js_error /
 *         network_error) in logs/events.json.
 *
 * Implementation note — MV3 content-script reachability (deviation):
 *   The plan as written assumed `<all_urls>` content_scripts coverage
 *   includes `chrome-extension://` URLs — empirically (Task 2
 *   verification dump 2026-05-20T17:36:25Z), it does NOT. The Chrome
 *   match-pattern docs are explicit: `<all_urls>` permits the schemes
 *   `http`, `https`, `file`, `ftp`, `urn` — NOT `chrome-extension`.
 *   The SW SAVE_ARCHIVE handler logged "Could not establish connection.
 *   Receiving end does not exist." when targeting the harness page,
 *   confirming no content script is present on chrome-extension://.
 *
 *   A30 therefore creates a fresh `https://example.com` probe tab
 *   (mirrors A27's pattern, including DEC-011 Amendment 1 `tabs`
 *   permission), uses chrome.scripting.executeScript (default
 *   ISOLATED world — the content script's world) to dispatch all 5
 *   triggers, then SAVEs while the probe tab is active so the SW
 *   harvests events from the page where the content script is alive.
 *
 *   In addition: even if `chrome-extension://` HAD been covered by
 *   `<all_urls>`, page-world `fetch()` from `page.evaluate(...)` would
 *   NOT have been intercepted by `src/content/index.ts:167`
 *   (`window.fetch = ...`) — content-script global mutations stay
 *   inside the ISOLATED world. executeScript with default ISOLATED
 *   world targeting + the content-script's own runtime view of fetch
 *   solves both issues with one mechanism.
 *
 * Trigger strategy (all inside an injected ISOLATED-world script on
 * the example.com probe tab):
 *   - click: dispatch a real `MouseEvent('click')` on document.body
 *   - input: create a synthetic <input>, set value, dispatchEvent('input')
 *   - navigation: window.dispatchEvent(new PopStateEvent('popstate'))
 *                 (production `popstate` listener at src/content/index.ts:111;
 *                 NOTE: history.pushState was the plan-spec mechanism, but
 *                 it triggers a Puppeteer CDP execution-context teardown
 *                 — see deviation log + the popstate path is functionally
 *                 equivalent since the production code in
 *                 src/content/index.ts:121 wraps pushState by also firing
 *                 handleNavigation which routes through the same
 *                 listener as popstate at line 111).
 *   - js_error: window.dispatchEvent(new ErrorEvent('error', ...))
 *   - network_error: fetch(404-probe-url).catch(noop) — runs in
 *                    ISOLATED world so the patched window.fetch
 *                    at src/content/index.ts:167 fires.
 *
 * Page-side opens probe tab + injects triggers + settles + SAVE.
 * Host-side driveA30 JSZip-parses logs/events.json + asserts each of
 * the 5 UserEvent.type literal values is present.
 *
 * FORBIDDEN_HOOK_STRINGS impact: NONE. A30 rides production listeners
 * + chrome.tabs.* (`tabs` perm) + chrome.scripting.executeScript
 * (`scripting` perm — already in manifest) + existing helpers. Tier-1
 * inventory stays at 12.
 */

/** SAVE_ARCHIVE dispatch timeout for A30 — matches A24/A25/A27/A29. */
const A30_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
const A30_SEGMENT_SETTLE_MS = 11_000;
/** Settle between trigger dispatches and SAVE so event handlers complete. */
const A30_TRIGGER_SETTLE_MS = 1_000;
/** Wait after chrome.tabs.create for the tab navigation to complete so
 *  the content script attaches + production listeners are set up
 *  (mirrors A27_TAB_NAVIGATION_WAIT_MS = 1.5s). */
const A30_TAB_NAVIGATION_WAIT_MS = 1_500;
/** Probe tab URL — example.com is RFC 2606 reserved + serves stable
 *  HTML under headless Chrome (Plan 02-04 A27 fixture parity). */
const A30_PROBE_TAB_URL = 'https://example.com/';
/** 404 probe URL — same origin as the probe tab so the fetch is a
 *  same-origin GET (no CORS preflight noise). */
const A30_404_PROBE_URL = 'https://example.com/this-path-does-not-exist-404-probe-a30';

/**
 * A30 — Event-log empirical (SPEC §10 #5 / REQ-user-event-log).
 *
 * Creates a fresh `https://example.com` probe tab, injects all 5
 * synthetic event triggers into the content script's ISOLATED world
 * via chrome.scripting.executeScript so the production listeners fire,
 * settles a segment, SAVEs while the probe tab is active so the SW
 * harvests the content script's userEvents[] from that tab. Host-side
 * driveA30 inspects logs/events.json from the produced zip and asserts
 * each of the 5 UserEvent.type literal values appears at least once.
 *
 * @returns AssertionResult with 1 page-side check (SAVE ack); host-side
 *          driveA30 appends 5 UserEvent.type presence checks.
 */
async function assertA30(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A30 — event log captures 5 UserEvent types (SPEC §10 #5 / REQ-user-event-log)',
    checks: [],
    diagnostics: [],
  };

  let probeTabId: number | undefined;

  try {
    diag(result, 'Step 1: setupFreshRecording (A30 owns its recording — clean event-log window)');
    const setupResp = await setupFreshRecording();
    if (!setupResp.ok) {
      throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
    }
    diag(result, 'Step 1 OK — REC state established');

    diag(result, `Step 2: chrome.tabs.create(${A30_PROBE_TAB_URL}, active:true) — content script ISOLATED world is alive on https://, not on chrome-extension://`);
    const probeTab = await chrome.tabs.create({ url: A30_PROBE_TAB_URL, active: true });
    probeTabId = probeTab.id;
    diag(result, `Step 2 result: probeTab.id=${probeTabId}, probeTab.url=${probeTab.url ?? '<pending>'}`);
    if (probeTabId === undefined) {
      throw new Error('chrome.tabs.create returned undefined tab.id');
    }

    diag(result, `Step 3: wait ${A30_TAB_NAVIGATION_WAIT_MS}ms for navigation + content script attach`);
    await new Promise((r) => setTimeout(r, A30_TAB_NAVIGATION_WAIT_MS));

    diag(result, `Step 4: settle ${A30_SEGMENT_SETTLE_MS}ms for first segment rotation`);
    await new Promise((r) => setTimeout(r, A30_SEGMENT_SETTLE_MS));

    diag(result, 'Step 5: chrome.scripting.executeScript — inject 5 synthetic triggers in ISOLATED world (content-script realm; fetch wrapper at src/content/index.ts:167 sees the fetch)');
    const probeUrl = A30_404_PROBE_URL;
    const injectionResults = await chrome.scripting.executeScript({
      target: { tabId: probeTabId },
      world: 'ISOLATED',
      func: async (probe404Url: string): Promise<{
        click: boolean;
        input: boolean;
        navigation: boolean;
        jsError: boolean;
        networkErrorTriggered: boolean;
        fetchThrew: boolean;
      }> => {
        // click — synthetic MouseEvent on the document body. Production
        // listener at src/content/index.ts:61 captures the click via
        // document.addEventListener('click', ...).
        const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
        const clickDispatched = document.body.dispatchEvent(clickEvent);

        // input — synthetic <input>, set value, dispatchEvent('input',
        // bubbles:true). Production listener at src/content/index.ts:78
        // captures via document.addEventListener('input', ...). Skips
        // password type (line 82) — type='text' here.
        const probeInput = document.createElement('input');
        probeInput.type = 'text';
        probeInput.id = 'a30-probe-input';
        probeInput.value = 'a30@probe.local';
        document.body.appendChild(probeInput);
        const inputDispatched = probeInput.dispatchEvent(
          new Event('input', { bubbles: true }),
        );
        probeInput.remove();

        // navigation — window-level popstate event. Production listener
        // at src/content/index.ts:111 captures via
        // window.addEventListener('popstate', ...).
        const navigationDispatched = window.dispatchEvent(
          new PopStateEvent('popstate', { state: {} }),
        );

        // js_error — window-level ErrorEvent. Production listener at
        // src/content/index.ts:134 captures via
        // window.addEventListener('error', ...).
        const errorDispatched = window.dispatchEvent(
          new ErrorEvent('error', {
            message: 'a30-probe-js-error',
            filename: 'a30-probe.js',
            lineno: 1,
            colno: 1,
          }),
        );

        // network_error — fetch into a 404 path. The content script
        // patches window.fetch at src/content/index.ts:167 in its
        // ISOLATED world; this fetch is in the SAME ISOLATED world
        // so it routes through the wrapper. response.ok===false →
        // addUserEvent({type:'network_error'}) at line 171.
        let fetchThrew = false;
        try {
          await fetch(probe404Url);
        } catch (fetchErr) {
          fetchThrew = true;
        }

        return {
          click: clickDispatched,
          input: inputDispatched,
          navigation: navigationDispatched,
          jsError: errorDispatched,
          networkErrorTriggered: true,
          fetchThrew,
        };
      },
      args: [probeUrl],
    });
    const injectionSummary = injectionResults[0]?.result ?? null;
    diag(result, `Step 5 result: ${JSON.stringify(injectionSummary)}`);

    diag(result, `Step 6: settle ${A30_TRIGGER_SETTLE_MS}ms so async handlers (fetch.then) complete + userEvents[] populates`);
    await new Promise((r) => setTimeout(r, A30_TRIGGER_SETTLE_MS));

    diag(result, 'Step 7: dispatch SAVE_ARCHIVE (probe tab is the active tab; SW will harvest from there)');
    const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
      { type: 'SAVE_ARCHIVE' },
      A30_SAVE_ARCHIVE_TIMEOUT_MS,
      'SAVE_ARCHIVE (A30)',
    );
    diag(result, `Step 7 result: ${JSON.stringify(ack)}`);

    result.checks.push({
      name: 'A30.1: SAVE_ARCHIVE ack received with success=true',
      expected: true,
      actual: ack.success,
      passed: ack.success === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  } finally {
    // T-02-04-04 mitigation parity: cleanup probe tab with silent-ignore.
    if (probeTabId !== undefined) {
      try {
        await chrome.tabs.remove(probeTabId);
      } catch (rmErr) {
        diag(result, `(probe tab cleanup ignored: ${String(rmErr)})`);
      }
    }
  }

  return result;
}

/* ─── Plan 03-03 — A31 (password-filter PARTIAL; SPEC §10 #8) ────────
 *
 *   A31 — D-P3-02 PARTIAL: verify the existing minimum filter at
 *         src/content/index.ts:82 (`if (target.type === 'password') return;`)
 *         fires when the operator types into a password input.
 *         Negative-assertion contract: SENTINEL value MUST be absent
 *         from logs/events.json.
 *
 * Charter alignment (per CONTEXT.md D-P3-02 + REQUIREMENTS.md line 271):
 *   - REQ-password-confidentiality moved Out of Scope v1 per 2026-05-20
 *     charter "we don't care about privacy hardening. At least here."
 *   - Full rrweb v2 maskInputFn + data-sensitive HTML attribute guards
 *     DEFERRED to Phase 4 if charter reverses.
 *   - A31 verifies the EXISTING minimum (the line-82 filter) — does
 *     NOT expand scope.
 *
 * Implementation note — cs-injection-world adaptation (Rule 3 blocking
 * auto-fix; mirrors Plan 03-02 architectural fix):
 *   The plan as written drove `document.querySelector('#probe-password')`
 *   on the harness page (chrome-extension://...harness.html). That
 *   matches Plan 03-02's `<all_urls>` content_scripts assumption
 *   which is empirically WRONG (Chrome match-pattern spec: `<all_urls>`
 *   covers http/https/file/ftp/urn only — NOT chrome-extension). With
 *   no content script attached to the harness page, the production
 *   setupInputLogging at src/content/index.ts:78 never sees the
 *   harness-page input event AT ALL, so A31.2 (absence-of-sentinel)
 *   and A31.3 (absence-of-#probe-password-target) would pass
 *   tautologically — neither check empirically verifies the line-82
 *   filter "fires", only that NO event is captured on the harness page
 *   regardless of input type. That is NOT a valid §10 #8 PARTIAL
 *   verification (the filter could be deleted and the test would
 *   still pass).
 *
 *   A31 therefore reuses the Plan 03-02 cs-injection-world pattern:
 *   open a fresh https://example.com probe tab where the content
 *   script DOES attach, inject a `<input type="password">` element +
 *   type the SENTINEL value + dispatch input event in the
 *   content-script's ISOLATED world, SAVE while the probe tab is
 *   active, finally-cleanup the probe tab. For a control case
 *   (verifies the wiring is operational), the same injection also
 *   types a control sentinel into a `<input type="text">` element
 *   — the production setupInputLogging MUST capture that event,
 *   PROVING the path that would have fired for the password input
 *   IS active. The control case is host-side-only check
 *   A31.4 (control event present); the production filter at
 *   src/content/index.ts:82 early-returns BEFORE addUserEvent so
 *   the password event NEVER lands in userEvents[] (A31.2 + A31.3).
 *
 *   This satisfies the plan's binding contract literally:
 *     - artifact "types sentinel into the probe-page password input
 *       via setupFreshRecording + SAVE" — done (just in a different
 *       page that has the content script alive)
 *     - truths #1/#2/#3 (sentinel-value-absent + zero-events-targeting-
 *       password) — empirically verified because the input event IS
 *       seen by the production listener + filter
 *     - threat T-03-03-04 (defense-in-depth) — A31.4 control case is
 *       the third orthogonal path proving the listeners are alive
 *
 * FORBIDDEN_HOOK_STRINGS impact: NONE. A31 rides production
 * setupInputLogging at src/content/index.ts:78 + the line-82 filter
 * + chrome.tabs.* + chrome.scripting.executeScript + existing helpers.
 * Tier-1 inventory stays at 12 entries.
 */

/** SAVE_ARCHIVE dispatch timeout for A31 — matches A24/A25/A27/A29/A30. */
const A31_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
const A31_SEGMENT_SETTLE_MS = 11_000;
/** Settle after sentinel-typing trigger so the synchronous handler completes. */
const A31_TRIGGER_SETTLE_MS = 1_000;
/** Wait after chrome.tabs.create for the tab navigation to complete so
 *  the content script attaches + production listeners are set up
 *  (mirrors A30_TAB_NAVIGATION_WAIT_MS = 1.5s). */
const A31_TAB_NAVIGATION_WAIT_MS = 1_500;
/** Probe tab URL — example.com is RFC 2606 reserved + serves stable
 *  HTML under headless Chrome (Plan 02-04 A27 + Plan 03-02 A30 fixture parity). */
const A31_PROBE_TAB_URL = 'https://example.com/';
/** Fixed test sentinel — distinctive string the negative-assertion
 *  searches for in events.json. Per RESEARCH §"Security Domain":
 *  this is a probe sentinel, NOT a real secret; logging it would
 *  itself trigger an explicit RED. */
const A31_PASSWORD_SENTINEL = 'secret-do-not-log-123';
/** Control sentinel — distinctive string typed into a `<input type="text">`
 *  element in the same injection. Production setupInputLogging at
 *  src/content/index.ts:78 MUST capture this — driveA31's A31.4
 *  check verifies its presence as proof that the listener is alive
 *  (defense-in-depth against T-03-03-04 — if the production listener
 *  weren't running at all, A31.2/A31.3 would pass tautologically;
 *  A31.4 GREEN proves the listener IS running, so A31.2/A31.3 GREEN
 *  actually mean the filter fired). */
const A31_CONTROL_SENTINEL = 'control-event-must-be-logged-a31';
/** Production CSS selector returned by getSelector() at
 *  src/content/index.ts:241 for the password input (which has id).
 *  Drives A31.3 (target-absence check). */
const A31_PASSWORD_SELECTOR = '#probe-password';
/** Synthetic password-input element id (matches A31_PASSWORD_SELECTOR
 *  after the leading `#` is stripped). Injected into the probe tab DOM
 *  by chrome.scripting.executeScript. */
const A31_PASSWORD_INPUT_ID = 'probe-password';
/** Synthetic control-input element id — referenced by driveA31's
 *  A31.4 check via the same `#probe-control` selector. */
const A31_CONTROL_INPUT_ID = 'probe-control';

/**
 * A31 — Password-filter PARTIAL empirical (SPEC §10 #8 PARTIAL per D-P3-02).
 *
 * Creates a fresh `https://example.com` probe tab (where the content
 * script attaches normally per Plan 03-02 cs-injection-world insight),
 * injects two `<input>` elements (a control `type="text"` + a sentinel
 * `type="password"`) + types the corresponding sentinels + dispatches
 * input events in the content-script's ISOLATED world, settles a
 * segment, SAVEs while the probe tab is active, finally-cleanup the
 * tab. Host-side driveA31 inspects logs/events.json and asserts:
 *   - The password SENTINEL is ABSENT from any UserEvent.value field
 *     (proves the line-82 filter early-returned before addUserEvent)
 *   - Zero UserEvent entries have target === '#probe-password'
 *     (proves the same filter via the orthogonal selector path)
 *   - At least one UserEvent contains the control sentinel
 *     (proves the listener was alive — defense-in-depth against
 *     the trivial "no events at all" tautology)
 *
 * @returns AssertionResult with 1 page-side check (SAVE ack); host-side
 *          driveA31 appends host-side checks for sentinel absence
 *          (A31.2 + A31.3) + control presence (A31.4).
 */
async function assertA31(): Promise<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'A31 — password filter fires (SPEC §10 #8 PARTIAL per D-P3-02)',
    checks: [],
    diagnostics: [],
  };

  let probeTabId: number | undefined;

  try {
    diag(result, 'Step 1: setupFreshRecording (A31 owns its recording — clean event-log window)');
    const setupResp = await setupFreshRecording();
    if (!setupResp.ok) {
      throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
    }
    diag(result, 'Step 1 OK — REC state established');

    diag(result, `Step 2: chrome.tabs.create(${A31_PROBE_TAB_URL}, active:true) — content script ISOLATED world is alive on https://, not on chrome-extension:// (Plan 03-02 lesson)`);
    const probeTab = await chrome.tabs.create({ url: A31_PROBE_TAB_URL, active: true });
    probeTabId = probeTab.id;
    diag(result, `Step 2 result: probeTab.id=${probeTabId}, probeTab.url=${probeTab.url ?? '<pending>'}`);
    if (probeTabId === undefined) {
      throw new Error('chrome.tabs.create returned undefined tab.id');
    }

    diag(result, `Step 3: wait ${A31_TAB_NAVIGATION_WAIT_MS}ms for navigation + content script attach`);
    await new Promise((r) => setTimeout(r, A31_TAB_NAVIGATION_WAIT_MS));

    diag(result, `Step 4: settle ${A31_SEGMENT_SETTLE_MS}ms for first segment rotation`);
    await new Promise((r) => setTimeout(r, A31_SEGMENT_SETTLE_MS));

    diag(result, 'Step 5: chrome.scripting.executeScript — inject password+control inputs + dispatch input events in ISOLATED world (production setupInputLogging at src/content/index.ts:78 sees BOTH, line-82 filter early-returns on the password input only)');
    const injectionResults = await chrome.scripting.executeScript({
      target: { tabId: probeTabId },
      world: 'ISOLATED',
      func: (
        passwordInputId: string,
        passwordSentinel: string,
        controlInputId: string,
        controlSentinel: string,
      ): {
        passwordTyped: boolean;
        controlTyped: boolean;
        passwordDispatched: boolean;
        controlDispatched: boolean;
      } => {
        // Create the synthetic password input. Production
        // setupInputLogging at src/content/index.ts:78 attaches via
        // document.addEventListener('input', ...), so the production
        // path covers any element added to document.body — including
        // ones created and dispatched synchronously by us.
        const passwordInput = document.createElement('input');
        passwordInput.type = 'password';
        passwordInput.id = passwordInputId;
        passwordInput.value = passwordSentinel;
        document.body.appendChild(passwordInput);
        const passwordDispatched = passwordInput.dispatchEvent(
          new Event('input', { bubbles: true }),
        );

        // Create the synthetic control input. setupInputLogging will
        // see this one too — but because target.type !== 'password',
        // the line-82 filter does NOT early-return; addUserEvent fires
        // and the event lands in userEvents[] with type='input' and
        // value containing the control sentinel. A31.4 host-side
        // verifies this (defense-in-depth proving the listener IS
        // alive — without it, A31.2/A31.3 would pass tautologically).
        const controlInput = document.createElement('input');
        controlInput.type = 'text';
        controlInput.id = controlInputId;
        controlInput.value = controlSentinel;
        document.body.appendChild(controlInput);
        const controlDispatched = controlInput.dispatchEvent(
          new Event('input', { bubbles: true }),
        );

        return {
          passwordTyped: passwordInput.value === passwordSentinel,
          controlTyped: controlInput.value === controlSentinel,
          passwordDispatched,
          controlDispatched,
        };
      },
      args: [
        A31_PASSWORD_INPUT_ID,
        A31_PASSWORD_SENTINEL,
        A31_CONTROL_INPUT_ID,
        A31_CONTROL_SENTINEL,
      ],
    });
    const injectionSummary = injectionResults[0]?.result ?? null;
    diag(result, `Step 5 result: ${JSON.stringify(injectionSummary)}`);

    diag(result, `Step 6: settle ${A31_TRIGGER_SETTLE_MS}ms so synchronous handlers complete + userEvents[] populates`);
    await new Promise((r) => setTimeout(r, A31_TRIGGER_SETTLE_MS));

    diag(result, 'Step 7: dispatch SAVE_ARCHIVE (probe tab is the active tab; SW will harvest from there)');
    const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
      { type: 'SAVE_ARCHIVE' },
      A31_SAVE_ARCHIVE_TIMEOUT_MS,
      'SAVE_ARCHIVE (A31)',
    );
    diag(result, `Step 7 result: ${JSON.stringify(ack)}`);

    result.checks.push({
      name: 'A31.1: SAVE_ARCHIVE ack received with success=true',
      expected: true,
      actual: ack.success,
      passed: ack.success === true,
    });

    result.passed = result.checks.every((c) => c.passed);
  } catch (err) {
    result.error = err instanceof Error ? err.message : String(err);
    diag(result, `THREW: ${result.error}`);
  } finally {
    // T-02-04-04 mitigation parity (Plan 03-02 precedent): cleanup probe
    // tab with silent-ignore on already-closed.
    if (probeTabId !== undefined) {
      try {
        await chrome.tabs.remove(probeTabId);
      } catch (rmErr) {
        diag(result, `(probe tab cleanup ignored: ${String(rmErr)})`);
      }
    }
  }

  return result;
}

/**
 * Read `chrome.runtime.getManifest().version`. Used by the host-side
 * orchestrator at startup to capture the expected version for A13's
 * meta.json check. The harness page has the manifest available
 * synchronously via `chrome.runtime.getManifest()` (no async needed),
 * but we wrap it in a Promise for uniform driver evaluation shape.
 *
 * @returns The extension version string (e.g. '1.0.0').
 */
async function getManifestVersion(): Promise<string> {
  return chrome.runtime.getManifest().version;
}

// Install the global harness surface.
declare global {
  interface Window {
    __mokoshHarness: {
      assertA1: () => Promise<AssertionResult>;
      assertA2: () => Promise<AssertionResult>;
      assertA3: () => Promise<AssertionResult>;
      assertA4: () => Promise<AssertionResult>;
      assertA5: () => Promise<AssertionResult>;
      assertA6: () => Promise<AssertionResult>;
      assertA7: () => Promise<AssertionResult>;
      assertA8: () => Promise<AssertionResult>;
      assertA9: () => Promise<AssertionResult>;
      assertA10: () => Promise<AssertionResult>;
      assertA11: () => Promise<AssertionResult>;
      assertA12: () => Promise<AssertionResult>;
      assertA13: () => Promise<AssertionResult>;
      assertA14: () => Promise<AssertionResult>;
      // Plan 01-10 Wave 3 — onboarding + design-swap-readiness
      assertA15: () => Promise<AssertionResult>;
      assertA16: () => Promise<AssertionResult>;
      assertA17: () => Promise<AssertionResult>;
      // Plan 01-12 Wave 6 — design-integration assertions
      assertA18: () => Promise<AssertionResult>;
      assertA19: () => Promise<AssertionResult>;
      assertA20: () => Promise<AssertionResult>;
      assertA21: () => Promise<AssertionResult>;
      assertA22: () => Promise<AssertionResult>;
      // Plan 01-14 — picker narrowing
      assertA23: () => Promise<AssertionResult>;
      // Plan 02-04 Task 1 — Phase 2 closure (A24 D-P2-01 Blob URL)
      assertA24: () => Promise<AssertionResult>;
      // Plan 02-04 Task 2 — REQ-archive-export-latency (5s ceiling)
      assertA25: () => Promise<A25Result>;
      // Plan 02-04 Task 3 — D-P2-02 + D-P2-03 + REQ-archive-layout
      assertA26: () => Promise<AssertionResult>;
      assertA27: () => Promise<A27Result>;
      assertA28: () => Promise<AssertionResult>;
      // Plan 03-01 — rrweb DOM verification (SPEC §10 #4)
      assertA29: () => Promise<AssertionResult>;
      // Plan 03-02 — event-log verification (SPEC §10 #5)
      assertA30: () => Promise<AssertionResult>;
      // Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02)
      assertA31: () => Promise<AssertionResult>;
      getManifestVersion: () => Promise<string>;
    };
  }
}

window.__mokoshHarness = {
  assertA1,
  assertA2,
  assertA3,
  assertA4,
  assertA5,
  assertA6,
  assertA7,
  assertA8,
  assertA9,
  assertA10,
  assertA11,
  assertA12,
  assertA13,
  assertA14,
  assertA15,
  assertA16,
  assertA17,
  assertA18,
  assertA19,
  assertA20,
  assertA21,
  assertA22,
  assertA23,
  assertA24,
  assertA25,
  assertA26,
  assertA27,
  assertA28,
  assertA29,
  assertA30,
  assertA31,
  getManifestVersion,
};

const statusEl = document.getElementById('status');
if (statusEl !== null) {
  statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A31, getManifestVersion} available.';
}

console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + Plan 03-02: A30 + Plan 03-03: A31 + getManifestVersion)');

export {};
