Files
mokosh/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md

791 lines
40 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 3: SPEC §10 smoke verification + DOM/event-log verification - Pattern Map
**Mapped:** 2026-05-20
**Files analyzed:** 5 anticipated (4 modifications + 1 new VERIFICATION.md) + optional 6th (Page.metrics scaffolding)
**Analogs found:** 5 / 5 (4 exact, 1 role-match; +1 partial for optional Page.metrics)
**Phase character:** Verification-heavy; Approach B harness extension (Plan 02-04 is direct precedent for 4 of 5 files).
## File Classification
| Anticipated File (Plans 03-01..05) | Role | Data Flow | Closest Analog | Match Quality |
|-------------------------------------|------|-----------|----------------|---------------|
| `tests/uat/extension-page-harness.ts` (page-side `assertA29..A3X`) | test (page-side assertion host) | request-response (chrome.runtime.sendMessage round-trip) + event-driven (chrome.downloads.onCreated listener for A24 echo, if needed by Plan 03-02) | `tests/uat/extension-page-harness.ts:2851-3413` (assertA24..assertA28 block; Plan 02-04) | EXACT (same file; append-only) |
| `tests/uat/lib/harness-page-driver.ts` (host-side `driveA29..A3X` + `JSZip` reads) | test (host-side driver + zip inspection) | file-I/O (downloadsDir polling + JSZip-parse) + request-response (page.evaluate wrapper) | `tests/uat/lib/harness-page-driver.ts:1202-1849` (driveA24..driveA28 + `findLatestZip` + `A28_EXPECTED_PATHS`; Plan 02-04) | EXACT (same file; append-only) |
| `tests/uat/harness.test.ts` (orchestrator wiring: imports + wrapped drivers + drivers array push + banner) | test (orchestrator) | request-response (sequential driver dispatch + bail-on-first-failure) | `tests/uat/harness.test.ts:93-99 + 316-335 + 406-430` (driveA24-A28 imports + wrappers + push; Plan 02-04) | EXACT (same file; append-only) |
| `tests/uat/extension-page-harness.html` (probe HTML append) | test (DOM scaffold) | static markup | `tests/uat/extension-page-harness.html:17-22` (existing `<h1>` + `<pre id="status">`; Plan 01-12 Wave 6 tokens.css preserved) | EXACT (same file; append-only) |
| `.planning/phases/03-.../03-VERIFICATION.md` (NEW; aggregator) | docs (verification aggregator with frontmatter) | docs synthesis | `.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md` (T5 override pattern; 5/5 must-haves; Plan 02-04 closure) PLUS `.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md` (per-requirement scorecard; cross-cutting gates table; operator empirical acks; deferred items section) | EXACT (T5 override) + ROLE-MATCH (scorecard structure) |
| `tests/uat/lib/ramMetrics.ts` OR inline in `harness-page-driver.ts` (OPTIONAL — D-P3-04 best-effort `puppeteer.Page.metrics()` scaffolding) | test (host-side utility) | request-response (CDP `Performance.getMetrics` via `Page.metrics()`) | NONE (research-derived; no existing analog) — closest scaffolding shape lives in `tests/uat/lib/harness-page-driver.ts:1255-1334` (driveA25 host-side latency measurement + diagnostic copy pattern) | NEW PATTERN (cite RESEARCH §"Code Examples" A3X) |
## Pattern Assignments
### `tests/uat/extension-page-harness.ts` (page-side `assertA29..A3X` for Plans 03-01..04)
**Analog:** `tests/uat/extension-page-harness.ts:2851-3413` (Plan 02-04 assertA24-A28)
**Existing imports / module context** — already present at top of file; no new imports expected:
- The harness uses ambient chrome.* types (`chrome.downloads.DownloadItem`, `chrome.tabs.create`, etc.); production surfaces only.
- Helper utilities `setupFreshRecording`, `sendMessageWithTimeout`, `diag` (file-local) — all reusable for Plan 03-01..04.
**Module constants block** — Plan 02-04 precedent for per-assertion tuning constants (lines 2977-2982):
```typescript
/** 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;
```
Phase 3 plans should follow the same `const A<NN>_<PURPOSE>_MS = NNNN_NNN;` form (`A29_SEGMENT_SETTLE_MS`, `A30_TRIGGER_PAUSE_MS`, `A31_PASSWORD_SENTINEL`, etc.).
**Page-side stub pattern (for host-side-only work)**`assertA26` lines 3164-3173 + `assertA28` lines 3307-3316:
```typescript
/**
* 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',
],
};
}
```
**Use for:** Plan 03-01 (rrweb session.json inspection is host-side JSZip), Plan 03-02 (events.json grep is host-side), Plan 03-03 (sentinel grep is host-side). Page-side stubs for A29 / A30 / A31; host-side drivers do the work.
**Page-side orchestration pattern (when chrome.* APIs are required pre-SAVE)**`assertA24` lines 2851-2962 (chrome.downloads.onCreated cross-realm capture) and `assertA27` lines 3202-3292 (chrome.tabs.create + activate + cleanup):
```typescript
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 => {
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)'}`);
}
// ... Step 2: settle, Step 3: install listener, Step 4: dispatch SAVE_ARCHIVE, Step 5: poll for capturedUrl
} 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 (idempotent; no-op if never added).
if (listenerInstalled) {
try { chrome.downloads.onCreated.removeListener(onCreatedListener); }
catch (rmErr) { diag(result, `(listener cleanup ignored: ${String(rmErr)})`); }
}
}
return result;
}
```
**Use for:** Plan 03-02 if event-log triggers (page.click / page.type / dispatchEvent) require pre-SAVE setup beyond what RESEARCH §"Code Examples" pattern 2 describes; Plan 03-03 password-sentinel typing pre-SAVE.
**Page-side extended-result shape (when host needs page data beyond AssertionResult)**`A25Result` lines 2997-3002 + `A27Result` lines 3145-3148:
```typescript
interface A25Result extends AssertionResult {
t0: number; // performance.now() at SAVE dispatch
tAck: number; // performance.now() at SAVE ack
t0Wall: number; // Date.now() at SAVE dispatch (cross-realm bracket anchor)
ackSuccess: boolean;
}
interface A27Result extends AssertionResult {
tabAUrl: string;
tabBUrl: string;
}
```
**Use for:** Plan 03-04 OPTIONAL Page.metrics scaffolding — `A3XResult extends AssertionResult { jsHeapUsedBytes: number; jsHeapTotalBytes: number; }` if Plan 03-04 ships scaffolding (D-P3-04 "if practical"; see RESEARCH §"Code Example A3X").
**`__mokoshHarness` registration pattern** — lines 3332-3372 (TypeScript ambient interface) + lines 3374-3404 (object literal):
```typescript
declare global {
interface Window {
__mokoshHarness: {
assertA1: () => Promise<AssertionResult>;
// ... (24 baseline assertions A1..A23)
assertA24: () => Promise<AssertionResult>;
assertA25: () => Promise<A25Result>;
assertA26: () => Promise<AssertionResult>;
assertA27: () => Promise<A27Result>;
assertA28: () => Promise<AssertionResult>;
getManifestVersion: () => Promise<string>;
};
}
}
window.__mokoshHarness = {
assertA1, assertA2, /* ... */ assertA27, assertA28, getManifestVersion,
};
```
**Use for:** Plans 03-01..04 each append their own `assertA<NN>` to BOTH the `declare global` interface AND the `window.__mokoshHarness = {...}` object literal. Lockstep is critical; missing one side breaks the orchestrator's `await harness.assertA<NN>()` call.
---
### `tests/uat/lib/harness-page-driver.ts` (host-side `driveA29..A3X` + `JSZip`)
**Analog:** `tests/uat/lib/harness-page-driver.ts:1202-1849` (Plan 02-04 driveA24-A28)
**Existing imports (already present in file)** — lines 36-45:
```typescript
import { spawnSync } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve as resolvePath } from 'node:path';
import JSZip from 'jszip';
import type { Page } from 'puppeteer';
import type { AssertionRecord, CheckRecord } from './assertions';
import { assertArchiveShape, extractEntryToFile } from './zip';
```
Plan 03-01 only needs to ADD a single import for the rrweb EventType enum:
```typescript
import { EventType } from '@rrweb/types'; // ← NEW for Plan 03-01 (verified at node_modules/@rrweb/types/dist/index.d.ts:186-194)
```
Plan 03-02 / 03-03 may add an import for `UserEvent` from `src/shared/types.ts`:
```typescript
import type { UserEvent } from '../../../src/shared/types'; // ← NEW for Plan 03-02 events.json grep
```
**Standard `page.evaluate` wrapper for page-orchestrated assertions** — driveA24 lines 1202-1209:
```typescript
export async function driveA24(page: Page): Promise<AssertionRecord> {
return await page.evaluate(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
const harness = (window as any).__mokoshHarness;
const r: AssertionRecord = await harness.assertA24();
return r;
}) as AssertionRecord;
}
```
**Use for:** Plan 03-02 driveA30 (if Plan 03-02 chooses page-side orchestration for event triggers — but RESEARCH recommends Puppeteer-side `page.click` / `page.type` via the page, not chrome.runtime.sendMessage round-trip; planner picks).
**Two-phase pattern (page-side stub + host-side JSZip read)** — driveA26 lines 1421-1567 (canonical 6-check meta.json shape gate; chains off A25):
```typescript
export async function driveA26(
page: Page,
downloadsDir: string,
): Promise<AssertionRecord> {
// Phase 1 — page-side stub (uniform orchestrator shape).
const pageResult = await page.evaluate(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
const harness = (window as any).__mokoshHarness;
const r: AssertionRecord = await harness.assertA26();
return r;
}) as AssertionRecord;
const mergedChecks: CheckRecord[] = pageResult.checks.slice();
const mergedDiagnostics: string[] = pageResult.diagnostics.slice();
// Phase 2 — locate A25's zip.
const zipPath = findLatestZip(downloadsDir);
if (zipPath === null) {
mergedChecks.push({
name: 'A26.0: at least one zip present in downloadsDir (chain-off A25)',
expected: '>=1 zip',
actual: 'no zip in downloadsDir',
passed: false,
});
return { passed: false, name: pageResult.name, checks: mergedChecks,
diagnostics: mergedDiagnostics, error: pageResult.error };
}
mergedDiagnostics.push(`A26 zipPath=${zipPath}`);
// Phase 3 — load + inspect target file inside zip.
const zipBytes = readFileSync(zipPath);
const zip = await JSZip.loadAsync(zipBytes);
const metaFile = zip.file('meta.json');
// ... A26.1..A26.6 structural checks
const mergedPassed = mergedChecks.every((c) => c.passed);
return { passed: mergedPassed, name: pageResult.name, checks: mergedChecks,
diagnostics: mergedDiagnostics, error: pageResult.error };
}
```
**Use for:** All four Plans 03-01..04 — same 3-phase shape. Plan 03-01 reads `rrweb/session.json`, Plan 03-02 reads `logs/events.json`, Plan 03-03 reads `logs/events.json` for sentinel grep.
**`findLatestZip` chain-off helper** — lines 1395-1406 (must be present; if Plan 03-* runs in a fresh wave the helper is already shipped by Plan 02-04):
```typescript
function findLatestZip(downloadsDir: string): string | null {
const candidates = readdirSync(downloadsDir).filter(isZipFilename);
if (candidates.length === 0) {
return null;
}
const withMtimes = candidates.map((name) => ({
name,
mtimeMs: statSync(resolvePath(downloadsDir, name)).mtimeMs,
}));
withMtimes.sort((a, b) => b.mtimeMs - a.mtimeMs);
return resolvePath(downloadsDir, withMtimes[0].name);
}
```
**Use for:** Every Plan 03-01..04 driver that reads from downloadsDir. Already present in file (no re-implementation).
**EventType enum grep pattern (Plan 03-01)** — derived from RESEARCH §"Code Examples A29" (verified against `@rrweb/types/dist/index.d.ts:186-194`):
```typescript
// Plan 03-01 driveA29 — structural rrweb event-shape grep (RESEARCH-cited pattern; no codebase analog)
import { EventType } from '@rrweb/types'; // FullSnapshot=2, IncrementalSnapshot=3, Meta=4
const rrwebRaw = await zip.file('rrweb/session.json')!.async('text');
const events: Array<{ type: number; timestamp: number }> = JSON.parse(rrwebRaw);
mergedChecks.push({
name: 'A29.1: rrweb/session.json contains > 0 events',
expected: '>0',
actual: events.length,
passed: events.length > 0,
});
mergedChecks.push({
name: `A29.2: rrweb emitted at least one Meta event (EventType.Meta=${EventType.Meta})`,
expected: 'has Meta',
actual: events.some((e) => e.type === EventType.Meta),
passed: events.some((e) => e.type === EventType.Meta),
});
mergedChecks.push({
name: `A29.3: rrweb emitted at least one FullSnapshot (EventType.FullSnapshot=${EventType.FullSnapshot})`,
expected: 'has FullSnapshot',
actual: events.some((e) => e.type === EventType.FullSnapshot),
passed: events.some((e) => e.type === EventType.FullSnapshot),
});
mergedChecks.push({
name: `A29.4: rrweb emitted at least one IncrementalSnapshot (EventType.IncrementalSnapshot=${EventType.IncrementalSnapshot})`,
expected: 'has IncrementalSnapshot',
actual: events.some((e) => e.type === EventType.IncrementalSnapshot),
passed: events.some((e) => e.type === EventType.IncrementalSnapshot),
});
```
**Use for:** Plan 03-01 only. RESEARCH §"Pitfall 1" warns: synthetic probe HTML needs at least one DOM mutation between load and SAVE for `IncrementalSnapshot` to fire — Plan 03-01 driver MUST trigger one via `page.evaluate(() => document.body.appendChild(...))` or `page.click('#probe-modal-trigger')`.
**UserEvent type-grep pattern (Plan 03-02)** — derived from RESEARCH §"Code Examples" pattern 2 + `src/shared/types.ts:124-131`:
```typescript
// Plan 03-02 driveA30 — events.json grep for 5 UserEvent.type values
import type { UserEvent } from '../../../src/shared/types';
const eventsRaw = await zip.file('logs/events.json')!.async('text');
const userEvents: UserEvent[] = JSON.parse(eventsRaw);
const EXPECTED_TYPES = ['click', 'input', 'navigation', 'js_error', 'network_error'] as const;
for (const expectedType of EXPECTED_TYPES) {
mergedChecks.push({
name: `A30.<i>: logs/events.json contains at least one '${expectedType}' event`,
expected: `>=1 ${expectedType}`,
actual: userEvents.filter((e) => e.type === expectedType).length,
passed: userEvents.some((e) => e.type === expectedType),
});
}
```
**Negative-assertion pattern (Plan 03-03 password sentinel grep)** — derived from RESEARCH §"Pattern 3" (no codebase analog; closest analog is driveA27's A27.7/A27.8 absence assertions at harness-page-driver.ts:1724-1737):
```typescript
// Plan 03-03 driveA31 — sentinel absence from logs/events.json
const SENTINEL = 'secret-do-not-log-123'; // fixed; not a real secret
const eventsRaw = await zip.file('logs/events.json')!.async('text');
const userEvents: UserEvent[] = JSON.parse(eventsRaw);
const eventsContainingSentinel = userEvents.filter(
(e) => e.value !== undefined && e.value.includes(SENTINEL),
);
mergedChecks.push({
name: 'A31.1: password sentinel ABSENT from logs/events.json (existing src/content/index.ts:82 filter VERIFIED)',
expected: 'absent (0 events containing sentinel)',
actual: `${eventsContainingSentinel.length} events containing sentinel`,
passed: eventsContainingSentinel.length === 0,
});
```
**Filter-pipeline form (no `continue`)** — driveA28 lines 1808-1810 (project style; CLAUDE.md "Control Flow" §):
```typescript
const actualPaths: string[] = Object.keys(zip.files)
.filter((path) => !zip.files[path].dir)
.sort();
```
**Use for:** Any enumeration loop in Plans 03-01..04. No `for...of` + `if (...) continue;` shape.
**Tab-cleanup safety pattern (T-02-04-04)** — assertA27 lines 3271-3289 (try/catch silent-ignore on already-closed):
```typescript
} finally {
// T-02-04-04 mitigation: cleanup tabs with silent-ignore on already-closed.
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)})`); }
}
}
```
**Use for:** Plan 03-02 if event-log trigger strategy involves opening a probe tab (e.g., `https://example.com` for js_error/network_error triggers).
---
### `tests/uat/harness.test.ts` (orchestrator wiring)
**Analog:** `tests/uat/harness.test.ts:65-130 + 316-335 + 406-430` (Plan 02-04)
**Import section pattern** — lines 92-100 (add A24-A28 imports per plan):
```typescript
// Plan 02-04 Task 1 — D-P2-01 empirical Blob URL verification
driveA24,
// Plan 02-04 Task 2 — REQ-archive-export-latency (5s ceiling)
driveA25,
// Plan 02-04 Task 3 — meta.json 8-field + multi-tab strict + REQ-archive-layout
driveA26,
driveA27,
driveA28,
getManifestVersion,
} from './lib/harness-page-driver';
```
**Use for:** Each Plan 03-01..04 appends ONE `driveA<NN>` import + a section banner comment naming the plan and assertion purpose.
**Wrapped-driver pattern (when driver needs `downloadsDir` beyond the standard `Page` arg)** — lines 322-335 (Plan 02-04 wrappers):
```typescript
// Plan 02-04 Task 2 — driveA25 needs downloadsDir for the host-side
// dispatch→file-on-disk latency check (mirrors A5/A12/A13 wrapping).
const driveA25Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
(page) => driveA25(page, handles.downloadsDir);
// Plan 02-04 Task 3 — driveA26/A27/A28 need downloadsDir for host-side
// zip inspection (JSZip-parse meta.json + zip-layout enumeration). A26
// chains off A25's zip (no new SAVE); A27 owns its SAVE (multi-tab);
// A28 chains off A27's zip (no new SAVE).
const driveA26Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
(page) => driveA26(page, handles.downloadsDir);
const driveA27Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
(page) => driveA27(page, handles.downloadsDir);
const driveA28Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
(page) => driveA28(page, handles.downloadsDir);
```
**Use for:** Plans 03-01..03 (all need `downloadsDir` to find the latest zip). Same wrapping pattern; one wrapper per driver.
**Drivers-array push pattern with explanatory banner comment** — lines 397-430 (Plan 02-04):
```typescript
// Plan 02-04 Task 1 A24: D-P2-01 empirical Blob URL verification.
// Installs chrome.downloads.onCreated listener cross-realm, dispatches
// SAVE_ARCHIVE, captures the download URL, asserts the `blob:` prefix
// (closes audit P0-6 end-to-end through a real Chrome instance +
// the offscreen mint round-trip + chrome.downloads platform call).
{ name: 'A24', drive: driveA24 },
// Plan 02-04 Task 2 A25: REQ-archive-export-latency / SPEC §10 #6.
{ name: 'A25', drive: driveA25Wrapped },
// Plan 02-04 Task 3 A26: D-P2-02 + D-P2-03 meta.json 8-field shape.
{ name: 'A26', drive: driveA26Wrapped },
// Plan 02-04 Task 3 A27: STRICT multi-tab urls[] post DEC-011 Amendment 1.
{ name: 'A27', drive: driveA27Wrapped },
// Plan 02-04 Task 3 A28: REQ-archive-layout strict 5-entry zip-layout.
{ name: 'A28', drive: driveA28Wrapped },
];
```
**Use for:** Plans 03-01..04 each append ONE `{ name: 'A<NN>', drive: driveA<NN>Wrapped }` entry with a banner comment citing the plan, the SPEC §10 # being verified, and the chaining strategy (own SAVE vs chain off prior).
**FORBIDDEN_HOOK_STRINGS UAT A0 mirror** — lines 115-130 (12 entries):
```typescript
const FORBIDDEN_HOOK_STRINGS: ReadonlyArray<string> = [
'__mokoshTest',
'setCurrentStream',
'setSegmentCountGetter',
'installFakeDisplayMedia',
'uninstallFakeDisplayMedia',
'dispatchEndedOnTrack',
'getSegmentCount',
'__mokoshOffscreenQuery',
'get-display-surface',
'get-segment-count',
// Plan 01-14 A23 surface — lockstep with unit-gate inventory at
// tests/background/no-test-hooks-in-prod-bundle.test.ts:105.
'lastGetDisplayMediaConstraints',
'get-last-getDisplayMedia-constraints',
];
```
**Lockstep requirement:** ANY new test-only `__MOKOSH_UAT__`-gated symbol introduced by Plan 03-* MUST be added to BOTH this UAT A0 mirror AND the unit-gate at `tests/background/no-test-hooks-in-prod-bundle.test.ts:108-126`. RESEARCH §"Anti-Patterns" + A6 in Assumptions Log: Phase 3 plans are EXPECTED to stay at 12 entries (rrweb + chrome.downloads + chrome.tabs are production surfaces). The MEDIUM-risk exception (A6) is Plan 03-04 Page.metrics scaffolding — Page.metrics is host-side (not bundled into page-realm); no new symbol expected.
---
### `tests/uat/extension-page-harness.html` (probe HTML append for Plan 03-01)
**Analog:** Current state of the file (lines 17-22) — append below, do NOT touch the head.
**Existing scaffold** (full file, 24 lines):
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mokosh UAT Harness (extension-internal page)</title>
<!--
Plan 01-12 Wave 6: load the canonical token system on the harness
page so A18 (Lora WOFF2 reachable via @font-face) and A21
(--mks-font-display resolves to Lora) both have the @font-face
rules + CSS custom properties + .mks-display-1 class visible via
document.styleSheets + getComputedStyle. Vite's crxjs plugin
handles the asset path rebasing at build:test time so the link
resolves under dist-test/ even with content-hashed asset filenames.
-->
<link rel="stylesheet" href="../../src/shared/tokens.css">
</head>
<body>
<h1>Mokosh UAT — extension-internal page harness</h1>
<p>This page lives at <code>chrome-extension://&lt;id&gt;/tests/uat/extension-page-harness.html</code>.</p>
<p>Puppeteer navigates a tab here and drives assertions via <code>window.__mokoshHarness.*</code>.</p>
<pre id="status">Ready.</pre>
<script type="module" src="./extension-page-harness.ts"></script>
</body>
</html>
```
**Probe-HTML append target:** between `<pre id="status">` (line 21) and `<script type="module">` (line 22). Form + table + modal-trigger HTML per RESEARCH §"Pitfall 4" (NO `<textarea>`; rrweb alpha.4 bug #1596).
**Critical constraints (per RESEARCH Assumption A2 + A5):**
- DO NOT modify the `<head>` block — `<link rel="stylesheet" href="../../src/shared/tokens.css">` is load-bearing for A18 + A21.
- DO NOT add stylesheet imports for the probe HTML — inline styles via `style="..."` or via the existing tokens.css `--mks-*` vars (defensive).
- Probe HTML stays minimal: 1 form (`<input type="text">` + `<input type="email">` + `<input type="password">` + `<button>`), 1 small table (2-3 rows), 1 hidden modal `<div>` toggled by a trigger button. Per RESEARCH §"Pitfall 1": at least one DOM mutation is required pre-SAVE for `IncrementalSnapshot` to fire.
---
### `.planning/phases/03-.../03-VERIFICATION.md` (NEW; aggregator)
**Analogs:**
- `.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md` (T5 override pattern + `overrides_applied` field; closest match for Plan 03-05 charter)
- `.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md` (per-requirement scorecard + cross-cutting gates + operator empirical acks + deferred items)
**Frontmatter shape (from `02-VERIFICATION.md:1-32`):**
```yaml
---
phase: 02-stabilize-export-pipeline
verified: 2026-05-20T18:00:00Z
status: passed
score: 5/5 must-haves verified
overrides_applied: 1
override_notes:
- dimension: "T5 operator empirical UAT (Plan 02-04 Task 4 Step 2)"
initial_status: "UNCERTAIN (human_needed)"
override_to: "VERIFIED"
rationale: |
User explicit delegation 2026-05-20: "why do i need to do all of this? It's on you to test..."
established the operating principle that automation covers what automation can cover.
Per saved memory feedback-trust-harness-over-manual-uat.md (created same session 2026-05-20),
operator empirical UAT is reserved for surfaces automation genuinely cannot verify (brand
judgment, ergonomics). For Phase 2 deep-pipeline work (Blob URL, meta schema, archive layout),
every operator-checklist surface IS covered by harness assertions:
- (a) <5s latency → A25 empirical via Puppeteer (real Chrome)
- ... (5 sub-bullets covering each empirical channel)
... (citation block ending with "The harness IS the canonical Phase 2 verification per the current memory.")
human_verification: []
---
```
**Phase 3 adaptation (per CONTEXT D-P3-02 + D-P3-04):**
```yaml
---
phase: 03-spec-10-smoke-verification-dom-event-log-verification
verified: 2026-05-20T<HH-MM-SS>Z
status: passed
score: 9/9 SPEC §10 criteria
overrides_applied: 1
override_notes:
- dimension: "SPEC §10 #8 — Password masking (PARTIAL per D-P3-02 charter)"
initial_status: "PARTIAL"
rationale: |
Full rrweb v2 maskInputFn + data-sensitive guards DEFERRED per 2026-05-20 charter
'we don't care about privacy hardening. At least here.' REQ-password-confidentiality
moved to Out of Scope v1. Existing src/content/index.ts:82
`if (target.type === 'password') return;` filter VERIFIED by Plan 03-03 A31
(sentinel string typed; greps events.json for absence).
human_verification:
- dimension: "SPEC §10 #9 — Extension RAM ≤ 50 MB"
rationale: |
Per D-P3-04: operator instructions in VERIFICATION below — load extension, idle 5 min,
open chrome://memory-internals OR chrome://extensions service-worker memory display,
verify extension background RAM < 50 MB. Page.metrics scaffolding (if shipped by
Plan 03-04) measures page-realm only and does not cover the SW target.
---
```
**Per-Requirement Scorecard table** (from `01-VERIFICATION.md:32-46`):
```markdown
## Per-Requirement Scorecard
| # | Requirement / Decision | Evidence | Status |
|---|---|---|---|
| 1 | REQ-video-ring-buffer | `src/offscreen/recorder.ts:52-58` (`MAX_SEGMENTS=3 × 10s = 30s`); ... | PASS |
| 2 | REQ-install-clean | `npm run build` exit 0, 9 chunks produced ... | PASS |
| ... |
```
**Phase 3 adaptation:** §10 #1-#9 row-by-row evidence table with Phase-citation (Phase 1 / Phase 2 / Phase 3) + plan-citation (e.g., "Plan 03-01 A29 GREEN") + commit-citation per criterion.
**Cross-Cutting Gates table** (from `01-VERIFICATION.md:48-56`):
```markdown
## Cross-Cutting Gates
| Gate | Evidence | Status |
|---|---|---|
| vitest | 28 files / **153 tests / 153 GREEN** ... | PASS |
| UAT harness | 24 drivers (A0 grep gate + A1..A14 + A15..A17 + A18..A22 + A23) ... | PASS |
| Tier-1 grep gate | 12 FORBIDDEN_HOOK_STRINGS ... | PASS |
| Pre-checkpoint bundle gates | 0 `googleapis`/`https://fonts` in dist; 0 test-hook leaks ... | PASS |
| tsc | `npx tsc --noEmit` exit 0 | PASS |
```
**Phase 3 adaptation:** Update UAT harness row (3X drivers post Plan 03-01..04); preserve baseline vitest + Tier-1 + bundle gates; cite Plan 03-05's own re-verification of all 6 pre-checkpoint bundle gates per saved memory `feedback-pre-checkpoint-bundle-gates.md`.
**Operator-Empirical Acks table** (from `01-VERIFICATION.md:70-78`):
```markdown
## Operator-Empirical Acks (verbatim + commit refs)
| Date | Plan | Operator response | Commit |
|---|---|---|---|
| 2026-05-15 | 01-07 (D-12 + A3 closure) | Chrome playback clean, ffmpeg dry-run exit 0 | cd61cbc |
| ... |
```
**Phase 3 adaptation:** Single row for the §10 #9 RAM operator ack (when received). Empty until ack lands; then verbatim quote + commit.
**Deferred Items section** (from `01-VERIFICATION.md:92-104` + `02-VERIFICATION.md:55-58`):
```markdown
## Forward-Looking Deferred Items (NOT gaps)
| Item | Owner | Source |
|---|---|---|
| rrweb v2 stable upgrade | Phase 4 | D-P3-03 defer rationale |
| Programmatic RAM measurement (puppeteer.Page.metrics enhancement OR chrome.devtools Memory API) | Phase 4 | D-P3-04 defer rationale |
| REQ-password-confidentiality v2 candidate (rrweb v2 maskInputFn + data-sensitive guards) | Phase 4 | D-P3-02 defer rationale |
| ... |
```
**Phase 3 adaptation:** Pull from CONTEXT §"Deferred" verbatim (10 items including the audit P1 polish + setimmediate polyfill + ROADMAP backfill).
---
### `tests/uat/lib/ramMetrics.ts` (OPTIONAL — Plan 03-04 Page.metrics scaffolding)
**Analog:** NONE in codebase. Closest scaffolding shape: `driveA25` host-side latency measurement pattern (`tests/uat/lib/harness-page-driver.ts:1255-1334`). New pattern; cite RESEARCH §"Code Example A3X" + §"Pitfall 2".
**Decision per D-P3-04 + RESEARCH Open Question 3 (recommended: SHIP):** Plan 03-04 ships ~30 lines of Page.metrics scaffolding either inline in `harness-page-driver.ts` (preferred per scope-minimization) OR as a new `tests/uat/lib/ramMetrics.ts` helper if planner prefers separation. RESEARCH §"Open Question 3" recommends shipping with explicit diagnostic copy.
**Concrete pattern (per RESEARCH §"Code Example A3X"):**
```typescript
// Plan 03-04 optional driveA3X — Page.metrics() scaffolding (D-P3-04 best-effort)
async function driveA3X_OptionalRamScaffold(page: Page): Promise<AssertionRecord> {
// Page.metrics is page-realm only — JSHeapUsedSize covers V8 isolate of THIS Page,
// NOT the MV3 service worker (separate target).
const metrics = await page.metrics();
const jsHeapMB = metrics.JSHeapUsedSize !== undefined
? metrics.JSHeapUsedSize / (1024 * 1024)
: -1;
const checks: CheckRecord[] = [
{
name: 'A3X.1: Page.metrics returned JSHeapUsedSize',
expected: '>= 0',
actual: String(jsHeapMB),
passed: jsHeapMB >= 0,
},
{
name: 'A3X.2: Page-realm JS heap < 50 MB (NOTE: scaffolding only; SW context excluded)',
expected: '< 50 MB',
actual: `${jsHeapMB.toFixed(2)} MB`,
passed: jsHeapMB < 50,
},
];
return {
passed: checks.every((c) => c.passed),
name: 'A3X — RAM scaffolding (best-effort; page-realm only per D-P3-04)',
checks,
diagnostics: [
'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.',
],
};
}
```
**Critical diagnostic copy gate (per RESEARCH §"Pitfall 2"):** The scaffolding MUST emit `'NOTE: page-realm only; SW context excluded'` in diagnostics so an operator does not interpret a green automation number as full §10 #9 closure. The binding §10 #9 gate is the operator chrome://memory-internals check; scaffolding is informational only.
---
## Shared Patterns
### Approach B Harness Extension Lockstep
**Source:** Plan 02-04 + Plan 01-13 (Approach B foundation)
**Apply to:** Plans 03-01, 03-02, 03-03, 03-04 (every Phase 3 plan that ships a new assertion)
The 3-file lockstep across each new assertion:
1. **`tests/uat/extension-page-harness.ts`** — append `async function assertA<NN>(): Promise<AssertionResult>` (page-side stub OR orchestrator) + extend `declare global` interface + add entry to `window.__mokoshHarness = {...}` object literal.
2. **`tests/uat/lib/harness-page-driver.ts`** — append `export async function driveA<NN>(page: Page, downloadsDir: string): Promise<AssertionRecord>` (3-phase pattern: page.evaluate stub → findLatestZip → JSZip read).
3. **`tests/uat/harness.test.ts`** — append `driveA<NN>` import + wrapped-driver const (if `downloadsDir` is needed) + drivers-array push with banner comment.
Missing ANY one of the three breaks the orchestrator (either at the `harness.assertA<NN>()` page.evaluate call OR at the `for (const drive of drivers)` loop). Plan-checker MUST validate all three are touched per new A-number.
### Pre-Checkpoint Bundle Gates (6/6 Standard Inventory)
**Source:** Saved memory `feedback-pre-checkpoint-bundle-gates.md` + `02-04-SUMMARY.md:158-168` (canonical 7-gate table; Phase 2 closure precedent)
**Apply to:** Plan 03-05 (before VERIFICATION.md is written) — ALL plans before any operator step
Standard 6/6 inventory (Plan 02-04 ran 7; Plan 03-05 should run the same):
| Gate | Concrete Check |
|------|----------------|
| Gate 1 | `npm run build` exit 0 |
| Gate 2 | SW CSP-safety: `grep -rn "new Function\\|eval(" dist/assets/` — 1 documented exception (setimmediate polyfill) |
| Gate 3 | SW Node-globals: `grep -rn "Buffer.from\\|Buffer.alloc\\|require(" dist/assets/index.ts-*.js` — 0 hits |
| Gate 4 | DOM-globals: `grep -rn "window.\\|document." dist/assets/index.ts-*.js` — bundled-lib idiom (typeof-guarded) |
| Gate 5 | Tier-1 SW-bundle-import gate (`tests/background/sw-bundle-import.test.ts`) GREEN |
| Gate 6 | FORBIDDEN_HOOK_STRINGS unit gate (`tests/background/no-test-hooks-in-prod-bundle.test.ts`) — 12 strings, 0 hits each |
| Gate 7 | Manifest validation gates: i18n + locale-parity + build — all GREEN |
### T5 Override Pattern (Harness-Coverage-as-Verification)
**Source:** `02-VERIFICATION.md:1-31` + saved memory `feedback-trust-harness-over-manual-uat.md`
**Apply to:** Plan 03-05 VERIFICATION.md — for §10 criteria where harness coverage exists (§10 #4 / #5 / #8) but NOT for §10 #9 RAM (genuine non-automatable)
When verifier returns `human_needed` for a criterion AND a harness assertion empirically covers the same surface:
- Move `human_verification` entry to `overrides_applied` block in frontmatter
- Include explicit user-delegation citation (date + verbatim quote, if recorded) + saved-memory reference
- Sub-bullet each harness assertion that covers the surface
- Mark `status: passed` rather than `status: human_needed`
**Counter-example (do NOT override):** §10 #9 RAM measurement — Page.metrics scaffolding is page-realm only (RESEARCH §"Pitfall 2"); the SW context can ONLY be measured via operator-driven chrome://memory-internals. This is the canonical `human_verification` entry per D-P3-04.
### Filter-Pipeline Form (No `continue`)
**Source:** `~/.claude/CLAUDE.md` "Control Flow" § + Plan 02-04 driveA28 (lines 1808-1810)
**Apply to:** Any enumeration loop in Plans 03-01..04
```typescript
// PREFERRED — filter pipeline
const matchingEvents = userEvents.filter((e) => e.type === 'click');
const realHttpUrls = urls.filter((u) => /^https?:\/\//.test(u));
// AVOID — for...of + continue
// for (const e of userEvents) { if (e.type !== 'click') continue; ... }
```
### Production-Surface-Only New Assertions (FORBIDDEN_HOOK_STRINGS stays at 12)
**Source:** `02-04-SUMMARY.md:99` + RESEARCH Assumption A6
**Apply to:** Plans 03-01..04 — riding production surfaces (rrweb.record + content-script GET_RRWEB_EVENTS + chrome.tabs.create + chrome.downloads.onCreated)
Phase 3 assertions ride production surfaces verified by Plans 01-13 + 02-04. NO new `__MOKOSH_UAT__`-gated test-only symbols expected. If Plan 03-04 Page.metrics scaffolding requires a new bridge op (e.g., `get-page-metrics` from offscreen → harness), that scenario adds 1-2 entries to BOTH FORBIDDEN_HOOK_STRINGS inventories AND requires Vite define-token tree-shake gating. Plan-checker MUST validate inventory count.
### Anti-Pattern: Re-reading the Same Range
**Source:** CLAUDE.md operator guidance
**Apply to:** All Phase 3 implementation work — read each source file ONCE, extract patterns, then implement
---
## No Analog Found
| File | Role | Data Flow | Reason / Mitigation |
|------|------|-----------|---------------------|
| `tests/uat/lib/ramMetrics.ts` (if Plan 03-04 chooses separate-file scaffolding) | test (host-side utility) | request-response (CDP Performance.getMetrics) | Page.metrics() has no codebase precedent. RESEARCH §"Code Example A3X" provides the canonical implementation; planner uses verbatim. Plan 03-04 may inline this in `harness-page-driver.ts` instead of new file (scope-minimization preferred). |
All other anticipated files have direct analogs from Plan 02-04 (most recent Approach B precedent).
## Per-Plan Anticipated File Touch Inventory
| Plan | Files Modified | Files Created | Pattern Source |
|------|----------------|---------------|----------------|
| 03-01 (rrweb DOM §10 #4) | `tests/uat/extension-page-harness.ts` + `tests/uat/extension-page-harness.html` (probe HTML) + `tests/uat/lib/harness-page-driver.ts` + `tests/uat/harness.test.ts` | — | Plan 02-04 (assertA26/driveA26 + JSZip + EventType import) |
| 03-02 (event-log §10 #5) | `tests/uat/extension-page-harness.ts` + `tests/uat/lib/harness-page-driver.ts` + `tests/uat/harness.test.ts` | — | Plan 02-04 (driveA26 chain pattern + UserEvent grep from src/shared/types.ts:124-131) |
| 03-03 (password-filter §10 #8 PARTIAL) | `tests/uat/extension-page-harness.ts` + `tests/uat/lib/harness-page-driver.ts` + `tests/uat/harness.test.ts` (possibly probe HTML if Plan 03-01 didn't add `<input type="password">`) | — | RESEARCH §"Pattern 3" + driveA27 absence-assertion shape |
| 03-04 (RAM §10 #9 best-effort) | OPTIONAL: `tests/uat/lib/harness-page-driver.ts` (Page.metrics inline) + `tests/uat/harness.test.ts` (wrapped driver) | OPTIONAL: `tests/uat/lib/ramMetrics.ts` | RESEARCH §"Code Example A3X" (no codebase analog) |
| 03-05 (§10 sweep aggregator) | — | `.planning/phases/03-.../03-VERIFICATION.md` | `01-VERIFICATION.md` (scorecard structure) + `02-VERIFICATION.md` (T5 override frontmatter) |
## Wave Sequencing Note
Per CONTEXT §"Claude's Discretion" + RESEARCH Open Question 1 + Pitfall 6: Plans 03-01..04 modify the SAME three harness files (`extension-page-harness.ts`, `harness-page-driver.ts`, `harness.test.ts`). RESEARCH recommends SEQUENTIAL execution within Wave 2: 03-01 → 03-02 → 03-03 → 03-04. Plan-checker validates `files_modified` overlap audit. Plan 03-05 (VERIFICATION.md aggregator) runs in Wave 3 after 03-01..04 land + UAT harness reaches 33/33 (or 32/32 if Plan 03-04 skips scaffolding).
## Metadata
**Analog search scope:** `tests/uat/`, `.planning/phases/01-stabilize-video-pipeline/`, `.planning/phases/02-stabilize-export-pipeline/`, `src/content/index.ts`, `src/shared/types.ts`, `node_modules/@rrweb/types/dist/index.d.ts`
**Files read (each ONCE, non-overlapping ranges):**
- `tests/uat/extension-page-harness.ts:2840-3409` (assertA24-A28 + __mokoshHarness wire)
- `tests/uat/lib/harness-page-driver.ts:1-130, 1180-1349, 1360-1599, 1700-1849` (4 non-overlapping ranges; covers driveA24/A25/A26/A27/A28 + findLatestZip + A28_EXPECTED_PATHS + imports)
- `tests/uat/harness.test.ts:1-100, 100-260, 320-440` (3 non-overlapping ranges; covers orchestrator imports + FORBIDDEN_HOOK_STRINGS + A0 gate + drivers wrap + push)
- `tests/uat/extension-page-harness.html` (full; 24 lines)
- `tests/uat/lib/zip.ts` (full; 140 lines)
- `tests/background/no-test-hooks-in-prod-bundle.test.ts:95-175` (FORBIDDEN_HOOK_STRINGS unit-gate)
- `.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md` (full; 123 lines)
- `.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md` (full; 166 lines)
- `.planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md` (full; 259 lines)
- `src/content/index.ts:75-89` (password filter at line 82) + grep for rrweb wiring anchors
- `src/shared/types.ts:120-155` (UserEvent + SessionMetadata)
- `node_modules/@rrweb/types/dist/index.d.ts:186-194` (EventType enum)
**Pattern extraction date:** 2026-05-20