From c02914df867c8efb6ec2bc0c0b3753f4db5b7726 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 20 May 2026 19:11:41 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(03-01):=20Task=201=20=E2=80=94=20probe?= =?UTF-8?q?=20HTML=20for=20A29=20rrweb=20DOM=20verification=20(SPEC=20?= =?UTF-8?q?=C2=A710=20#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Append form (text + email + password + submit) + table (thead + 2 rows) + modal trigger + hidden modal div below existing `
`
  scaffold; preserves `` block + tokens.css link untouched (A18/A21
  invariant).
- Modal trigger uses inline onclick to toggle style.display — rrweb
  records the attribute mutation, satisfying IncrementalSnapshot
  emission per RESEARCH Pitfall 1 (synthetic probe HTML emits Meta +
  FullSnapshot but NOT IncrementalSnapshot without a DOM mutation
  between page load and SAVE).
- Per RESEARCH Pitfall 4: the rrweb-alpha.4-leaky multi-line input
  element (rrweb-io/rrweb#1596) is excluded; only single-line inputs.
- Per UI-SPEC §"Test Fixture Conventions": data-test-* attributes
  only; no data-mokosh-* (production-welcome-page reserved); no
  tokens.css import on the probe sub-tree (head already imports the
  canonical tokens for A18/A21).
- npm run build exit 0; all 7 acceptance grep gates GREEN.
---
 tests/uat/extension-page-harness.html | 36 +++++++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/tests/uat/extension-page-harness.html b/tests/uat/extension-page-harness.html
index 84a5134..94888be 100644
--- a/tests/uat/extension-page-harness.html
+++ b/tests/uat/extension-page-harness.html
@@ -19,6 +19,42 @@
     

This page lives at chrome-extension://<id>/tests/uat/extension-page-harness.html.

Puppeteer navigates a tab here and drives assertions via window.__mokoshHarness.*.

Ready.
+ +
+ + + + +
+ + + + + + + + +
col-acol-b
row-1-arow-1-b
row-2-arow-2-b
+ + From cc13f319a10b10948d8e5628b4ac6f9f4a6cac19 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 20 May 2026 19:17:47 +0200 Subject: [PATCH 2/3] =?UTF-8?q?feat(03-01):=20Task=202=20=E2=80=94=20asser?= =?UTF-8?q?tA29=20+=20driveA29=20+=20orchestrator=20wiring=20(A29=2030/30?= =?UTF-8?q?=20GREEN)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Page-side (tests/uat/extension-page-harness.ts): - assertA29 dispatches probe-page DOM mutation (input value + modal toggle), settles 500ms for rrweb IncrementalSnapshot to enqueue, setupFreshRecording, 11s segment-settle, SAVE_ARCHIVE; pushes A29.1 SAVE ack check. Module-local constants: A29_SAVE_ARCHIVE_TIMEOUT_MS=15s, A29_SEGMENT_SETTLE_MS=11s, A29_MUTATION_SETTLE_MS=500ms. - declare global interface + window.__mokoshHarness object literal extended with assertA29 (single-method-per-assertion contract). - statusEl + console banner updated A28 → A29 + cite Plan 03-01. Host-side (tests/uat/lib/harness-page-driver.ts): - Add `import { EventType } from '@rrweb/types';`. - driveA29 — 3-phase orchestration mirroring driveA26: Phase 1 page.evaluate harness.assertA29(); Phase 2 findLatestZip; Phase 3 JSZip.loadAsync rrweb/session.json + EventType grep. Appends A29.0a (rrweb/session.json present) + A29.2..A29.5 (events.length>0 + Meta + FullSnapshot + IncrementalSnapshot). Orchestrator (tests/uat/harness.test.ts): - driveA29 imported after driveA28. - driveA29Wrapped const captures handles.downloadsDir. - drivers array push A29 entry with banner citing Plan 03-01 + Pitfall 1. - Architecture banner string updated A28 → A29. Empirical verification (HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat): - UAT harness: 30/30 GREEN (29 prior + A29 NEW). - A29 events.length=4; event types observed: 2, 3, 4 (FullSnapshot, IncrementalSnapshot, Meta — all three required types present). - Pitfall 1 mitigation empirically verified — the pre-SAVE DOM mutation produced the IncrementalSnapshot. - vitest 171/171 GREEN preserved (full suite). - Tier-1 FORBIDDEN_HOOK_STRINGS unit gate 13/13 GREEN (12 strings × 0 hits each) — A29 rides production rrweb wiring + GET_RRWEB_EVENTS bridge + sendMessageWithTimeout helper; NO new __MOKOSH_UAT__ symbols. - npx tsc --noEmit exit 0. --- tests/uat/extension-page-harness.ts | 110 ++++++++++++++++++- tests/uat/harness.test.ts | 15 ++- tests/uat/lib/harness-page-driver.ts | 151 +++++++++++++++++++++++++++ 3 files changed, 273 insertions(+), 3 deletions(-) diff --git a/tests/uat/extension-page-harness.ts b/tests/uat/extension-page-harness.ts index c9feef6..f56b8fa 100644 --- a/tests/uat/extension-page-harness.ts +++ b/tests/uat/extension-page-harness.ts @@ -3315,6 +3315,109 @@ async function assertA28(): Promise { }; } +/* ─── 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 { + 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; +} + /** * Read `chrome.runtime.getManifest().version`. Used by the host-side * orchestrator at startup to capture the expected version for A13's @@ -3366,6 +3469,8 @@ declare global { assertA26: () => Promise; assertA27: () => Promise; assertA28: () => Promise; + // Plan 03-01 — rrweb DOM verification (SPEC §10 #4) + assertA29: () => Promise; getManifestVersion: () => Promise; }; } @@ -3400,14 +3505,15 @@ window.__mokoshHarness = { assertA26, assertA27, assertA28, + assertA29, getManifestVersion, }; const statusEl = document.getElementById('status'); if (statusEl !== null) { - statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A28, getManifestVersion} available.'; + statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A29, 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 + getManifestVersion)'); +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)'); export {}; diff --git a/tests/uat/harness.test.ts b/tests/uat/harness.test.ts index eb3d315..f9ec2e6 100644 --- a/tests/uat/harness.test.ts +++ b/tests/uat/harness.test.ts @@ -97,6 +97,8 @@ import { driveA26, driveA27, driveA28, + // Plan 03-01 — rrweb DOM verification (SPEC §10 #4 / REQ-rrweb-dom-buffer) + driveA29, getManifestVersion, } from './lib/harness-page-driver'; import { @@ -265,7 +267,7 @@ async function assertA0_GrepGate(): Promise<{ */ async function main(): Promise { process.stdout.write('\nMokosh Plan 01-13 + 01-14 + 02-04 — UAT harness orchestrator\n'); - process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28)\n'); + 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'); process.stdout.write('='.repeat(72) + '\n'); // A0 pre-flight (no Chrome launch needed; runs against built dist/). @@ -333,6 +335,10 @@ async function main(): Promise { (page) => driveA27(page, handles.downloadsDir); const driveA28Wrapped: (page: import('puppeteer').Page) => Promise = (page) => driveA28(page, handles.downloadsDir); + // 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); const drivers: ReadonlyArray<{ readonly name: string; @@ -428,6 +434,13 @@ async function main(): Promise { // and asserts EXACTLY 5 paths: video/last_30sec.webm, rrweb/session.json, // logs/events.json, screenshot.png, meta.json (set-equality; no extras). { name: 'A28', drive: driveA28Wrapped }, + // 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 }, ]; const buffers = { swConsole: handles.swConsole, offConsole: handles.offConsole }; diff --git a/tests/uat/lib/harness-page-driver.ts b/tests/uat/lib/harness-page-driver.ts index b1c572f..8ca1afe 100644 --- a/tests/uat/lib/harness-page-driver.ts +++ b/tests/uat/lib/harness-page-driver.ts @@ -39,6 +39,7 @@ import { tmpdir } from 'node:os'; import { join, resolve as resolvePath } from 'node:path'; import JSZip from 'jszip'; +import { EventType } from '@rrweb/types'; import type { Page } from 'puppeteer'; import type { AssertionRecord, CheckRecord } from './assertions'; @@ -1847,3 +1848,153 @@ export async function driveA28( error: pageResult.error, }; } + +/* ─── 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.0: at least one zip present in downloadsDir + * - 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 a29MergedPassed = mergedChecks.every((c) => c.passed); + return { + passed: a29MergedPassed, + name: pageResult.name, + checks: mergedChecks, + diagnostics: mergedDiagnostics, + error: pageResult.error, + }; +} From dc57f5cfc03edc1a1416eb3726fcc1cb172b21d6 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 20 May 2026 19:20:39 +0200 Subject: [PATCH 3/3] =?UTF-8?q?docs(03-01):=20complete=20A29=20rrweb=20DOM?= =?UTF-8?q?=20verification=20plan=20=E2=80=94=20SUMMARY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 2/2 plan tasks completed (c02914d + cc13f31). - UAT harness 29 → 30 GREEN; vitest 171/171 preserved. - Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12. - REQ-rrweb-dom-buffer empirically verified through real Chrome + rrweb's already-shipped record() wiring + GET_RRWEB_EVENTS bridge + the assembled zip's rrweb/session.json content. - A29 events.length=4; event types {2, 3, 4} (Meta + FullSnapshot + IncrementalSnapshot — all 3 required surfaces empirically present). - Worktree mode: STATE.md / ROADMAP.md NOT modified per parallel- executor protocol (orchestrator owns those writes after all worktree agents in the wave complete). --- .../03-01-SUMMARY.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md diff --git a/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md new file mode 100644 index 0000000..1b9a44d --- /dev/null +++ b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md @@ -0,0 +1,227 @@ +--- +phase: 03-spec-10-smoke-verification-dom-event-log-verification +plan: 01 +subsystem: testing +tags: + - uat-harness + - a29 + - rrweb + - dom-verification + - spec-10-4 + - req-rrweb-dom-buffer + - approach-b + - probe-html + - eventtype-enum + - phase-3-wave-1 + +requires: + - phase: 01-stabilize-video-pipeline + provides: "Plan 01-13 UAT harness Approach B (extension-internal page + synthetic MediaStream; page-side assertA* + host-side driveA* + harness.test.ts orchestrator); FORBIDDEN_HOOK_STRINGS lockstep pattern; pre-checkpoint bundle gates" + - phase: 02-stabilize-export-pipeline + provides: "Plan 02-04 A24-A28 harness extension (closest analog); findLatestZip helper at tests/uat/lib/harness-page-driver.ts; JSZip host-side parse pattern; chained-assertion / mtime-sort pattern; rrweb wiring + GET_RRWEB_EVENTS bridge production-shipped (src/content/index.ts:284-318)" + +provides: + - 1 new UAT harness assertion (A29) empirically verifying REQ-rrweb-dom-buffer + SPEC §10 #4 end-to-end through a real Chrome instance against synthetic probe HTML (form + table + modal + DOM-mutation trigger) + - assertA29 page-side orchestrator (DOM mutation dispatch + setupFreshRecording + SAVE) at tests/uat/extension-page-harness.ts + - driveA29 host-side 3-phase driver (page.evaluate + findLatestZip + JSZip rrweb/session.json + EventType-enum grep) at tests/uat/lib/harness-page-driver.ts + - Probe HTML in tests/uat/extension-page-harness.html (form with text+email+password+submit; table with thead+2 rows; modal trigger button with hidden modal div) appended BELOW existing scaffold; head + tokens.css link preserved + - EventType import `import { EventType } from '@rrweb/types';` (already transitively present; first explicit use) + - Orchestrator extension: drivers array 28 → 29; total 30/30 with A0; banner mentions A29 + +affects: + - phase-03 plans 02/03/04/05 (will follow same Approach B template + can chain off A29 if needed) + - phase-04 future rrweb v2 upgrade (A29's EventType enum import surface needs migration validation if @rrweb/types relocates NodeType per RESEARCH §"State of the Art") + +tech-stack: + added: + - "@rrweb/types EventType enum (explicit import in tests/uat/lib/harness-page-driver.ts; transitively present via rrweb 2.0.0-alpha.4 since Phase 1)" + patterns: + - "Approach B harness extension (Plan 02-04 verbatim template): page-side assertXX + host-side driveXX 3-phase (page.evaluate → findLatestZip → JSZip parse + grep) — proven on driveA26 (meta.json) + driveA28 (zip-layout); reused verbatim for driveA29 (rrweb/session.json)" + - "RESEARCH Pitfall 1 mitigation pattern: synthetic probe HTML + pre-SAVE DOM mutation dispatch (input.value + dispatchEvent + modal click) ensures rrweb emits IncrementalSnapshot in addition to Meta + FullSnapshot — empirically verified A29.5 GREEN with events.length=4 + event types {2,3,4}" + - "Page-side orchestrator (NOT stub) — assertA29 dispatches DOM mutation + setupFreshRecording + SAVE because the mutation MUST land BEFORE the GET_RRWEB_EVENTS bridge pulls the buffer; chaining off A28's already-completed zip would miss the IncrementalSnapshot window per Pitfall 1" + +key-files: + modified: + - tests/uat/extension-page-harness.html (probe HTML: form#probe-form + table#probe-table + button#probe-modal-trigger + div#probe-modal — appended BELOW existing `
` scaffold; head + tokens.css link preserved; no