--- phase: 03 slug: spec-10-smoke-verification-dom-event-log-verification plan: 01 type: execute wave: 1 depends_on: [] files_modified: - tests/uat/extension-page-harness.html - tests/uat/extension-page-harness.ts - tests/uat/lib/harness-page-driver.ts - tests/uat/harness.test.ts autonomous: true requirements: - REQ-rrweb-dom-buffer tags: - uat-harness - a29 - rrweb - spec-10-4 - approach-b - probe-html user_setup: [] must_haves: truths: - "rrweb session.json contains > 0 events after a probe-page interaction" - "rrweb emits at least one Meta event (EventType=4) on session start" - "rrweb emits at least one FullSnapshot (EventType=2) on session start" - "rrweb emits at least one IncrementalSnapshot (EventType=3) after a DOM mutation on the probe page" - "UAT harness exits 0 with 29 + 1 = 30/30 assertions GREEN (A0..A28 baseline preserved + new A29)" artifacts: - path: "tests/uat/extension-page-harness.html" provides: "Probe HTML (form + table + modal trigger + DOM mutation source) appended below existing scaffold; tokens.css link in head untouched" contains: "id=\"probe-form\"" - path: "tests/uat/extension-page-harness.ts" provides: "assertA29 page-side stub registered on window.__mokoshHarness" contains: "assertA29" - path: "tests/uat/lib/harness-page-driver.ts" provides: "driveA29 host-side: triggers probe-page DOM mutation, runs setupFreshRecording + SAVE, JSZip-parses rrweb/session.json, EventType enum grep" contains: "driveA29" - path: "tests/uat/harness.test.ts" provides: "driveA29 import + wrapped driver + drivers-array push entry with banner comment" contains: "driveA29" key_links: - from: "tests/uat/harness.test.ts" to: "tests/uat/lib/harness-page-driver.ts driveA29" via: "import + wrapped driver const + drivers-array push" pattern: "driveA29Wrapped" - from: "tests/uat/lib/harness-page-driver.ts driveA29" to: "tests/uat/extension-page-harness.ts assertA29" via: "page.evaluate(() => window.__mokoshHarness.assertA29())" pattern: "harness.assertA29\\(\\)" - from: "tests/uat/lib/harness-page-driver.ts driveA29" to: "@rrweb/types EventType enum" via: "import { EventType } from '@rrweb/types'" pattern: "import.*EventType.*from.*@rrweb/types" --- Extend the UAT harness with A29 — empirical verification that SPEC §10 #4 (rrweb DOM event recording on typical pages) is satisfied. Production wiring at `src/content/index.ts:284-309` already ships `rrweb.record()` + `maskInputOptions.password=true`; A29 confirms it actually emits the required EventType events on a synthetic probe page (form + table + modal + DOM-mutation trigger). Purpose: Closes REQ-rrweb-dom-buffer empirical verification gap. Phase 1 shipped the wiring; Phase 3 confirms it works end-to-end through a real Chrome instance + a real probe page. Output: 4-check A29 assertion (events.length > 0 + Meta present + FullSnapshot present + IncrementalSnapshot present); UAT count 29 → 30 GREEN; probe HTML appended to extension-page-harness.html below the existing `` scaffold (head unchanged; tokens.css link preserved for A18/A21). @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/REQUIREMENTS.md @.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md @.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md @.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md @.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-UI-SPEC.md @.planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md @src/content/index.ts @tests/uat/extension-page-harness.html From @rrweb/types/dist/index.d.ts (lines 186-194): ```typescript export declare enum EventType { DomContentLoaded = 0, Load = 1, FullSnapshot = 2, IncrementalSnapshot = 3, Meta = 4, Custom = 5, Plugin = 6 } ``` From tests/uat/lib/assertions.ts (lines 25-44): ```typescript export interface CheckRecord { readonly name: string; readonly expected: unknown; readonly actual: unknown; readonly passed: boolean; } export interface AssertionRecord { readonly passed: boolean; readonly name: string; readonly checks: ReadonlyArray; readonly diagnostics: ReadonlyArray; readonly error?: string; } ``` From tests/uat/extension-page-harness.ts (line 169): ```typescript interface AssertionResult { passed: boolean; name: string; checks: Array<{ name: string; expected: unknown; actual: unknown; passed: boolean }>; diagnostics: string[]; error?: string; } function diag(result: AssertionResult, line: string): void; // line 190 async function setupFreshRecording(): Promise<{ ok: boolean; error?: string }>; // line 825 async function sendMessageWithTimeout(msg: unknown, timeoutMs: number, label: string): Promise; // line 234 ``` From tests/uat/lib/harness-page-driver.ts (lines 1395, 41): ```typescript function findLatestZip(downloadsDir: string): string | null; // host-side; mtime-sort wins import JSZip from 'jszip'; // host-only, not bundled to page ``` From src/content/index.ts (verifier subject — READ-ONLY): ```typescript // Lines 284-311: record({ emit(event){ rrwebEvents.push(event) }, maskInputOptions: { password: true, ... } }) // Line 318: chrome.runtime.onMessage 'GET_RRWEB_EVENTS' returns { events, userEvents } // Line 82: setupInputLogging filter — if (target.type === 'password') return; ``` From src/background/index.ts (READ-ONLY): ```typescript // GET_RRWEB_EVENTS handler in createArchive: chrome.tabs.sendMessage(activeTabId, { type: 'GET_RRWEB_EVENTS' }) // Result lands in rrweb/session.json inside the assembled zip ``` # Plan Anchors - **Approach B pattern source:** Plan 02-04 driveA26 (canonical host-side JSZip read; chains off prior SAVE) — see `02-04-SUMMARY.md` and `tests/uat/lib/harness-page-driver.ts:1421-1567` for the exact 3-phase shape (page-side stub → findLatestZip → JSZip-parse). - **RESEARCH Pitfall 1 (MUST OBSERVE):** synthetic probe HTML produces `Meta` + `FullSnapshot` on load but NOT `IncrementalSnapshot` unless a DOM mutation happens between page load and SAVE. Plan must inject a mutation pre-SAVE (e.g. click the modal trigger button or input text). - **RESEARCH Pitfall 4 (HARD BAN):** Probe HTML MUST NOT include `` — rrweb 2.0.0-alpha.4 issue #1596 leaks textarea values even with `maskInputOptions.textarea` set. - **UI-SPEC ban (HARD):** Probe HTML must NOT modify the `` block. The existing `` is load-bearing for A18 + A21. Probe HTML appends below the existing `` (line 21) and BEFORE the existing `` (line 22). 3. Insert the following probe HTML block BETWEEN line 21 and line 22 (preserving the trailing newline structure): ```html Submit col-acol-b row-1-arow-1-b row-2-arow-2-b Toggle modal Probe modal content (Plan 03-01 A29 IncrementalSnapshot trigger). ``` 4. Verify no `` was introduced (grep should return 0 hits on the modified file). 5. Verify the `` block is unchanged. 6. No new dependencies, no other file edits in this task. grep -c "id=\"probe-form\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' && grep -c "id=\"probe-modal-trigger\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' && grep -c "id=\"probe-table\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' && ! grep -q "textarea" tests/uat/extension-page-harness.html && grep -q '' tests/uat/extension-page-harness.html - `grep -c 'id="probe-form"' tests/uat/extension-page-harness.html` returns exactly `1`. - `grep -c 'id="probe-modal-trigger"' tests/uat/extension-page-harness.html` returns exactly `1`. - `grep -c 'id="probe-table"' tests/uat/extension-page-harness.html` returns exactly `1`. - `grep -c 'id="probe-password"' tests/uat/extension-page-harness.html` returns exactly `1` (carried into Plan 03-03). - `grep -c 'textarea' tests/uat/extension-page-harness.html` returns `0`. - `grep -c '' tests/uat/extension-page-harness.html` returns exactly `1` (head unchanged). - `grep -c 'data-mokosh-slot\|data-mokosh-key\|data-mokosh-i18n-key' tests/uat/extension-page-harness.html` returns `0`. - `npm run build` exits 0. Probe HTML lives in the harness page below the existing scaffold; head + script tag + existing body content all unchanged; no textarea; ready for Task 2 to wire assertA29. Task 2: Add assertA29 page-side stub + driveA29 host-side + harness.test.ts orchestrator wiring tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts - tests/uat/extension-page-harness.ts lines 3151-3413 (assertA26 stub + assertA27 multi-tab + assertA28 stub + declare global block + window.__mokoshHarness object literal — pattern for new assertion) - tests/uat/lib/harness-page-driver.ts lines 1395-1567 (findLatestZip + driveA26 3-phase pattern; canonical for host-side JSZip parse + EventType-style structural assertions) - tests/uat/lib/harness-page-driver.ts lines 36-46 (existing imports + JSZip + AssertionRecord/CheckRecord) - tests/uat/harness.test.ts lines 65-130 (existing driver imports + FORBIDDEN_HOOK_STRINGS at 12 entries) - tests/uat/harness.test.ts lines 316-431 (existing wrapped-driver consts + drivers-array push pattern) - node_modules/@rrweb/types/dist/index.d.ts lines 186-194 (EventType enum values; confirmed in interfaces above) - .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md §"EventType enum grep pattern (Plan 03-01)" and §"Page-side stub pattern" (canonical implementation) Page-side (`tests/uat/extension-page-harness.ts`): - Adds module-local constants `A29_SEGMENT_SETTLE_MS = 11_000` (matches A24/A25/A27 settle window). - Adds `async function assertA29(): Promise` that - dispatches a programmatic DOM mutation on the probe HTML: - sets `#probe-text.value = 'probe'` and dispatches `Event('input', { bubbles: true })` - clicks `#probe-modal-trigger` (toggles `#probe-modal` style.display) - waits 500 ms for rrweb to enqueue the IncrementalSnapshot - calls `setupFreshRecording()` then waits `A29_SEGMENT_SETTLE_MS` - dispatches `SAVE_ARCHIVE` via `sendMessageWithTimeout` (15s timeout, label 'SAVE_ARCHIVE (A29)') - pushes `A29.1: SAVE_ARCHIVE ack success===true` to checks - returns the `AssertionResult` (page-side computes ONLY the SAVE ack; the EventType-enum-shape checks live host-side in driveA29 because @rrweb/types isn't bundled to the page). - Adds `assertA29: () => Promise;` to the `declare global { interface Window { __mokoshHarness: { ... } } }` block (preserve existing entries; append after assertA28). - Adds `assertA29,` to the `window.__mokoshHarness = { ... }` object literal (preserve existing entries; append after assertA28). - Update the closing console.log message line (currently at line 3411) to mention `+ Plan 03-01: A29`. Host-side (`tests/uat/lib/harness-page-driver.ts`): - Adds `import { EventType } from '@rrweb/types';` at the top of the imports section (after the JSZip import on line 41). - Adds `export async function driveA29(page: Page, downloadsDir: string): Promise` modeled on driveA26 (lines 1421-1567): - Phase 1 — page.evaluate `harness.assertA29()` → `pageResult` - Phase 2 — `findLatestZip(downloadsDir)`; if null push `A29.0` failure and return - Phase 3 — JSZip.loadAsync(buf); read `rrweb/session.json` entry; if absent push failed `A29.0a: rrweb/session.json present` check + return - Parse the JSON as `Array<{ type: number; timestamp: number }>`. - Push 4 checks (names verbatim): - `A29.1: rrweb/session.json contains > 0 events` — expected `>0`, actual `events.length`, passed `events.length > 0` - `A29.2: rrweb emitted at least one Meta event (EventType.Meta=4)` — expected `has Meta`, actual `events.some((e) => e.type === EventType.Meta)`, passed same - `A29.3: rrweb emitted at least one FullSnapshot (EventType.FullSnapshot=2)` — expected `has FullSnapshot`, actual `events.some((e) => e.type === EventType.FullSnapshot)`, passed same - `A29.4: rrweb emitted at least one IncrementalSnapshot (EventType.IncrementalSnapshot=3)` — expected `has IncrementalSnapshot`, actual `events.some((e) => e.type === EventType.IncrementalSnapshot)`, passed same - Merge with `pageResult.checks` (carries A29.1 SAVE ack already); recompute `mergedPassed = mergedChecks.every((c) => c.passed)`. - Push diagnostic `A29 zipPath=${zipPath}`, `A29 events.length=${events.length}`, and `A29 event types: ${[...new Set(events.map((e) => e.type))].sort().join(',')}`. Orchestrator (`tests/uat/harness.test.ts`): - Adds `driveA29,` to the import block from `./lib/harness-page-driver` (after `driveA28,`). - Adds `const driveA29Wrapped: (page: import('puppeteer').Page) => Promise = (page) => driveA29(page, handles.downloadsDir);` after the existing `driveA28Wrapped` const (mirrors A26/A28 host-side-needs-downloadsDir pattern). - Adds `{ name: 'A29', drive: driveA29Wrapped },` to the `drivers` array after the A28 entry, with a banner comment block citing Plan 03-01 + SPEC §10 #4 + the chaining strategy (A29 owns its SAVE; injects DOM mutation pre-SAVE per RESEARCH Pitfall 1). - FORBIDDEN_HOOK_STRINGS array stays at 12 entries (no new `__MOKOSH_UAT__`-gated symbols). 1. Open `tests/uat/extension-page-harness.ts`. Locate the `assertA28` stub (currently around lines 3294-3316) and the `getManifestVersion` declaration block following it. 2. Insert the new `assertA29` block AFTER `assertA28` (and AFTER its end-of-file comments / before `getManifestVersion`). Use this concrete code: ```typescript /* ─── 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.0/A29.1..A29.4 EventType checks. */ async function assertA29(): Promise { 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('#probe-text'); if (textInput !== null) { textInput.value = 'probe'; textInput.dispatchEvent(new Event('input', { bubbles: true })); } const modalTrigger = document.querySelector('#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; } ``` 3. In the SAME file, locate the `declare global { interface Window { __mokoshHarness: { ... } } }` block (around lines 3332-3372). Inside the type literal, AFTER the existing `assertA28: () => Promise;` entry and BEFORE `getManifestVersion`, insert: ```typescript // Plan 03-01 — rrweb DOM verification (SPEC §10 #4) assertA29: () => Promise; ``` 4. Locate the `window.__mokoshHarness = { ... }` object literal (around lines 3374-3404). After `assertA28,` and before `getManifestVersion,` insert: ```typescript assertA29, ``` 5. Locate the trailing `console.log('[harness-page] ready — ...')` line (around line 3411) and replace the message to append `+ Plan 03-01: A29`. Concrete new line: ```typescript 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 + getManifestVersion)'); ``` 6. Open `tests/uat/lib/harness-page-driver.ts`. In the import block (after the `import JSZip from 'jszip';` line — around line 41), add: ```typescript import { EventType } from '@rrweb/types'; ``` 7. At the end of the file (after the existing `driveA28` export), append the new `driveA29` export: ```typescript /* ─── Plan 03-01 — driveA29 (rrweb DOM verification host-side) ─────── */ /** * Drive A29 (Plan 03-01 — SPEC §10 #4 / REQ-rrweb-dom-buffer). * * Three-phase orchestration: * 1. Page-side assertA29 dispatches the probe DOM mutations, settles, * runs setupFreshRecording, settles a segment, dispatches SAVE. * Returns AssertionRecord with A29.1 (SAVE ack) only. * 2. Host-side findLatestZip picks the just-produced zip (mtime-sort * wins; race-free per Plan 02-04 precedent — assertA29 awaits the * SAVE ack before returning so the zip has landed by here). * 3. Host-side JSZip-parse rrweb/session.json + EventType-enum grep. * * Checks appended host-side: * - A29.0a: rrweb/session.json entry exists in zip * - A29.1 (already from page side): SAVE_ARCHIVE ack success * - A29.2: events.length > 0 * - A29.3: events.some(e => e.type === EventType.Meta) * - A29.4: events.some(e => e.type === EventType.FullSnapshot) * - A29.5: events.some(e => e.type === EventType.IncrementalSnapshot) * * RESEARCH Pitfall 1 mitigation: the page-side dispatches a real DOM * mutation (input value + modal toggle) BEFORE SAVE so the * IncrementalSnapshot check (A29.5) has actual content to find. * * @param page - The harness page from `launchHarnessBrowser`. * @param downloadsDir - Absolute path to the per-run downloads directory. * @returns AssertionRecord with 6 merged checks (A29.0a + A29.1..A29.5). */ export async function driveA29( page: Page, downloadsDir: string, ): Promise { // Phase 1 — page-side orchestration + SAVE. 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.assertA29(); return r; }) as AssertionRecord; const mergedChecks: CheckRecord[] = pageResult.checks.slice(); const mergedDiagnostics: string[] = pageResult.diagnostics.slice(); // Phase 2 — locate the produced zip. const zipPath = findLatestZip(downloadsDir); if (zipPath === null) { mergedChecks.push({ name: 'A29.0: at least one zip present in downloadsDir', 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(`A29 zipPath=${zipPath}`); // Phase 3 — load + inspect rrweb/session.json. const zipBytes = readFileSync(zipPath); const zip = await JSZip.loadAsync(zipBytes); const rrwebFile = zip.file('rrweb/session.json'); mergedChecks.push({ name: 'A29.0a: rrweb/session.json entry exists in zip', expected: true, actual: rrwebFile !== null, passed: rrwebFile !== null, }); if (rrwebFile === null) { return { passed: false, name: pageResult.name, checks: mergedChecks, diagnostics: mergedDiagnostics, error: pageResult.error, }; } const rrwebText = await rrwebFile.async('string'); let events: Array<{ type: number; timestamp: number }> = []; let parseErr: string | null = null; try { events = JSON.parse(rrwebText) as Array<{ type: number; timestamp: number }>; } catch (err) { parseErr = err instanceof Error ? err.message : String(err); } if (parseErr !== null) { mergedChecks.push({ name: 'A29.0b: rrweb/session.json parses as JSON', expected: 'JSON.parse success', actual: ``, passed: false, }); return { passed: false, name: pageResult.name, checks: mergedChecks, diagnostics: mergedDiagnostics, error: pageResult.error, }; } const distinctTypes = [...new Set(events.map((e) => e.type))].sort((a, b) => a - b); mergedDiagnostics.push(`A29 events.length=${events.length}`); mergedDiagnostics.push(`A29 event types: ${distinctTypes.join(',')}`); mergedChecks.push({ name: 'A29.2: rrweb/session.json contains > 0 events', expected: '>0', actual: events.length, passed: events.length > 0, }); mergedChecks.push({ name: `A29.3: 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.4: 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.5: 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), }); const mergedPassed = mergedChecks.every((c) => c.passed); return { passed: mergedPassed, name: pageResult.name, checks: mergedChecks, diagnostics: mergedDiagnostics, error: pageResult.error, }; } ``` 8. Open `tests/uat/harness.test.ts`. In the import block from `./lib/harness-page-driver` (around lines 65-101), AFTER `driveA28,` and BEFORE `getManifestVersion,`, add: ```typescript // Plan 03-01 — rrweb DOM verification (SPEC §10 #4 / REQ-rrweb-dom-buffer) driveA29, ``` 9. In the same file, locate the existing `driveA28Wrapped` const (around lines 330-335). AFTER it, add: ```typescript // Plan 03-01 — driveA29 needs downloadsDir for host-side JSZip parse // of rrweb/session.json from the just-produced zip. const driveA29Wrapped: (page: import('puppeteer').Page) => Promise = (page) => driveA29(page, handles.downloadsDir); ``` 10. In the `drivers` array (around lines 337-431), AFTER the existing `{ name: 'A28', drive: driveA28Wrapped },` entry, add: ```typescript // Plan 03-01 A29: rrweb DOM verification (SPEC §10 #4). // A29 owns its SAVE because the probe-page DOM mutation must // happen between page load and SAVE so rrweb's IncrementalSnapshot // fires (RESEARCH Pitfall 1). Host-side driveA29 JSZip-parses // rrweb/session.json and asserts the EventType enum surfaces // (Meta=4, FullSnapshot=2, IncrementalSnapshot=3) are present. { name: 'A29', drive: driveA29Wrapped }, ``` 11. Update the orchestrator banner string (currently at line 268: `'Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28)'`) to append `, A29`. Concrete replacement string for line 268: ```typescript process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29)\n'); ``` 12. Run `npx tsc --noEmit` to confirm no type errors. Expected: clean. 13. Run `SKIP_PROD_REBUILD=0 HEADLESS=1 npm run test:uat` (full rebuild + harness). Expected: `30/30 GREEN` (29 prior + A29). npx tsc --noEmit && grep -c "assertA29" tests/uat/extension-page-harness.ts | grep -v '^#' | awk '$1 >= 3' && grep -c "driveA29" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | awk '$1 >= 2' && grep -c "driveA29" tests/uat/harness.test.ts | grep -v '^#' | awk '$1 >= 3' && grep -c "from '@rrweb/types'" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | grep -q '^1$' && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat - `npx tsc --noEmit` exits 0. - `grep -c 'assertA29' tests/uat/extension-page-harness.ts` returns >=3 (function definition + declare global entry + object literal entry). - `grep -c 'driveA29' tests/uat/lib/harness-page-driver.ts` returns >=2 (function definition + docstring or signature mention). - `grep -c 'driveA29' tests/uat/harness.test.ts` returns >=3 (import line + wrapped const + drivers array push). - `grep -c "from '@rrweb/types'" tests/uat/lib/harness-page-driver.ts` returns exactly 1. - `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with stdout containing `UAT harness: 30/30 assertions passed`. - `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (Tier-1 inventory stays at 12 entries; verifies no new hook leaks). UAT harness runs 30/30 GREEN with A29 covering SPEC §10 #4 end-to-end: rrweb session.json from the assembled zip contains > 0 events with Meta + FullSnapshot + IncrementalSnapshot EventType-enum members all present. FORBIDDEN_HOOK_STRINGS unchanged at 12. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Puppeteer host ↔ page realm | Test harness drives the page via page.evaluate; no production data flows out | | Page realm ↔ content script | Existing GET_RRWEB_EVENTS round-trip via chrome.runtime.sendMessage | | Content script ↔ SW | Existing chrome.tabs.sendMessage path (read-only for Plan 03-01) | | dist-test/ ↔ dist/ | Two-bundle separation: Plan 03-01 adds NO test-only symbols; production bundle invariant unchanged | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-03-01-01 | Information Disclosure | Probe HTML password input visible in DOM | accept | Sentinel value is not introduced by Plan 03-01 (Plan 03-03 types the negative-assertion sentinel). Plan 03-01 leaves the password input empty; rrweb's existing maskInputOptions.password=true at src/content/index.ts:306 masks any value that lands. | | T-03-01-02 | Tampering | New probe HTML elements interfere with A18/A21 token-resolution checks | mitigate | UI-SPEC ban enforced: probe HTML appends BELOW existing scaffold; head + tokens.css link untouched; probe sub-tree uses no var(--mks-*) tokens; verification grep at task acceptance proves the existing link line is preserved. | | T-03-01-03 | Information Disclosure | Test-only hook surface leaking to production bundle | mitigate | A29 rides production rrweb wiring + existing GET_RRWEB_EVENTS bridge + sendMessageWithTimeout helper. FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries; Tier-1 unit-test gate runs as part of Task 2 acceptance. | | T-03-01-04 | Denial of Service | rrweb cleanupOldEvents fires mid-A29 + drops the IncrementalSnapshot before SAVE | accept | A29 settles 500ms after mutation + 11s segment-settle + SAVE. CLEANUP_INTERVAL_MS=60s + retention=10min at src/content/index.ts:16-18 — A29's wall-clock <12s budget is far below the cleanup window; mutation event safely retained. | No new production surface; threat surface unchanged from Phase 2 (CON-manifest-permissions intact including DEC-011 Amendment 1 tabs permission). UAT harness extension is test-only and tree-shakes from production bundle via the existing `__MOKOSH_UAT__` Vite-define-token — A29 introduces no new `__MOKOSH_UAT__`-gated symbols. - `npx tsc --noEmit` exits 0. - `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 30/30 GREEN. - `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 FORBIDDEN_HOOK_STRINGS × 0 hits each). - `grep -c '' tests/uat/extension-page-harness.html` returns 1. - A29 GREEN in UAT harness across 6 host-side checks (A29.0a + A29.1..A29.5). - Probe HTML committed to harness page in correct location; head + script tag preserved. - FORBIDDEN_HOOK_STRINGS unchanged at 12 entries. - vitest baseline preserved (171/171 GREEN; SKIP_BUILD=1 path). - REQ-rrweb-dom-buffer empirically verified end-to-end through rrweb's `record()` wiring + GET_RRWEB_EVENTS bridge + the assembled zip's rrweb/session.json content. After completion, create `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md` documenting: - 4-check A29 contract verified (Meta + FullSnapshot + IncrementalSnapshot + count > 0) - Probe HTML composition rationale (form + table + modal; NO textarea) - DOM-mutation injection pattern (input value + modal click) - RESEARCH Pitfall 1 mitigation confirmed empirically - UAT 29 → 30 GREEN; Tier-1 inventory unchanged at 12 - Plan 03-02 wave dependency: SAME 3 harness files; SEQUENTIAL within Wave 2 per RESEARCH Pitfall 6
` scaffold (head unchanged; tokens.css link preserved for A18/A21).
` (line 21) and BEFORE the existing `` (line 22). 3. Insert the following probe HTML block BETWEEN line 21 and line 22 (preserving the trailing newline structure): ```html
Probe modal content (Plan 03-01 A29 IncrementalSnapshot trigger).