chore: merge executor worktree (worktree-agent-ab0a9017eb674054f) — Wave 1 Plan 03-01
This commit is contained in:
@@ -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 `<pre id="status">` scaffold; head + tokens.css link preserved; no <textarea> per RESEARCH Pitfall 4)
|
||||||
|
- tests/uat/extension-page-harness.ts (assertA29 page-side orchestrator + 3 module-local constants A29_SAVE_ARCHIVE_TIMEOUT_MS=15s + A29_SEGMENT_SETTLE_MS=11s + A29_MUTATION_SETTLE_MS=500ms; __mokoshHarness surface 28 → 29 methods)
|
||||||
|
- tests/uat/lib/harness-page-driver.ts (driveA29 host-side; @rrweb/types EventType import)
|
||||||
|
- tests/uat/harness.test.ts (driveA29 import + driveA29Wrapped const + drivers array push + banner A28 → A29)
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "assertA29 is a page-side ORCHESTRATOR (not a stub like assertA26/A28). The DOM mutation MUST dispatch BEFORE setupFreshRecording + segment-settle + SAVE so the IncrementalSnapshot lands in the rrweb buffer that the GET_RRWEB_EVENTS bridge pulls. Chaining off A28's already-completed zip is NOT viable here (Pitfall 1 — no mutation between A28's pre-existing recording and its SAVE)."
|
||||||
|
- "Host-side checks pushed AFTER the page-side A29.1 ack: A29.0 (zip present) + A29.0a (rrweb/session.json present) + A29.0b (JSON.parse) gate before A29.2..A29.5 (length>0 + Meta + FullSnapshot + IncrementalSnapshot via EventType enum). Naming preserves the page-side A29.1 + 4 plan-binding checks (A29.2..A29.5) verbatim from the plan's must-haves."
|
||||||
|
- "Probe HTML appended BELOW existing scaffold (line 21 `<pre id=\"status\">` → line 22 `<script>`); head + tokens.css link untouched per UI-SPEC + threat T-03-01-02. The modal-toggle inline onclick is the DOM-mutation source (style.display attribute mutation = IncrementalSnapshot trigger)."
|
||||||
|
- "Filter-pipeline form preserved (`[...new Set(events.map((e) => e.type))].sort((a, b) => a - b)`); no `continue`; if-else chains over early returns. CLAUDE.md Control Flow § honored."
|
||||||
|
- "@rrweb/types EventType enum imported (not magic numbers 2/3/4). Per CLAUDE.md TypeScript § semantic type aliases + RESEARCH Anti-Patterns § avoid hand-coded mapping."
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Page-side orchestrator pattern (NOT stub) for assertions where the page-side MUST own the SAVE because pre-SAVE DOM state matters — reusable for Plan 03-02 event-log triggers (click/input/navigation/js_error/network_error) which also need page-driven event injection BEFORE SAVE."
|
||||||
|
- "EventType enum grep against rrweb/session.json: structural assertion (events.some((e) => e.type === EventType.X)) is the canonical rrweb verification pattern. Avoids snapshot-comparison brittleness (RESEARCH Anti-Patterns + State of the Art tables). Reusable for any future rrweb-content assertion."
|
||||||
|
- "DOM-mutation source pattern: inline `onclick` handler toggling `style.display='block'|'none'` on a hidden `<div>` produces a clean rrweb attribute-mutation IncrementalSnapshot without coupling to chrome.* APIs or test-only hooks. Production-surface-friendly."
|
||||||
|
|
||||||
|
requirements-completed:
|
||||||
|
- REQ-rrweb-dom-buffer
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: "~10 min"
|
||||||
|
completed: 2026-05-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 03 Plan 01: A29 rrweb DOM verification harness extension Summary
|
||||||
|
|
||||||
|
**Single new harness assertion (A29) empirically verifies SPEC §10 #4 + REQ-rrweb-dom-buffer end-to-end through a real Chrome instance: rrweb's already-shipped `record()` wiring at `src/content/index.ts:285` emits Meta (EventType=4) + FullSnapshot (EventType=2) + IncrementalSnapshot (EventType=3) on the synthetic probe HTML (form + table + modal) when the driver injects a pre-SAVE DOM mutation. UAT count 29 → 30 GREEN; vitest 171/171 preserved; Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** ~10 min (Phase 3 Wave 1; first plan)
|
||||||
|
- **Started:** 2026-05-20T17:08:31Z (worktree spawn)
|
||||||
|
- **Completed:** 2026-05-20T17:18:03Z (SUMMARY commit)
|
||||||
|
- **Tasks:** 2 of 2 plan tasks complete (both autonomous)
|
||||||
|
- **Files modified:** 4 (1 HTML probe append + 3 TypeScript harness wires)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- **A29 (REQ-rrweb-dom-buffer + SPEC §10 #4):** 6 merged checks — A29.1 SAVE_ARCHIVE ack (page-side) + A29.0a rrweb/session.json present + A29.2 events.length>0 + A29.3 has Meta + A29.4 has FullSnapshot + A29.5 has IncrementalSnapshot. EMPIRICALLY verified GREEN on the harness's synthetic MediaStream + probe HTML.
|
||||||
|
- **Probe HTML composed per RESEARCH Pitfall 1 + Pitfall 4 + UI-SPEC:** form (#probe-form) with single-line text + email + password + submit inputs (Pitfall 4 — no `<textarea>` to avoid rrweb-alpha.4 issue #1596); table (#probe-table) with thead + 2 data rows; modal trigger (#probe-modal-trigger) with inline onclick toggling #probe-modal style.display; head + tokens.css link untouched (UI-SPEC + threat T-03-01-02).
|
||||||
|
- **DOM-mutation injection pattern empirically validated:** page-side assertA29 dispatches `#probe-text.value = 'probe'` + `dispatchEvent('input', { bubbles: true })` + `#probe-modal-trigger.click()` → 500ms settle → setupFreshRecording → 11s segment-settle → SAVE_ARCHIVE. rrweb captured 4 events spanning 3 distinct EventType surfaces (2 + 3 + 4); A29.5 IncrementalSnapshot empirically present.
|
||||||
|
- **Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12:** A29 rides production rrweb wiring + the existing GET_RRWEB_EVENTS round-trip + setupFreshRecording / sendMessageWithTimeout helpers. The unit-test gate (`tests/background/no-test-hooks-in-prod-bundle.test.ts`) AND the UAT A0 mirror (`tests/uat/harness.test.ts`) BOTH stay at 12 entries.
|
||||||
|
- **vitest baseline preserved:** 171/171 GREEN (full suite, 31 test files). Tier-1 FORBIDDEN_HOOK_STRINGS gate 13/13 sub-tests GREEN; 12 strings × 0 hits each.
|
||||||
|
- **tsc clean:** `npx tsc --noEmit` exit 0 on the modified surface.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each plan task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: probe HTML for A29 rrweb DOM verification** — `c02914d` (feat)
|
||||||
|
2. **Task 2: assertA29 + driveA29 + orchestrator wiring (A29 30/30 GREEN)** — `cc13f31` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `tests/uat/extension-page-harness.html` — probe HTML appended below `<pre id="status">` (line 21) and above `<script>` (line 22). Form (#probe-form) with text+email+password+submit; table (#probe-table) with thead + 2 rows; modal trigger (#probe-modal-trigger) toggling hidden div (#probe-modal). NO `<textarea>`; head + tokens.css link preserved.
|
||||||
|
- `tests/uat/extension-page-harness.ts` — assertA29 page-side orchestrator + 3 module-local constants (A29_SAVE_ARCHIVE_TIMEOUT_MS=15s, A29_SEGMENT_SETTLE_MS=11s, A29_MUTATION_SETTLE_MS=500ms); declare global interface extended; window.__mokoshHarness object literal extended; statusEl text + console banner updated A28 → A29.
|
||||||
|
- `tests/uat/lib/harness-page-driver.ts` — `import { EventType } from '@rrweb/types';` added after JSZip import; driveA29 host-side (3-phase: page.evaluate → findLatestZip → JSZip rrweb/session.json + EventType-enum grep). 6 checks total (A29.0a + A29.1..A29.5).
|
||||||
|
- `tests/uat/harness.test.ts` — driveA29 import + driveA29Wrapped const (mirrors driveA26/A27/A28 downloadsDir-needs wrapping) + drivers array push entry with Plan 03-01 banner + Architecture banner string updated.
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- **assertA29 is a page-side ORCHESTRATOR (not a stub).** The pre-SAVE DOM mutation MUST dispatch BEFORE setupFreshRecording + segment-settle + SAVE so the IncrementalSnapshot lands in the rrweb buffer that the GET_RRWEB_EVENTS bridge pulls. Chaining off A28's already-completed zip would miss the IncrementalSnapshot window (Pitfall 1). This diverges from assertA26/A28 (true stubs) and matches assertA27's orchestrator shape — but with a focus on rrweb DOM events rather than chrome.tabs multi-tab state.
|
||||||
|
- **Host-side checks A29.0/A29.0a/A29.0b gate before A29.2..A29.5.** Standard guard pattern from driveA26 (A26.1 metaFile-present + early-return; A26.2 JSON.parse + early-return) replicated for rrweb/session.json. A29.0 = zip present; A29.0a = rrweb/session.json entry exists; A29.0b = JSON.parse success. A29.2..A29.5 only execute if guards pass — produces clearer failure modes than testing inside try/catch.
|
||||||
|
- **EventType enum imported (not magic numbers).** `import { EventType } from '@rrweb/types';` makes the assertion-name template strings include the canonical enum value (`EventType.Meta=4`, `EventType.FullSnapshot=2`, `EventType.IncrementalSnapshot=3`) — operator-readable + tsc-validated. Matches CLAUDE.md TypeScript § "semantic type aliases over raw types" + RESEARCH Anti-Patterns ban on hand-coded mapping.
|
||||||
|
- **Probe HTML composition.** Per RESEARCH §"Specific Ideas" + UI-SPEC §"Test Fixture Conventions": synthetic inline HTML (no external network dependency); form variety (text + email + password — no `<textarea>` per Pitfall 4); small table (2 rows × 2 cols; covers rrweb tr/td snapshot path); hidden modal with inline-onclick toggle (provides the deterministic DOM-mutation source for Pitfall 1 mitigation). data-test-* attributes only (no data-mokosh-*; production-welcome-page reserved). No tokens.css import on probe sub-tree (head already imports canonical tokens for A18/A21).
|
||||||
|
- **Comment-text rewrite (Rule 1-equivalent).** The literal acceptance grep `! grep -q "textarea" tests/uat/extension-page-harness.html` would have failed against the original explanatory comment ("NO <textarea>") which referenced the banned element-name to document WHY it's excluded. Reworded the comment to avoid the literal token while preserving the provenance ("the rrweb-alpha.4-leaky multi-line input element" + "rrweb-io/rrweb issue #1596"). No behavioural impact; the actual DOM still has zero `<textarea>` elements (the binding contract per Pitfall 4). Documented inline in this SUMMARY because it's a planner-side acceptance-gate calibration, not a code deviation.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
1. **Comment-text wording adjustment (literal acceptance-gate calibration).**
|
||||||
|
- **Found during:** Task 1 verification.
|
||||||
|
- **Issue:** The plan's `<verify>` gate (`! grep -q "textarea" tests/uat/extension-page-harness.html`) and acceptance criterion (`grep -c "textarea" ... returns 0`) match BOTH the actual element AND any documentation mention. The original explanatory comment ("NO <textarea> (rrweb 2.0.0-alpha.4 issue #1596 leaks textarea values even with maskInputOptions.textarea set)") contained the literal word "textarea" 3× — would fail the literal grep.
|
||||||
|
- **Fix:** Reworded the comment to reference "the rrweb-alpha.4-leaky multi-line input element" + cite the rrweb-io/rrweb#1596 issue (preserves provenance for future readers). The binding contract — "no `<textarea>` element rendered" — remains 100% enforced; `grep -c -E '<textarea[ />]'` against the body content returns 0.
|
||||||
|
- **Files modified:** tests/uat/extension-page-harness.html (comment text only).
|
||||||
|
- **Verification:** Both acceptance gates green: `grep -c "textarea" tests/uat/extension-page-harness.html` returns 0; no actual `<textarea>` element in body.
|
||||||
|
- **Committed in:** c02914d (Task 1 commit).
|
||||||
|
|
||||||
|
**Total deviations:** 1 comment-text calibration (acceptance-gate literal match). No code-behaviour deviations. All plan must-haves + structural artifacts + key-links delivered verbatim.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Automation gates (this run)
|
||||||
|
|
||||||
|
- **tsc --noEmit:** Exit 0; clean.
|
||||||
|
- **npm run build:** Exit 0; dist/ populated (cited bundle output `dist/assets/index.ts-8LkXuqac.js` SW entry as Plan 02-04 closure precedent).
|
||||||
|
- **vitest 171/171 GREEN** — full suite preserved (31 test files; 11.97s).
|
||||||
|
- **Tier-1 FORBIDDEN_HOOK_STRINGS gate** (`tests/background/no-test-hooks-in-prod-bundle.test.ts`): 13/13 sub-tests GREEN; 12 strings × 0 hits each (5.85s).
|
||||||
|
- **UAT harness end-to-end:** `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exit 0; **30/30 GREEN** (29 prior + A29).
|
||||||
|
|
||||||
|
### A29 empirical evidence (from the live UAT trace)
|
||||||
|
|
||||||
|
```
|
||||||
|
[PASS] A29.1: SAVE_ARCHIVE ack received with success=true
|
||||||
|
expected: true, actual: true
|
||||||
|
[PASS] A29.0a: rrweb/session.json entry exists in zip
|
||||||
|
expected: true, actual: true
|
||||||
|
[PASS] A29.2: rrweb/session.json contains > 0 events
|
||||||
|
expected: ">0", actual: 4
|
||||||
|
[PASS] A29.3: rrweb emitted at least one Meta event (EventType.Meta=4)
|
||||||
|
expected: "has Meta", actual: true
|
||||||
|
[PASS] A29.4: rrweb emitted at least one FullSnapshot (EventType.FullSnapshot=2)
|
||||||
|
expected: "has FullSnapshot", actual: true
|
||||||
|
[PASS] A29.5: rrweb emitted at least one IncrementalSnapshot (EventType.IncrementalSnapshot=3)
|
||||||
|
expected: "has IncrementalSnapshot", actual: true
|
||||||
|
|
||||||
|
Diagnostics:
|
||||||
|
- A29 zipPath=/tmp/mokosh-uat-YgE1lu/8ddbec10-5461-4592-85b1-87c87131ef66.zip
|
||||||
|
- A29 events.length=4
|
||||||
|
- A29 event types: 2,3,4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plan must-haves coverage (all GREEN)
|
||||||
|
|
||||||
|
- `truths[0]` "rrweb session.json contains > 0 events after a probe-page interaction" — A29.2 PASS (events.length=4).
|
||||||
|
- `truths[1]` "rrweb emits at least one Meta event (EventType=4) on session start" — A29.3 PASS.
|
||||||
|
- `truths[2]` "rrweb emits at least one FullSnapshot (EventType=2) on session start" — A29.4 PASS.
|
||||||
|
- `truths[3]` "rrweb emits at least one IncrementalSnapshot (EventType=3) after a DOM mutation on the probe page" — A29.5 PASS (Pitfall 1 mitigation EMPIRICALLY verified).
|
||||||
|
- `truths[4]` "UAT harness exits 0 with 29 + 1 = 30/30 assertions GREEN (A0..A28 baseline preserved + new A29)" — PASS.
|
||||||
|
- `artifacts[0]` Probe HTML appended; head + tokens.css preserved — PASS (`grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">'` returns 1; head untouched).
|
||||||
|
- `artifacts[1]` assertA29 page-side stub registered on window.__mokoshHarness — PASS (3 grep hits in extension-page-harness.ts).
|
||||||
|
- `artifacts[2]` driveA29 host-side with full pattern — PASS (2 grep hits in harness-page-driver.ts; @rrweb/types import added).
|
||||||
|
- `artifacts[3]` driveA29 import + wrapped driver + drivers-array push entry with banner comment — PASS (6 grep hits in harness.test.ts).
|
||||||
|
- `key_links[0]` harness.test.ts → driveA29 — PASS (driveA29Wrapped const present).
|
||||||
|
- `key_links[1]` driveA29 → assertA29 via page.evaluate — PASS (`harness.assertA29()` invocation present in driveA29).
|
||||||
|
- `key_links[2]` driveA29 → @rrweb/types EventType — PASS (`import { EventType } from '@rrweb/types';` present).
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None blocking. One comment-text wording calibration documented above (Deviations §1) to satisfy a literal acceptance grep.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
None new. The plan's threat_model (T-03-01-01..T-03-01-04) was already analyzed at planner-time; implementation honors all mitigations:
|
||||||
|
|
||||||
|
- **T-03-01-01 (Information Disclosure — password input visible in DOM):** Probe HTML carries `<input type="password" id="probe-password">` with NO sentinel value. rrweb's existing `maskInputOptions.password=true` at `src/content/index.ts:306` masks any value the user / driver / future Plan 03-03 types into it. A29 itself does NOT type into the password input; Plan 03-03 owns the sentinel grep.
|
||||||
|
- **T-03-01-02 (Tampering — probe HTML interferes with A18/A21):** Probe HTML appended BELOW existing `<pre id="status">`; head untouched. tokens.css link preserved (`grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">'` returns 1). UAT trace shows A18 + A21 still GREEN.
|
||||||
|
- **T-03-01-03 (Information Disclosure — test-only hook leaks):** A29 rides production rrweb wiring + GET_RRWEB_EVENTS bridge + existing `setupFreshRecording`/`sendMessageWithTimeout` helpers. Zero new `__MOKOSH_UAT__`-gated symbols. Tier-1 inventory unchanged at 12 entries (13/13 unit-gate sub-tests GREEN).
|
||||||
|
- **T-03-01-04 (DoS — cleanupOldEvents drops the IncrementalSnapshot):** A29's wall-clock budget is ~11.5s (500ms mutation settle + 11s segment settle + SAVE dispatch). CLEANUP_INTERVAL_MS=60s + retention=10min at `src/content/index.ts:16-18` — far above the A29 window. Empirical evidence: 4 events captured spanning all 3 required EventType surfaces.
|
||||||
|
|
||||||
|
## Phase 3 Wave Sequencing
|
||||||
|
|
||||||
|
Per CONTEXT D-P3-01 + RESEARCH Pitfall 6: Plans 03-01..04 modify the SAME three harness files (extension-page-harness.ts, harness-page-driver.ts, harness.test.ts). RESEARCH §"Wave Sequencing Note" recommends SEQUENTIAL execution within Wave 2: 03-01 → 03-02 → 03-03 → 03-04. Plan 03-02 will follow A29's page-side-orchestrator pattern (event-log triggers require similar pre-SAVE event injection per RESEARCH Pattern 2). Plan 03-05 (VERIFICATION.md aggregator) runs in Wave 3 after 03-01..04 land.
|
||||||
|
|
||||||
|
## Next Plan Readiness
|
||||||
|
|
||||||
|
- **Plan 03-02 (event-log §10 #5):** Will likely add A30 with similar page-side orchestrator (Puppeteer page.click + page.type + page.evaluate dispatchEvent + fetch(404)) + host-side UserEvent grep against `logs/events.json`. A29's setupFreshRecording + segment-settle + SAVE template is reusable verbatim.
|
||||||
|
- **Plan 03-03 (§10 #8 PARTIAL password-filter):** Will type sentinel into the existing `#probe-password` element (already shipped by Plan 03-01; no new HTML touch) + negative-assertion grep on `logs/events.json` per RESEARCH Pattern 3.
|
||||||
|
- **Plan 03-04 (§10 #9 RAM best-effort):** Optional `puppeteer.Page.metrics()` scaffolding per RESEARCH §"Code Example A3X"; uncoupled from the rrweb / event-log surface.
|
||||||
|
- **Plan 03-05 (§10 sweep VERIFICATION.md aggregator):** Inherits A29 as the binding §10 #4 empirical gate (Phase 3 row in the per-requirement scorecard).
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- A29 assertion added: CONFIRMED via git log + grep (`assertA29` 3 hits in page; `driveA29` 2 hits in driver + 6 in orchestrator).
|
||||||
|
- UAT count: 29 → 30 GREEN: **EMPIRICALLY CONFIRMED** via `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exit 0; 30/30 assertions PASSED.
|
||||||
|
- vitest 171/171 GREEN preserved: CONFIRMED (full suite; 31 test files; 11.97s).
|
||||||
|
- FORBIDDEN_HOOK_STRINGS inventory at 12 (unchanged): CONFIRMED via Tier-1 unit-gate 13/13 sub-tests GREEN.
|
||||||
|
- Probe HTML invariants: no `<textarea>` (CONFIRMED `grep -c "<textarea" returns 0`); head + tokens.css link preserved (CONFIRMED `grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">'` returns 1); no data-mokosh-* attrs (CONFIRMED `grep -c -E 'data-mokosh-...' returns 0`).
|
||||||
|
- tsc clean: CONFIRMED (`npx tsc --noEmit` exit 0).
|
||||||
|
- 2/2 plan tasks committed atomically (c02914d + cc13f31).
|
||||||
|
- SUMMARY.md created and committed.
|
||||||
|
|
||||||
|
### File existence verification
|
||||||
|
|
||||||
|
```
|
||||||
|
FOUND: tests/uat/extension-page-harness.html (probe HTML appended)
|
||||||
|
FOUND: tests/uat/extension-page-harness.ts (assertA29 + window.__mokoshHarness entry)
|
||||||
|
FOUND: tests/uat/lib/harness-page-driver.ts (driveA29 + @rrweb/types import)
|
||||||
|
FOUND: tests/uat/harness.test.ts (driveA29 import + Wrapped const + drivers push)
|
||||||
|
FOUND: .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md (this file)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit verification
|
||||||
|
|
||||||
|
```
|
||||||
|
FOUND: c02914d feat(03-01): Task 1 — probe HTML for A29 rrweb DOM verification (SPEC §10 #4)
|
||||||
|
FOUND: cc13f31 feat(03-01): Task 2 — assertA29 + driveA29 + orchestrator wiring (A29 30/30 GREEN)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 03-spec-10-smoke-verification-dom-event-log-verification*
|
||||||
|
*Plan: 01*
|
||||||
|
*Completed: 2026-05-20*
|
||||||
@@ -19,6 +19,42 @@
|
|||||||
<p>This page lives at <code>chrome-extension://<id>/tests/uat/extension-page-harness.html</code>.</p>
|
<p>This page lives at <code>chrome-extension://<id>/tests/uat/extension-page-harness.html</code>.</p>
|
||||||
<p>Puppeteer navigates a tab here and drives assertions via <code>window.__mokoshHarness.*</code>.</p>
|
<p>Puppeteer navigates a tab here and drives assertions via <code>window.__mokoshHarness.*</code>.</p>
|
||||||
<pre id="status">Ready.</pre>
|
<pre id="status">Ready.</pre>
|
||||||
|
<!--
|
||||||
|
Plan 03-01 Wave 1 — probe HTML for A29 rrweb DOM verification
|
||||||
|
(SPEC §10 #4). Provides form + table + modal-trigger so rrweb's
|
||||||
|
record() emits Meta + FullSnapshot + IncrementalSnapshot events
|
||||||
|
against this page. Per RESEARCH Pitfall 4: this fixture omits
|
||||||
|
the rrweb-alpha.4-leaky multi-line input element (rrweb-io/rrweb
|
||||||
|
issue #1596 leaks its value even with maskInputOptions set);
|
||||||
|
only single-line inputs are used below. Per UI-SPEC Section
|
||||||
|
"Test Fixture Conventions": probe HTML uses plain data-test-*
|
||||||
|
attributes; no data-mokosh-* (production-welcome-page reserved);
|
||||||
|
no tokens.css import (the harness head already imports the
|
||||||
|
canonical tokens for A18/A21 — probe sub-tree should appear
|
||||||
|
unstyled to keep rrweb snapshots focused on structural DOM).
|
||||||
|
-->
|
||||||
|
<form id="probe-form" data-test="probe-form">
|
||||||
|
<input type="text" id="probe-text" data-test="probe-text" />
|
||||||
|
<input type="email" id="probe-email" data-test="probe-email" />
|
||||||
|
<input type="password" id="probe-password" data-test="probe-password" />
|
||||||
|
<button type="submit" id="probe-submit" data-test="probe-submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
<table id="probe-table" data-test="probe-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>col-a</th><th>col-b</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>row-1-a</td><td>row-1-b</td></tr>
|
||||||
|
<tr><td>row-2-a</td><td>row-2-b</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button id="probe-modal-trigger" data-test="probe-modal-trigger"
|
||||||
|
onclick="(function(){var m=document.getElementById('probe-modal');if(m){m.style.display=(m.style.display==='block'?'none':'block');}})()">
|
||||||
|
Toggle modal
|
||||||
|
</button>
|
||||||
|
<div id="probe-modal" data-test="probe-modal" style="display:none;">
|
||||||
|
<p>Probe modal content (Plan 03-01 A29 IncrementalSnapshot trigger).</p>
|
||||||
|
</div>
|
||||||
<script type="module" src="./extension-page-harness.ts"></script>
|
<script type="module" src="./extension-page-harness.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3315,6 +3315,109 @@ async function assertA28(): Promise<AssertionResult> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Plan 03-01 Task 2 — A29 (rrweb DOM verification; SPEC §10 #4) ─
|
||||||
|
*
|
||||||
|
* A29 — REQ-rrweb-dom-buffer empirical: rrweb's record() (already
|
||||||
|
* wired at src/content/index.ts:285) emits Meta + FullSnapshot
|
||||||
|
* + at least one IncrementalSnapshot when the harness page
|
||||||
|
* contains the probe HTML (form + table + modal) AND the driver
|
||||||
|
* injects a DOM mutation before SAVE (RESEARCH Pitfall 1:
|
||||||
|
* static probe HTML emits Meta + FullSnapshot but not
|
||||||
|
* IncrementalSnapshot without mutation).
|
||||||
|
*
|
||||||
|
* Page side: dispatch the probe-page DOM mutations (input value +
|
||||||
|
* modal toggle), settle, setupFreshRecording, settle one segment,
|
||||||
|
* dispatch SAVE_ARCHIVE, push A29.1 ack check. The host-side driveA29
|
||||||
|
* does the EventType-enum-shape grep against rrweb/session.json from
|
||||||
|
* the assembled zip (matches A26's chained-assertion pattern; JSZip
|
||||||
|
* + @rrweb/types are host-only deps).
|
||||||
|
*
|
||||||
|
* FORBIDDEN_HOOK_STRINGS impact: NONE. A29 rides production rrweb
|
||||||
|
* wiring (record() at src/content/index.ts:285 + GET_RRWEB_EVENTS
|
||||||
|
* round-trip at src/background/index.ts → src/content/index.ts:318)
|
||||||
|
* + the existing setupFreshRecording / sendMessageWithTimeout
|
||||||
|
* helpers. Tier-1 inventory stays at 12 entries.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** SAVE_ARCHIVE dispatch timeout for A29 — matches A24/A25/A27. */
|
||||||
|
const A29_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||||
|
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
|
||||||
|
const A29_SEGMENT_SETTLE_MS = 11_000;
|
||||||
|
/** Settle window between DOM mutation and SAVE so rrweb's
|
||||||
|
* IncrementalSnapshot lands in the in-memory buffer before
|
||||||
|
* GET_RRWEB_EVENTS fires. */
|
||||||
|
const A29_MUTATION_SETTLE_MS = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A29 — rrweb DOM event recording empirical (SPEC §10 #4).
|
||||||
|
*
|
||||||
|
* Page-side dispatches the probe-page mutation (input.value + modal
|
||||||
|
* toggle), settles, runs setupFreshRecording + segment-settle, then
|
||||||
|
* dispatches SAVE_ARCHIVE. Only the SAVE ack lives in the
|
||||||
|
* page-side checks; the EventType-enum-shape grep is host-side
|
||||||
|
* because @rrweb/types is host-only.
|
||||||
|
*
|
||||||
|
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
|
||||||
|
* driveA29 appends A29.0a/A29.2..A29.5 EventType checks.
|
||||||
|
*/
|
||||||
|
async function assertA29(): Promise<AssertionResult> {
|
||||||
|
const result: AssertionResult = {
|
||||||
|
passed: false,
|
||||||
|
name: 'A29 — rrweb DOM events recorded without errors (SPEC §10 #4 / REQ-rrweb-dom-buffer)',
|
||||||
|
checks: [],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
diag(result, 'Step 1: dispatch probe-page DOM mutation (input value + modal toggle)');
|
||||||
|
const textInput = document.querySelector<HTMLInputElement>('#probe-text');
|
||||||
|
if (textInput !== null) {
|
||||||
|
textInput.value = 'probe';
|
||||||
|
textInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
const modalTrigger = document.querySelector<HTMLButtonElement>('#probe-modal-trigger');
|
||||||
|
if (modalTrigger !== null) {
|
||||||
|
modalTrigger.click();
|
||||||
|
}
|
||||||
|
diag(result, `Step 1 OK — mutations dispatched (#probe-text=${textInput !== null}, #probe-modal-trigger=${modalTrigger !== null})`);
|
||||||
|
|
||||||
|
diag(result, `Step 2: settle ${A29_MUTATION_SETTLE_MS}ms for rrweb IncrementalSnapshot to enqueue`);
|
||||||
|
await new Promise((r) => setTimeout(r, A29_MUTATION_SETTLE_MS));
|
||||||
|
|
||||||
|
diag(result, 'Step 3: setupFreshRecording');
|
||||||
|
const setupResp = await setupFreshRecording();
|
||||||
|
if (!setupResp.ok) {
|
||||||
|
throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
|
||||||
|
}
|
||||||
|
diag(result, 'Step 3 OK — REC state established');
|
||||||
|
|
||||||
|
diag(result, `Step 4: settle ${A29_SEGMENT_SETTLE_MS}ms for first segment rotation`);
|
||||||
|
await new Promise((r) => setTimeout(r, A29_SEGMENT_SETTLE_MS));
|
||||||
|
|
||||||
|
diag(result, 'Step 5: dispatch SAVE_ARCHIVE');
|
||||||
|
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
|
||||||
|
{ type: 'SAVE_ARCHIVE' },
|
||||||
|
A29_SAVE_ARCHIVE_TIMEOUT_MS,
|
||||||
|
'SAVE_ARCHIVE (A29)',
|
||||||
|
);
|
||||||
|
diag(result, `Step 5 result: ${JSON.stringify(ack)}`);
|
||||||
|
|
||||||
|
result.checks.push({
|
||||||
|
name: 'A29.1: SAVE_ARCHIVE ack received with success=true',
|
||||||
|
expected: true,
|
||||||
|
actual: ack.success,
|
||||||
|
passed: ack.success === true,
|
||||||
|
});
|
||||||
|
|
||||||
|
result.passed = result.checks.every((c) => c.passed);
|
||||||
|
} catch (err) {
|
||||||
|
result.error = err instanceof Error ? err.message : String(err);
|
||||||
|
diag(result, `THREW: ${result.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read `chrome.runtime.getManifest().version`. Used by the host-side
|
* Read `chrome.runtime.getManifest().version`. Used by the host-side
|
||||||
* orchestrator at startup to capture the expected version for A13's
|
* orchestrator at startup to capture the expected version for A13's
|
||||||
@@ -3366,6 +3469,8 @@ declare global {
|
|||||||
assertA26: () => Promise<AssertionResult>;
|
assertA26: () => Promise<AssertionResult>;
|
||||||
assertA27: () => Promise<A27Result>;
|
assertA27: () => Promise<A27Result>;
|
||||||
assertA28: () => Promise<AssertionResult>;
|
assertA28: () => Promise<AssertionResult>;
|
||||||
|
// Plan 03-01 — rrweb DOM verification (SPEC §10 #4)
|
||||||
|
assertA29: () => Promise<AssertionResult>;
|
||||||
getManifestVersion: () => Promise<string>;
|
getManifestVersion: () => Promise<string>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3400,14 +3505,15 @@ window.__mokoshHarness = {
|
|||||||
assertA26,
|
assertA26,
|
||||||
assertA27,
|
assertA27,
|
||||||
assertA28,
|
assertA28,
|
||||||
|
assertA29,
|
||||||
getManifestVersion,
|
getManifestVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
if (statusEl !== null) {
|
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 {};
|
export {};
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ import {
|
|||||||
driveA26,
|
driveA26,
|
||||||
driveA27,
|
driveA27,
|
||||||
driveA28,
|
driveA28,
|
||||||
|
// Plan 03-01 — rrweb DOM verification (SPEC §10 #4 / REQ-rrweb-dom-buffer)
|
||||||
|
driveA29,
|
||||||
getManifestVersion,
|
getManifestVersion,
|
||||||
} from './lib/harness-page-driver';
|
} from './lib/harness-page-driver';
|
||||||
import {
|
import {
|
||||||
@@ -265,7 +267,7 @@ async function assertA0_GrepGate(): Promise<{
|
|||||||
*/
|
*/
|
||||||
async function main(): Promise<number> {
|
async function main(): Promise<number> {
|
||||||
process.stdout.write('\nMokosh Plan 01-13 + 01-14 + 02-04 — UAT harness orchestrator\n');
|
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');
|
process.stdout.write('='.repeat(72) + '\n');
|
||||||
|
|
||||||
// A0 pre-flight (no Chrome launch needed; runs against built dist/).
|
// A0 pre-flight (no Chrome launch needed; runs against built dist/).
|
||||||
@@ -333,6 +335,10 @@ async function main(): Promise<number> {
|
|||||||
(page) => driveA27(page, handles.downloadsDir);
|
(page) => driveA27(page, handles.downloadsDir);
|
||||||
const driveA28Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
const driveA28Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||||
(page) => driveA28(page, handles.downloadsDir);
|
(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<AssertionRecord> =
|
||||||
|
(page) => driveA29(page, handles.downloadsDir);
|
||||||
|
|
||||||
const drivers: ReadonlyArray<{
|
const drivers: ReadonlyArray<{
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
@@ -428,6 +434,13 @@ async function main(): Promise<number> {
|
|||||||
// and asserts EXACTLY 5 paths: video/last_30sec.webm, rrweb/session.json,
|
// and asserts EXACTLY 5 paths: video/last_30sec.webm, rrweb/session.json,
|
||||||
// logs/events.json, screenshot.png, meta.json (set-equality; no extras).
|
// logs/events.json, screenshot.png, meta.json (set-equality; no extras).
|
||||||
{ name: 'A28', drive: driveA28Wrapped },
|
{ 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 };
|
const buffers = { swConsole: handles.swConsole, offConsole: handles.offConsole };
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { tmpdir } from 'node:os';
|
|||||||
import { join, resolve as resolvePath } from 'node:path';
|
import { join, resolve as resolvePath } from 'node:path';
|
||||||
|
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
import { EventType } from '@rrweb/types';
|
||||||
import type { Page } from 'puppeteer';
|
import type { Page } from 'puppeteer';
|
||||||
|
|
||||||
import type { AssertionRecord, CheckRecord } from './assertions';
|
import type { AssertionRecord, CheckRecord } from './assertions';
|
||||||
@@ -1847,3 +1848,153 @@ export async function driveA28(
|
|||||||
error: pageResult.error,
|
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<AssertionRecord> {
|
||||||
|
// 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: `<error: ${parseErr}>`,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user