5 plans across 5 waves (Wave 2 sequential per RESEARCH Pitfall 6 file overlap): - 03-01 Wave 1: rrweb DOM verification harness extension (A29; REQ-rrweb-dom-buffer; §10 #4) - 03-02 Wave 2: event-log verification harness extension (A30; REQ-user-event-log; §10 #5) - 03-03 Wave 3: §10 #8 password-filter PARTIAL verification (A31; D-P3-02 charter) - 03-04 Wave 4: §10 #9 RAM ceiling best-effort + Page.metrics scaffolding (A32; D-P3-04) - 03-05 Wave 5: §10 sweep VERIFICATION.md + REQUIREMENTS/ROADMAP/STATE marker flips (REQ-install-clean + REQ-rrweb-dom-buffer + REQ-user-event-log) Each plan has: - frontmatter (wave + depends_on + files_modified + autonomous + requirements + tags + must_haves) - tasks with mandatory <read_first> + <acceptance_criteria> + concrete <action> - <threat_model> block per security gate - Validation map row(s) added to 03-VALIDATION.md (10 tasks total) Expected UAT growth: 29/29 → 33/33 GREEN (A29-A32 + 03-05 docs). Expected vitest baseline preserved: 171/171. Expected Tier-1 FORBIDDEN_HOOK_STRINGS: 12 (A29+ ride production surfaces only). ROADMAP.md Phase 3 entry replaces "Plans: TBD" with full 5-plan list. VALIDATION.md status: planner_filled (nyquist_compliant: true; wave_0_complete: true). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
38 KiB
phase, slug, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, user_setup, must_haves
| phase | slug | plan | type | wave | depends_on | files_modified | autonomous | requirements | tags | user_setup | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03 | spec-10-smoke-verification-dom-event-log-verification | 01 | execute | 1 |
|
true |
|
|
|
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 <pre id="status"> scaffold (head unchanged; tokens.css link
preserved for A18/A21).
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.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.htmlFrom @rrweb/types/dist/index.d.ts (lines 186-194):
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):
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<CheckRecord>;
readonly diagnostics: ReadonlyArray<string>;
readonly error?: string;
}
From tests/uat/extension-page-harness.ts (line 169):
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<T>(msg: unknown, timeoutMs: number, label: string): Promise<T>; // line 234
From tests/uat/lib/harness-page-driver.ts (lines 1395, 41):
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):
// 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):
// 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.mdandtests/uat/lib/harness-page-driver.ts:1421-1567for the exact 3-phase shape (page-side stub → findLatestZip → JSZip-parse). - RESEARCH Pitfall 1 (MUST OBSERVE): synthetic probe HTML produces
Meta+FullSnapshoton load but NOTIncrementalSnapshotunless 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
<textarea>— rrweb 2.0.0-alpha.4 issue #1596 leaks textarea values even withmaskInputOptions.textareaset. - UI-SPEC ban (HARD): Probe HTML must NOT modify the
<head>block. The existing<link rel="stylesheet" href="../../src/shared/tokens.css">is load-bearing for A18 + A21. Probe HTML appends below the existing<pre id="status">(line 21) and BEFORE the existing<script>tag. - FORBIDDEN_HOOK_STRINGS lockstep (RESEARCH §"Anti-Patterns"): A29
rides production surfaces (rrweb.record + GET_RRWEB_EVENTS bridge +
chrome.runtime.sendMessage). Tier-1 inventory stays at 12 entries.
No new
__MOKOSH_UAT__-gated symbols expected. - Source audit citation: Implements REQ-rrweb-dom-buffer per
REQUIREMENTS.md lines 54-60 (10-min window + 5000-event cap + DOM
capture via rrweb 2.0.0-alpha.4 pin). The 10-min cleanup mechanics
are already covered by
src/content/index.ts:27-50and are NOT exercised by A29 — A29's contract is "rrweb records events without errors on probe pages", which closes §10 #4 literally per CONTEXT D-P3-01.
`, two `
` lines, and `
` are
NOT modified. The existing `<script type="module">` is NOT moved.
- The probe HTML appears between line 21 (``) and
line 22 (`<script type="module">`) in the source — appended to
the body after the existing scaffold, before the script.
- No `` element anywhere in the file (rrweb alpha.4
issue #1596).
- No `<link>` or `<style>` additions referencing tokens.css.
- No `data-mokosh-slot`, `data-mokosh-key`, or `data-mokosh-i18n-key`
attributes (reserved for production welcome page).
- The probe HTML uses inline minimal styles (`style="display:none"`
on the modal); no `<style>` block.
- On the modal trigger click, `document.getElementById('probe-modal')`
toggles its `style.display` between 'none' and 'block' via a
small inline `onclick` handler. (rrweb records the attribute
mutation, satisfying IncrementalSnapshot emission.)
</behavior>
<action>
1. Read the current `tests/uat/extension-page-harness.html` (24 lines, contents shown in interfaces).
2. Open the file for editing. Locate the line ` <pre id="status">Ready.</pre>` (line 21) and the next line ` <script type="module" src="./extension-page-harness.ts"></script>` (line 22).
3. Insert the following probe HTML block BETWEEN line 21 and line 22 (preserving the trailing newline structure):
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-html display"> <span class="c"><!--
</span><span class="c"> Plan 03-01 Wave 1 — probe HTML for A29 rrweb DOM verification
</span><span class="c"> (SPEC §10 #4). Provides form + table + modal-trigger so rrweb's
</span><span class="c"> record() emits Meta + FullSnapshot + IncrementalSnapshot events
</span><span class="c"> against this page. Per RESEARCH Pitfall 4: NO <textarea> (rrweb
</span><span class="c"> 2.0.0-alpha.4 issue #1596 leaks textarea values even with
</span><span class="c"> maskInputOptions.textarea set). Per UI-SPEC Section "Test Fixture
</span><span class="c"> Conventions": probe HTML uses plain data-test-* attributes; no
</span><span class="c"> data-mokosh-* (production-welcome-page reserved); no tokens.css
</span><span class="c"> import (the harness head already imports the canonical tokens
</span><span class="c"> for A18/A21 — probe sub-tree should appear unstyled to keep
</span><span class="c"> rrweb snapshots focused on structural DOM).
</span><span class="c"> --></span>
<span class="p"><</span><span class="nt">form</span> <span class="na">id</span><span class="o">=</span><span class="s">"probe-form"</span> <span class="na">data-test</span><span class="o">=</span><span class="s">"probe-form"</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"text"</span> <span class="na">id</span><span class="o">=</span><span class="s">"probe-text"</span> <span class="na">data-test</span><span class="o">=</span><span class="s">"probe-text"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"email"</span> <span class="na">id</span><span class="o">=</span><span class="s">"probe-email"</span> <span class="na">data-test</span><span class="o">=</span><span class="s">"probe-email"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"password"</span> <span class="na">id</span><span class="o">=</span><span class="s">"probe-password"</span> <span class="na">data-test</span><span class="o">=</span><span class="s">"probe-password"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="o">=</span><span class="s">"submit"</span> <span class="na">id</span><span class="o">=</span><span class="s">"probe-submit"</span> <span class="na">data-test</span><span class="o">=</span><span class="s">"probe-submit"</span><span class="p">></span>Submit<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"><</span><span class="nt">table</span> <span class="na">id</span><span class="o">=</span><span class="s">"probe-table"</span> <span class="na">data-test</span><span class="o">=</span><span class="s">"probe-table"</span><span class="p">></span>
<span class="p"><</span><span class="nt">thead</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>col-a<span class="p"></</span><span class="nt">th</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>col-b<span class="p"></</span><span class="nt">th</span><span class="p">></</span><span class="nt">tr</span><span class="p">></span>
<span class="p"></</span><span class="nt">thead</span><span class="p">></span>
<span class="p"><</span><span class="nt">tbody</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>row-1-a<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>row-1-b<span class="p"></</span><span class="nt">td</span><span class="p">></</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>row-2-a<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>row-2-b<span class="p"></</span><span class="nt">td</span><span class="p">></</span><span class="nt">tr</span><span class="p">></span>
<span class="p"></</span><span class="nt">tbody</span><span class="p">></span>
<span class="p"></</span><span class="nt">table</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">id</span><span class="o">=</span><span class="s">"probe-modal-trigger"</span> <span class="na">data-test</span><span class="o">=</span><span class="s">"probe-modal-trigger"</span>
<span class="na">onclick</span><span class="o">=</span><span class="s">"(function(){var m=document.getElementById('probe-modal');if(m){m.style.display=(m.style.display==='block'?'none':'block');}})()"</span><span class="p">></span>
Toggle modal
<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"probe-modal"</span> <span class="na">data-test</span><span class="o">=</span><span class="s">"probe-modal"</span> <span class="na">style</span><span class="o">=</span><span class="s">"display:none;"</span><span class="p">></span>
<span class="p"><</span><span class="nt">p</span><span class="p">></span>Probe modal content (Plan 03-01 A29 IncrementalSnapshot trigger).<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</code></pre></div><ol start="4" dir="auto">
<li>Verify no <code><textarea></code> was introduced (grep should return 0 hits on the modified file).</li>
<li>Verify the <code><head></code> block is unchanged.</li>
<li>No new dependencies, no other file edits in this task.
</action>
<verify>
<automated>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 '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html</automated>
</verify>
<acceptance_criteria>
<ul dir="auto">
<li><code>grep -c 'id="probe-form"' tests/uat/extension-page-harness.html</code> returns exactly <code>1</code>.</li>
<li><code>grep -c 'id="probe-modal-trigger"' tests/uat/extension-page-harness.html</code> returns exactly <code>1</code>.</li>
<li><code>grep -c 'id="probe-table"' tests/uat/extension-page-harness.html</code> returns exactly <code>1</code>.</li>
<li><code>grep -c 'id="probe-password"' tests/uat/extension-page-harness.html</code> returns exactly <code>1</code> (carried into Plan 03-03).</li>
<li><code>grep -c 'textarea' tests/uat/extension-page-harness.html</code> returns <code>0</code>.</li>
<li><code>grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html</code> returns exactly <code>1</code> (head unchanged).</li>
<li><code>grep -c 'data-mokosh-slot\|data-mokosh-key\|data-mokosh-i18n-key' tests/uat/extension-page-harness.html</code> returns <code>0</code>.</li>
<li><code>npm run build</code> exits 0.
</acceptance_criteria>
<done>
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.
</done>
</task></li>
</ul>
</li>
</ol>
<task type="auto" tdd="false">
<name>Task 2: Add assertA29 page-side stub + driveA29 host-side + harness.test.ts orchestrator wiring</name>
<files>tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
<read_first>
- 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)
</read_first>
<behavior>
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<AssertionResult>` 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<AssertionResult>;` 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`.
<pre><code>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<AssertionRecord>` 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<AssertionRecord> = (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).
</code></pre>
</behavior>
<action>
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:
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display"><span class="cm">/* ─── Plan 03-01 Task 2 — A29 (rrweb DOM verification; SPEC §10 #4) ─
</span><span class="cm"> *
</span><span class="cm"> * A29 — REQ-rrweb-dom-buffer empirical: rrweb's record() (already
</span><span class="cm"> * wired at src/content/index.ts:285) emits Meta + FullSnapshot
</span><span class="cm"> * + at least one IncrementalSnapshot when the harness page
</span><span class="cm"> * contains the probe HTML (form + table + modal) AND the driver
</span><span class="cm"> * injects a DOM mutation before SAVE (RESEARCH Pitfall 1:
</span><span class="cm"> * static probe HTML emits Meta + FullSnapshot but not
</span><span class="cm"> * IncrementalSnapshot without mutation).
</span><span class="cm"> *
</span><span class="cm"> * Page side: dispatch the probe-page DOM mutations (input value +
</span><span class="cm"> * modal toggle), settle, setupFreshRecording, settle one segment,
</span><span class="cm"> * dispatch SAVE_ARCHIVE, push A29.1 ack check. The host-side driveA29
</span><span class="cm"> * does the EventType-enum-shape grep against rrweb/session.json from
</span><span class="cm"> * the assembled zip (matches A26's chained-assertion pattern; JSZip
</span><span class="cm"> * + @rrweb/types are host-only deps).
</span><span class="cm"> *
</span><span class="cm"> * FORBIDDEN_HOOK_STRINGS impact: NONE. A29 rides production rrweb
</span><span class="cm"> * wiring (record() at src/content/index.ts:285 + GET_RRWEB_EVENTS
</span><span class="cm"> * round-trip at src/background/index.ts → src/content/index.ts:318)
</span><span class="cm"> * + the existing setupFreshRecording / sendMessageWithTimeout
</span><span class="cm"> * helpers. Tier-1 inventory stays at 12 entries.
</span><span class="cm"> */</span>
<span class="cm">/** SAVE_ARCHIVE dispatch timeout for A29 — matches A24/A25/A27. */</span>
<span class="kr">const</span> <span class="nx">A29_SAVE_ARCHIVE_TIMEOUT_MS</span> <span class="o">=</span> <span class="mi">15</span><span class="nx">_000</span><span class="p">;</span>
<span class="cm">/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */</span>
<span class="kr">const</span> <span class="nx">A29_SEGMENT_SETTLE_MS</span> <span class="o">=</span> <span class="mi">11</span><span class="nx">_000</span><span class="p">;</span>
<span class="cm">/** Settle window between DOM mutation and SAVE so rrweb's
</span><span class="cm"> * IncrementalSnapshot lands in the in-memory buffer before
</span><span class="cm"> * GET_RRWEB_EVENTS fires. */</span>
<span class="kr">const</span> <span class="nx">A29_MUTATION_SETTLE_MS</span> <span class="o">=</span> <span class="mi">500</span><span class="p">;</span>
<span class="cm">/**
</span><span class="cm"> * A29 — rrweb DOM event recording empirical (SPEC §10 #4).
</span><span class="cm"> *
</span><span class="cm"> * Page-side dispatches the probe-page mutation (input.value + modal
</span><span class="cm"> * toggle), settles, runs setupFreshRecording + segment-settle, then
</span><span class="cm"> * dispatches SAVE_ARCHIVE. Only the SAVE ack lives in the
</span><span class="cm"> * page-side checks; the EventType-enum-shape grep is host-side
</span><span class="cm"> * because @rrweb/types is host-only.
</span><span class="cm"> *
</span><span class="cm"> * @returns AssertionResult with 1 page-side check (SAVE ack); host-side
</span><span class="cm"> * driveA29 appends A29.0/A29.1..A29.4 EventType checks.
</span><span class="cm"> */</span>
<span class="kr">async</span> <span class="kd">function</span> <span class="nx">assertA29</span><span class="p">()</span><span class="o">:</span> <span class="nx">Promise</span><span class="p"><</span><span class="nt">AssertionResult</span><span class="p">></span> <span class="p">{</span>
<span class="kr">const</span> <span class="nx">result</span>: <span class="kt">AssertionResult</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">passed</span>: <span class="kt">false</span><span class="p">,</span>
<span class="nx">name</span><span class="o">:</span> <span class="s1">'A29 — rrweb DOM events recorded without errors (SPEC §10 #4 / REQ-rrweb-dom-buffer)'</span><span class="p">,</span>
<span class="nx">checks</span><span class="o">:</span> <span class="p">[],</span>
<span class="nx">diagnostics</span><span class="o">:</span> <span class="p">[],</span>
<span class="p">};</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nx">diag</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="s1">'Step 1: dispatch probe-page DOM mutation (input value + modal toggle)'</span><span class="p">);</span>
<span class="kr">const</span> <span class="nx">textInput</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p"><</span><span class="nt">HTMLInputElement</span><span class="p">>(</span><span class="s1">'#probe-text'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">textInput</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">textInput</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="s1">'probe'</span><span class="p">;</span>
<span class="nx">textInput</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span><span class="k">new</span> <span class="nx">Event</span><span class="p">(</span><span class="s1">'input'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">bubbles</span>: <span class="kt">true</span> <span class="p">}));</span>
<span class="p">}</span>
<span class="kr">const</span> <span class="nx">modalTrigger</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p"><</span><span class="nt">HTMLButtonElement</span><span class="p">>(</span><span class="s1">'#probe-modal-trigger'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">modalTrigger</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">modalTrigger</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span>
<span class="p">}</span>
<span class="nx">diag</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="sb">`Step 1 OK — mutations dispatched (#probe-text=</span><span class="si">${</span><span class="nx">textInput</span> <span class="o">!==</span> <span class="kc">null</span><span class="si">}</span><span class="sb">, #probe-modal-trigger=</span><span class="si">${</span><span class="nx">modalTrigger</span> <span class="o">!==</span> <span class="kc">null</span><span class="si">}</span><span class="sb">)`</span><span class="p">);</span>
<span class="nx">diag</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="sb">`Step 2: settle </span><span class="si">${</span><span class="nx">A29_MUTATION_SETTLE_MS</span><span class="si">}</span><span class="sb">ms for rrweb IncrementalSnapshot to enqueue`</span><span class="p">);</span>
<span class="k">await</span> <span class="k">new</span> <span class="nx">Promise</span><span class="p">((</span><span class="nx">r</span><span class="p">)</span> <span class="o">=></span> <span class="nx">setTimeout</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span> <span class="nx">A29_MUTATION_SETTLE_MS</span><span class="p">));</span>
<span class="nx">diag</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="s1">'Step 3: setupFreshRecording'</span><span class="p">);</span>
<span class="kr">const</span> <span class="nx">setupResp</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">setupFreshRecording</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">setupResp</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="sb">`setupFreshRecording failed: </span><span class="si">${</span><span class="nx">setupResp</span><span class="p">.</span><span class="nx">error</span> <span class="o">??</span> <span class="s1">'(no error)'</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">diag</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="s1">'Step 3 OK — REC state established'</span><span class="p">);</span>
<span class="nx">diag</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="sb">`Step 4: settle </span><span class="si">${</span><span class="nx">A29_SEGMENT_SETTLE_MS</span><span class="si">}</span><span class="sb">ms for first segment rotation`</span><span class="p">);</span>
<span class="k">await</span> <span class="k">new</span> <span class="nx">Promise</span><span class="p">((</span><span class="nx">r</span><span class="p">)</span> <span class="o">=></span> <span class="nx">setTimeout</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span> <span class="nx">A29_SEGMENT_SETTLE_MS</span><span class="p">));</span>
<span class="nx">diag</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="s1">'Step 5: dispatch SAVE_ARCHIVE'</span><span class="p">);</span>
<span class="kr">const</span> <span class="nx">ack</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">sendMessageWithTimeout</span><span class="o"><</span><span class="p">{</span> <span class="nx">success</span>: <span class="kt">boolean</span><span class="p">;</span> <span class="nx">error?</span>: <span class="kt">string</span> <span class="p">}</span><span class="o">></span><span class="p">(</span>
<span class="p">{</span> <span class="kr">type</span><span class="o">:</span> <span class="s1">'SAVE_ARCHIVE'</span> <span class="p">},</span>
<span class="nx">A29_SAVE_ARCHIVE_TIMEOUT_MS</span><span class="p">,</span>
<span class="s1">'SAVE_ARCHIVE (A29)'</span><span class="p">,</span>
<span class="p">);</span>
<span class="nx">diag</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="sb">`Step 5 result: </span><span class="si">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">ack</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
<span class="nx">result</span><span class="p">.</span><span class="nx">checks</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="nx">name</span><span class="o">:</span> <span class="s1">'A29.1: SAVE_ARCHIVE ack received with success=true'</span><span class="p">,</span>
<span class="nx">expected</span>: <span class="kt">true</span><span class="p">,</span>
<span class="nx">actual</span>: <span class="kt">ack.success</span><span class="p">,</span>
<span class="nx">passed</span>: <span class="kt">ack.success</span> <span class="o">===</span> <span class="kc">true</span><span class="p">,</span>
<span class="p">});</span>
<span class="nx">result</span><span class="p">.</span><span class="nx">passed</span> <span class="o">=</span> <span class="nx">result</span><span class="p">.</span><span class="nx">checks</span><span class="p">.</span><span class="nx">every</span><span class="p">((</span><span class="nx">c</span><span class="p">)</span> <span class="o">=></span> <span class="nx">c</span><span class="p">.</span><span class="nx">passed</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">result</span><span class="p">.</span><span class="nx">error</span> <span class="o">=</span> <span class="nx">err</span> <span class="k">instanceof</span> <span class="nb">Error</span> <span class="o">?</span> <span class="nx">err.message</span> : <span class="kt">String</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="nx">diag</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="sb">`THREW: </span><span class="si">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">error</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><ol start="3" dir="auto">
<li>In the SAME file, locate the <code>declare global { interface Window { __mokoshHarness: { ... } } }</code> block (around lines 3332-3372). Inside the type literal, AFTER the existing <code>assertA28: () => Promise<AssertionResult>;</code> entry and BEFORE <code>getManifestVersion</code>, insert:</li>
</ol>
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display"> <span class="c1">// Plan 03-01 — rrweb DOM verification (SPEC §10 #4)
</span><span class="c1"></span> <span class="nx">assertA29</span><span class="o">:</span> <span class="p">()</span> <span class="o">=></span> <span class="nx">Promise</span><span class="p"><</span><span class="nt">AssertionResult</span><span class="p">>;</span>
</code></pre></div><ol start="4" dir="auto">
<li>Locate the <code>window.__mokoshHarness = { ... }</code> object literal (around lines 3374-3404). After <code>assertA28,</code> and before <code>getManifestVersion,</code> insert:</li>
</ol>
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display"> <span class="nx">assertA29</span><span class="p">,</span>
</code></pre></div><ol start="5" dir="auto">
<li>Locate the trailing <code>console.log('[harness-page] ready — ...')</code> line (around line 3411) and replace the message to append <code>+ Plan 03-01: A29</code>. Concrete new line:</li>
</ol>
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'[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)'</span><span class="p">);</span>
</code></pre></div><ol start="6" dir="auto">
<li>Open <code>tests/uat/lib/harness-page-driver.ts</code>. In the import block (after the <code>import JSZip from 'jszip';</code> line — around line 41), add:</li>
</ol>
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display"><span class="kr">import</span> <span class="p">{</span> <span class="nx">EventType</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">'@rrweb/types'</span><span class="p">;</span>
</code></pre></div><ol start="7" dir="auto">
<li>At the end of the file (after the existing <code>driveA28</code> export), append the new <code>driveA29</code> export:</li>
</ol>
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display">
<span class="cm">/* ─── Plan 03-01 — driveA29 (rrweb DOM verification host-side) ─────── */</span>
<span class="cm">/**
</span><span class="cm"> * Drive A29 (Plan 03-01 — SPEC §10 #4 / REQ-rrweb-dom-buffer).
</span><span class="cm"> *
</span><span class="cm"> * Three-phase orchestration:
</span><span class="cm"> * 1. Page-side assertA29 dispatches the probe DOM mutations, settles,
</span><span class="cm"> * runs setupFreshRecording, settles a segment, dispatches SAVE.
</span><span class="cm"> * Returns AssertionRecord with A29.1 (SAVE ack) only.
</span><span class="cm"> * 2. Host-side findLatestZip picks the just-produced zip (mtime-sort
</span><span class="cm"> * wins; race-free per Plan 02-04 precedent — assertA29 awaits the
</span><span class="cm"> * SAVE ack before returning so the zip has landed by here).
</span><span class="cm"> * 3. Host-side JSZip-parse rrweb/session.json + EventType-enum grep.
</span><span class="cm"> *
</span><span class="cm"> * Checks appended host-side:
</span><span class="cm"> * - A29.0a: rrweb/session.json entry exists in zip
</span><span class="cm"> * - A29.1 (already from page side): SAVE_ARCHIVE ack success
</span><span class="cm"> * - A29.2: events.length > 0
</span><span class="cm"> * - A29.3: events.some(e => e.type === EventType.Meta)
</span><span class="cm"> * - A29.4: events.some(e => e.type === EventType.FullSnapshot)
</span><span class="cm"> * - A29.5: events.some(e => e.type === EventType.IncrementalSnapshot)
</span><span class="cm"> *
</span><span class="cm"> * RESEARCH Pitfall 1 mitigation: the page-side dispatches a real DOM
</span><span class="cm"> * mutation (input value + modal toggle) BEFORE SAVE so the
</span><span class="cm"> * IncrementalSnapshot check (A29.5) has actual content to find.
</span><span class="cm"> *
</span><span class="cm"> * @param page - The harness page from `launchHarnessBrowser`.
</span><span class="cm"> * @param downloadsDir - Absolute path to the per-run downloads directory.
</span><span class="cm"> * @returns AssertionRecord with 6 merged checks (A29.0a + A29.1..A29.5).
</span><span class="cm"> */</span>
<span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">driveA29</span><span class="p">(</span>
<span class="nx">page</span>: <span class="kt">Page</span><span class="p">,</span>
<span class="nx">downloadsDir</span>: <span class="kt">string</span><span class="p">,</span>
<span class="p">)</span><span class="o">:</span> <span class="nx">Promise</span><span class="p"><</span><span class="nt">AssertionRecord</span><span class="p">></span> <span class="p">{</span>
<span class="c1">// Phase 1 — page-side orchestration + SAVE.
</span><span class="c1"></span> <span class="kr">const</span> <span class="nx">pageResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">evaluate</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
</span><span class="c1"></span> <span class="kr">const</span> <span class="nx">harness</span> <span class="o">=</span> <span class="p">(</span><span class="nb">window</span> <span class="kr">as</span> <span class="kt">any</span><span class="p">).</span><span class="nx">__mokoshHarness</span><span class="p">;</span>
<span class="kr">const</span> <span class="nx">r</span>: <span class="kt">AssertionRecord</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">harness</span><span class="p">.</span><span class="nx">assertA29</span><span class="p">();</span>
<span class="k">return</span> <span class="nx">r</span><span class="p">;</span>
<span class="p">})</span> <span class="kr">as</span> <span class="nx">AssertionRecord</span><span class="p">;</span>
<span class="kr">const</span> <span class="nx">mergedChecks</span>: <span class="kt">CheckRecord</span><span class="p">[]</span> <span class="o">=</span> <span class="nx">pageResult</span><span class="p">.</span><span class="nx">checks</span><span class="p">.</span><span class="nx">slice</span><span class="p">();</span>
<span class="kr">const</span> <span class="nx">mergedDiagnostics</span>: <span class="kt">string</span><span class="p">[]</span> <span class="o">=</span> <span class="nx">pageResult</span><span class="p">.</span><span class="nx">diagnostics</span><span class="p">.</span><span class="nx">slice</span><span class="p">();</span>
<span class="c1">// Phase 2 — locate the produced zip.
</span><span class="c1"></span> <span class="kr">const</span> <span class="nx">zipPath</span> <span class="o">=</span> <span class="nx">findLatestZip</span><span class="p">(</span><span class="nx">downloadsDir</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">zipPath</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">mergedChecks</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="nx">name</span><span class="o">:</span> <span class="s1">'A29.0: at least one zip present in downloadsDir'</span><span class="p">,</span>
<span class="nx">expected</span><span class="o">:</span> <span class="s1">'>=1 zip'</span><span class="p">,</span>
<span class="nx">actual</span><span class="o">:</span> <span class="s1">'no zip in downloadsDir'</span><span class="p">,</span>
<span class="nx">passed</span>: <span class="kt">false</span><span class="p">,</span>
<span class="p">});</span>
<span class="k">return</span> <span class="p">{</span>
<span class="nx">passed</span>: <span class="kt">false</span><span class="p">,</span>
<span class="nx">name</span>: <span class="kt">pageResult.name</span><span class="p">,</span>
<span class="nx">checks</span>: <span class="kt">mergedChecks</span><span class="p">,</span>
<span class="nx">diagnostics</span>: <span class="kt">mergedDiagnostics</span><span class="p">,</span>
<span class="nx">error</span>: <span class="kt">pageResult.error</span><span class="p">,</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="nx">mergedDiagnostics</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="sb">`A29 zipPath=</span><span class="si">${</span><span class="nx">zipPath</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
<span class="c1">// Phase 3 — load + inspect rrweb/session.json.
</span><span class="c1"></span> <span class="kr">const</span> <span class="nx">zipBytes</span> <span class="o">=</span> <span class="nx">readFileSync</span><span class="p">(</span><span class="nx">zipPath</span><span class="p">);</span>
<span class="kr">const</span> <span class="nx">zip</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">JSZip</span><span class="p">.</span><span class="nx">loadAsync</span><span class="p">(</span><span class="nx">zipBytes</span><span class="p">);</span>
<span class="kr">const</span> <span class="nx">rrwebFile</span> <span class="o">=</span> <span class="nx">zip</span><span class="p">.</span><span class="nx">file</span><span class="p">(</span><span class="s1">'rrweb/session.json'</span><span class="p">);</span>
<span class="nx">mergedChecks</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="nx">name</span><span class="o">:</span> <span class="s1">'A29.0a: rrweb/session.json entry exists in zip'</span><span class="p">,</span>
<span class="nx">expected</span>: <span class="kt">true</span><span class="p">,</span>
<span class="nx">actual</span>: <span class="kt">rrwebFile</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">,</span>
<span class="nx">passed</span>: <span class="kt">rrwebFile</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">,</span>
<span class="p">});</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">rrwebFile</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
<span class="nx">passed</span>: <span class="kt">false</span><span class="p">,</span>
<span class="nx">name</span>: <span class="kt">pageResult.name</span><span class="p">,</span>
<span class="nx">checks</span>: <span class="kt">mergedChecks</span><span class="p">,</span>
<span class="nx">diagnostics</span>: <span class="kt">mergedDiagnostics</span><span class="p">,</span>
<span class="nx">error</span>: <span class="kt">pageResult.error</span><span class="p">,</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="kr">const</span> <span class="nx">rrwebText</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">rrwebFile</span><span class="p">.</span><span class="kr">async</span><span class="p">(</span><span class="s1">'string'</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">events</span>: <span class="kt">Array</span><span class="o"><</span><span class="p">{</span> <span class="kr">type</span><span class="o">:</span> <span class="kt">number</span><span class="p">;</span> <span class="nx">timestamp</span>: <span class="kt">number</span> <span class="p">}</span><span class="o">></span> <span class="o">=</span> <span class="p">[];</span>
<span class="kd">let</span> <span class="nx">parseErr</span>: <span class="kt">string</span> <span class="o">|</span> <span class="kc">null</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nx">events</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">rrwebText</span><span class="p">)</span> <span class="kr">as</span> <span class="nb">Array</span><span class="o"><</span><span class="p">{</span> <span class="kr">type</span><span class="o">:</span> <span class="kt">number</span><span class="p">;</span> <span class="nx">timestamp</span>: <span class="kt">number</span> <span class="p">}</span><span class="o">></span><span class="p">;</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">parseErr</span> <span class="o">=</span> <span class="nx">err</span> <span class="k">instanceof</span> <span class="nb">Error</span> <span class="o">?</span> <span class="nx">err.message</span> : <span class="kt">String</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">parseErr</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">mergedChecks</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="nx">name</span><span class="o">:</span> <span class="s1">'A29.0b: rrweb/session.json parses as JSON'</span><span class="p">,</span>
<span class="nx">expected</span><span class="o">:</span> <span class="s1">'JSON.parse success'</span><span class="p">,</span>
<span class="nx">actual</span><span class="o">:</span> <span class="sb">`<error: </span><span class="si">${</span><span class="nx">parseErr</span><span class="si">}</span><span class="sb">>`</span><span class="p">,</span>
<span class="nx">passed</span>: <span class="kt">false</span><span class="p">,</span>
<span class="p">});</span>
<span class="k">return</span> <span class="p">{</span>
<span class="nx">passed</span>: <span class="kt">false</span><span class="p">,</span>
<span class="nx">name</span>: <span class="kt">pageResult.name</span><span class="p">,</span>
<span class="nx">checks</span>: <span class="kt">mergedChecks</span><span class="p">,</span>
<span class="nx">diagnostics</span>: <span class="kt">mergedDiagnostics</span><span class="p">,</span>
<span class="nx">error</span>: <span class="kt">pageResult.error</span><span class="p">,</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="kr">const</span> <span class="nx">distinctTypes</span> <span class="o">=</span> <span class="p">[...</span><span class="k">new</span> <span class="nx">Set</span><span class="p">(</span><span class="nx">events</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="kr">type</span><span class="p">))].</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="nx">a</span> <span class="o">-</span> <span class="nx">b</span><span class="p">);</span>
<span class="nx">mergedDiagnostics</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="sb">`A29 events.length=</span><span class="si">${</span><span class="nx">events</span><span class="p">.</span><span class="nx">length</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
<span class="nx">mergedDiagnostics</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="sb">`A29 event types: </span><span class="si">${</span><span class="nx">distinctTypes</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="s1">','</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
<span class="nx">mergedChecks</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="nx">name</span><span class="o">:</span> <span class="s1">'A29.2: rrweb/session.json contains > 0 events'</span><span class="p">,</span>
<span class="nx">expected</span><span class="o">:</span> <span class="s1">'>0'</span><span class="p">,</span>
<span class="nx">actual</span>: <span class="kt">events.length</span><span class="p">,</span>
<span class="nx">passed</span>: <span class="kt">events.length</span> <span class="o">></span> <span class="mi">0</span><span class="p">,</span>
<span class="p">});</span>
<span class="nx">mergedChecks</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="nx">name</span><span class="o">:</span> <span class="sb">`A29.3: rrweb emitted at least one Meta event (EventType.Meta=</span><span class="si">${</span><span class="nx">EventType</span><span class="p">.</span><span class="nx">Meta</span><span class="si">}</span><span class="sb">)`</span><span class="p">,</span>
<span class="nx">expected</span><span class="o">:</span> <span class="s1">'has Meta'</span><span class="p">,</span>
<span class="nx">actual</span>: <span class="kt">events.some</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="nx">EventType</span><span class="p">.</span><span class="nx">Meta</span><span class="p">),</span>
<span class="nx">passed</span>: <span class="kt">events.some</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="nx">EventType</span><span class="p">.</span><span class="nx">Meta</span><span class="p">),</span>
<span class="p">});</span>
<span class="nx">mergedChecks</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="nx">name</span><span class="o">:</span> <span class="sb">`A29.4: rrweb emitted at least one FullSnapshot (EventType.FullSnapshot=</span><span class="si">${</span><span class="nx">EventType</span><span class="p">.</span><span class="nx">FullSnapshot</span><span class="si">}</span><span class="sb">)`</span><span class="p">,</span>
<span class="nx">expected</span><span class="o">:</span> <span class="s1">'has FullSnapshot'</span><span class="p">,</span>
<span class="nx">actual</span>: <span class="kt">events.some</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="nx">EventType</span><span class="p">.</span><span class="nx">FullSnapshot</span><span class="p">),</span>
<span class="nx">passed</span>: <span class="kt">events.some</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="nx">EventType</span><span class="p">.</span><span class="nx">FullSnapshot</span><span class="p">),</span>
<span class="p">});</span>
<span class="nx">mergedChecks</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="nx">name</span><span class="o">:</span> <span class="sb">`A29.5: rrweb emitted at least one IncrementalSnapshot (EventType.IncrementalSnapshot=</span><span class="si">${</span><span class="nx">EventType</span><span class="p">.</span><span class="nx">IncrementalSnapshot</span><span class="si">}</span><span class="sb">)`</span><span class="p">,</span>
<span class="nx">expected</span><span class="o">:</span> <span class="s1">'has IncrementalSnapshot'</span><span class="p">,</span>
<span class="nx">actual</span>: <span class="kt">events.some</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="nx">EventType</span><span class="p">.</span><span class="nx">IncrementalSnapshot</span><span class="p">),</span>
<span class="nx">passed</span>: <span class="kt">events.some</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="nx">EventType</span><span class="p">.</span><span class="nx">IncrementalSnapshot</span><span class="p">),</span>
<span class="p">});</span>
<span class="kr">const</span> <span class="nx">mergedPassed</span> <span class="o">=</span> <span class="nx">mergedChecks</span><span class="p">.</span><span class="nx">every</span><span class="p">((</span><span class="nx">c</span><span class="p">)</span> <span class="o">=></span> <span class="nx">c</span><span class="p">.</span><span class="nx">passed</span><span class="p">);</span>
<span class="k">return</span> <span class="p">{</span>
<span class="nx">passed</span>: <span class="kt">mergedPassed</span><span class="p">,</span>
<span class="nx">name</span>: <span class="kt">pageResult.name</span><span class="p">,</span>
<span class="nx">checks</span>: <span class="kt">mergedChecks</span><span class="p">,</span>
<span class="nx">diagnostics</span>: <span class="kt">mergedDiagnostics</span><span class="p">,</span>
<span class="nx">error</span>: <span class="kt">pageResult.error</span><span class="p">,</span>
<span class="p">};</span>
<span class="p">}</span>
</code></pre></div><ol start="8" dir="auto">
<li>Open <code>tests/uat/harness.test.ts</code>. In the import block from <code>./lib/harness-page-driver</code> (around lines 65-101), AFTER <code>driveA28,</code> and BEFORE <code>getManifestVersion,</code>, add:</li>
</ol>
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display"> <span class="c1">// Plan 03-01 — rrweb DOM verification (SPEC §10 #4 / REQ-rrweb-dom-buffer)
</span><span class="c1"></span> <span class="nx">driveA29</span><span class="p">,</span>
</code></pre></div><ol start="9" dir="auto">
<li>In the same file, locate the existing <code>driveA28Wrapped</code> const (around lines 330-335). AFTER it, add:</li>
</ol>
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display"> <span class="c1">// Plan 03-01 — driveA29 needs downloadsDir for host-side JSZip parse
</span><span class="c1"></span> <span class="c1">// of rrweb/session.json from the just-produced zip.
</span><span class="c1"></span> <span class="kr">const</span> <span class="nx">driveA29Wrapped</span><span class="o">:</span> <span class="p">(</span><span class="nx">page</span>: <span class="kt">import</span><span class="p">(</span><span class="s1">'puppeteer'</span><span class="p">).</span><span class="nx">Page</span><span class="p">)</span> <span class="o">=></span> <span class="nx">Promise</span><span class="p"><</span><span class="nt">AssertionRecord</span><span class="p">></span> <span class="o">=</span>
<span class="p">(</span><span class="nx">page</span><span class="p">)</span> <span class="o">=></span> <span class="nx">driveA29</span><span class="p">(</span><span class="nx">page</span><span class="p">,</span> <span class="nx">handles</span><span class="p">.</span><span class="nx">downloadsDir</span><span class="p">);</span>
</code></pre></div><ol start="10" dir="auto">
<li>In the <code>drivers</code> array (around lines 337-431), AFTER the existing <code>{ name: 'A28', drive: driveA28Wrapped },</code> entry, add:</li>
</ol>
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display"> <span class="c1">// Plan 03-01 A29: rrweb DOM verification (SPEC §10 #4).
</span><span class="c1"></span> <span class="c1">// A29 owns its SAVE because the probe-page DOM mutation must
</span><span class="c1"></span> <span class="c1">// happen between page load and SAVE so rrweb's IncrementalSnapshot
</span><span class="c1"></span> <span class="c1">// fires (RESEARCH Pitfall 1). Host-side driveA29 JSZip-parses
</span><span class="c1"></span> <span class="c1">// rrweb/session.json and asserts the EventType enum surfaces
</span><span class="c1"></span> <span class="c1">// (Meta=4, FullSnapshot=2, IncrementalSnapshot=3) are present.
</span><span class="c1"></span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'A29'</span><span class="p">,</span> <span class="nx">drive</span>: <span class="kt">driveA29Wrapped</span> <span class="p">},</span>
</code></pre></div><ol start="11" dir="auto">
<li>Update the orchestrator banner string (currently at line 268: <code>'Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28)'</code>) to append <code>, A29</code>. Concrete replacement string for line 268:</li>
</ol>
<div data-attr-class="P7BsDCtu_WFzxtsQ:code-block-container code-overflow-scroll"><pre data-attr-class="P7BsDCtu_WFzxtsQ:code-block"><code data-attr-class="P7BsDCtu_WFzxtsQ:chroma language-typescript display"> <span class="nx">process</span><span class="p">.</span><span class="nx">stdout</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="s1">'Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29)\n'</span><span class="p">);</span>
</code></pre></div><ol start="12" dir="auto">
<li>Run <code>npx tsc --noEmit</code> to confirm no type errors. Expected: clean.</li>
<li>Run <code>SKIP_PROD_REBUILD=0 HEADLESS=1 npm run test:uat</code> (full rebuild + harness). Expected: <code>30/30 GREEN</code> (29 prior + A29).
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
<ul dir="auto">
<li><code>npx tsc --noEmit</code> exits 0.</li>
<li><code>grep -c 'assertA29' tests/uat/extension-page-harness.ts</code> returns >=3 (function definition + declare global entry + object literal entry).</li>
<li><code>grep -c 'driveA29' tests/uat/lib/harness-page-driver.ts</code> returns >=2 (function definition + docstring or signature mention).</li>
<li><code>grep -c 'driveA29' tests/uat/harness.test.ts</code> returns >=3 (import line + wrapped const + drivers array push).</li>
<li><code>grep -c "from '@rrweb/types'" tests/uat/lib/harness-page-driver.ts</code> returns exactly 1.</li>
<li><code>HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat</code> exits 0 with stdout containing <code>UAT harness: 30/30 assertions passed</code>.</li>
<li><code>npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts</code> exits 0 (Tier-1 inventory stays at 12 entries; verifies no new hook leaks).
</acceptance_criteria>
<done>
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.
</done>
</task></li>
</ul>
</li>
</ol>
</tasks>
<p dir="auto"><threat_model></p>
<h2 id="user-content-trust-boundaries" dir="auto">Trust Boundaries</h2>
<table>
<thead>
<tr>
<th>Boundary</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Puppeteer host ↔ page realm</td>
<td>Test harness drives the page via page.evaluate; no production data flows out</td>
</tr>
<tr>
<td>Page realm ↔ content script</td>
<td>Existing GET_RRWEB_EVENTS round-trip via chrome.runtime.sendMessage</td>
</tr>
<tr>
<td>Content script ↔ SW</td>
<td>Existing chrome.tabs.sendMessage path (read-only for Plan 03-01)</td>
</tr>
<tr>
<td>dist-test/ ↔ dist/</td>
<td>Two-bundle separation: Plan 03-01 adds NO test-only symbols; production bundle invariant unchanged</td>
</tr>
</tbody>
</table>
<h2 id="user-content-stride-threat-register" dir="auto">STRIDE Threat Register</h2>
<table>
<thead>
<tr>
<th>Threat ID</th>
<th>Category</th>
<th>Component</th>
<th>Disposition</th>
<th>Mitigation Plan</th>
</tr>
</thead>
<tbody>
<tr>
<td>T-03-01-01</td>
<td>Information Disclosure</td>
<td>Probe HTML password input visible in DOM</td>
<td>accept</td>
<td>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.</td>
</tr>
<tr>
<td>T-03-01-02</td>
<td>Tampering</td>
<td>New probe HTML elements interfere with A18/A21 token-resolution checks</td>
<td>mitigate</td>
<td>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.</td>
</tr>
<tr>
<td>T-03-01-03</td>
<td>Information Disclosure</td>
<td>Test-only hook surface leaking to production bundle</td>
<td>mitigate</td>
<td>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.</td>
</tr>
<tr>
<td>T-03-01-04</td>
<td>Denial of Service</td>
<td>rrweb cleanupOldEvents fires mid-A29 + drops the IncrementalSnapshot before SAVE</td>
<td>accept</td>
<td>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.</td>
</tr>
</tbody>
</table>
<p dir="auto">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 <code>__MOKOSH_UAT__</code> Vite-define-token —
A29 introduces no new <code>__MOKOSH_UAT__</code>-gated symbols.
</threat_model></p>
<verification>
- `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 '<textarea' tests/uat/extension-page-harness.html` returns 0.
- `grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html` returns 1.
</verification>
<p dir="auto"><success_criteria></p>
<ul dir="auto">
<li>A29 GREEN in UAT harness across 6 host-side checks (A29.0a + A29.1..A29.5).</li>
<li>Probe HTML committed to harness page in correct location; head + script tag preserved.</li>
<li>FORBIDDEN_HOOK_STRINGS unchanged at 12 entries.</li>
<li>vitest baseline preserved (171/171 GREEN; SKIP_BUILD=1 path).</li>
<li>REQ-rrweb-dom-buffer empirically verified end-to-end through rrweb's
<code>record()</code> wiring + GET_RRWEB_EVENTS bridge + the assembled zip's
rrweb/session.json content.
</success_criteria></li>
</ul>
<output>
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
</output>
</body></html>