Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -0,0 +1,655 @@
|
||||
# Phase 3: SPEC §10 smoke verification + DOM/event-log verification - Research
|
||||
|
||||
**Researched:** 2026-05-20
|
||||
**Domain:** UAT harness verification (Approach B extension) for rrweb DOM capture + event-log + RAM scaffolding + §10 sweep
|
||||
**Confidence:** HIGH (Q1, Q2, Q3 verified against registry + official docs + existing harness; Q4 LOW for new chrome.* patterns — no production new surface required for charter)
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 3 is verification-heavy and the Approach B harness pattern (page-side `assertA*` + host-side `driveA*` + `harness.test.ts` orchestrator + Tier-1 `FORBIDDEN_HOOK_STRINGS` lockstep + pre-checkpoint bundle gates) established in Plans 01-13, 01-14, 02-04 is fully mature. The four scoped research questions resolve as follows:
|
||||
|
||||
1. **`puppeteer.Page.metrics()`** is reliable ONLY for the page realm it is called on; it returns CDP `Performance.getMetrics`, which exposes `JSHeapUsedSize` for the page's V8 isolate. It **does NOT measure the MV3 service worker context** — service workers live in a separate target with its own V8 isolate, and the metrics API on `Page` does not aggregate across workers/iframes. Scaffolding via `Page.metrics()` is feasible against the harness extension-internal page (a Page target) but yields a value that under-reports the operator-facing "extension background RAM ≤ 50 MB" claim. Per D-P3-04 best-effort framing, scaffolding via `Page.metrics()` MAY be added but MUST come with explicit diagnostic copy ("page-realm only; SW context excluded") and SHOULD NOT block §10 #9 verification.
|
||||
|
||||
2. **rrweb 2.0.0-alpha.4 alpha-pin is safe** for Phase 3 verification: npm registry `latest` tag for `rrweb` still points at 2.0.0-alpha.4 (2023 release; npm dist-tag confirmed by `npm view rrweb dist-tags`). The newer alpha.20 (2026-02-03) introduced a `NodeType` enum move from `rrweb-snapshot` to `@rrweb/types` (breaking import sites) but does not affect what Mokosh actually uses (the `record()` entry point + `EventType` enum signatures). No known regressions in alpha.4 that would make synthetic-probe-page verification flaky. Stable v2 has NOT shipped (zero non-alpha releases in the dist-tags table). Phase 4 upgrade research is correctly deferred.
|
||||
|
||||
3. **rrweb deterministic verification on synthetic probe pages** works via the *event-shape* assertion pattern, not snapshot-comparison. rrweb's own test suite uses snapshot files filtered through `stringifySnapshots` to normalize non-determinism (mouse positions, timestamps, blob URLs). For Mokosh's "rrweb records DOM events without errors" charter (SPEC §10 #4), the deterministic gate is structural: assert `events.length > 0` + `events[0].type === EventType.Meta` (=4) + at least one `EventType.FullSnapshot` (=2) + at least one `EventType.IncrementalSnapshot` (=3) emitted during a probe page lifecycle. This is far simpler than snapshot-comparison and matches the charter ("records without errors", not "produces specific DOM snapshot").
|
||||
|
||||
4. **chrome.* surface for §10 verification** is already covered by the existing 29-assertion harness — no new chrome.* patterns required for Phase 3. The Blob URL path is verified empirically by A24 via `chrome.downloads.onCreated` cross-realm capture (Plan 02-04). `chrome.tabs.captureVisibleTab` is verified transitively by A28 set-equality (screenshot.png present in zip). The chrome-extension://-context Network panel observation is an operator-only surface (Phase 2 VERIFICATION.md T5 cited as override) — no new automation pattern is required by Phase 3 charter.
|
||||
|
||||
**Primary recommendation:** Plan 03-01 through 03-05 follow the Plan 02-04 template verbatim: page-side stubs + host-side driveA* + chained-assertion + findLatestZip + JSZip host-only parse. New assertions stay on production surfaces (rrweb's already-shipped `record()` + content-script `GET_RRWEB_EVENTS` handler + `chrome.tabs.sendMessage`); Tier-1 FORBIDDEN_HOOK_STRINGS stays at **12 entries** (no new test-only symbols expected). RAM scaffolding stays optional and best-effort per D-P3-04. VERIFICATION.md aggregator (Plan 03-05) replicates Phase 2 VERIFICATION.md frontmatter shape (`overrides_applied` + `human_verification`).
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|------------------|
|
||||
| REQ-install-clean | Fresh build + load unpacked into Chrome without errors; manifest:name resolves; no remote-font CSP errors; branded icons; en+ru parity | Already COMPLETED Phase 1 Plan 01-12; Phase 3 verifies via §10 #1 aggregator in VERIFICATION.md citing existing tests (no-remote-fonts + manifest-i18n + locale-parity + bundle gates) |
|
||||
| REQ-rrweb-dom-buffer | rrweb records DOM events via `record()` over 10-min window, 5000-event cap, oldest-dropped; sensitive fields masked | Plan 03-01: synthetic form+table+modal probe HTML in `extension-page-harness.html`; harness assertion grep `events.length > 0` + EventType enum values 2/3/4; existing wiring at `src/content/index.ts:284-311` already ships maskInputOptions.password=true |
|
||||
| REQ-user-event-log | Captures 5 event types: click, input (no password), navigation (popstate/hashchange/pushState/replaceState), js_error (error + unhandledrejection), network_error (fetch >= 400 + XHR >= 400 + network failure) | Plan 03-02: Puppeteer page.click + page.type + page.goto + dispatchEvent(ErrorEvent) + fetch(404); harness greps `events.json` parsed from latest archive for one entry of each `type` value; existing wiring at `src/content/index.ts:60-237` |
|
||||
</phase_requirements>
|
||||
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
|
||||
- **D-P3-01:** Phase 3 = exactly 5 atomic plans:
|
||||
- 03-01 rrweb DOM verification harness extension (§10 #4)
|
||||
- 03-02 event-log verification harness extension (§10 #5)
|
||||
- 03-03 §10 #8 password-filter verification (verify existing minimum; PARTIAL mark)
|
||||
- 03-04 §10 #9 RAM ceiling best-effort (operator instructions + optional `puppeteer.Page.metrics` scaffolding)
|
||||
- 03-05 full §10 sweep VERIFICATION.md aggregating §10 #1-#9 evidence
|
||||
|
||||
- **D-P3-02:** SPEC §10 #8 = verify existing minimum + PARTIAL mark in VERIFICATION.md. The `src/content/index.ts:82` `if (target.type === 'password') return;` filter is the existing minimum. Plan 03-03 ships a harness assertion that VERIFIES this filter fires (synthetic password input + grep events.json for absence of entered value). NOT in scope: rrweb v2 `maskInputFn`, `data-sensitive` HTML attribute guards, full §10 #8 closure.
|
||||
|
||||
- **D-P3-03:** rrweb 2.0.0-alpha.4 stays pinned through Phase 3. Upgrade research + implementation DEFER TO PHASE 4.
|
||||
|
||||
- **D-P3-04:** SPEC §10 #9 = best-effort + operator instructions in VERIFICATION.md. Optional scaffolding via `puppeteer.Page.metrics()` if practical without research budget. NOT in scope: `chrome.devtools.Memory` API.
|
||||
|
||||
### Claude's Discretion
|
||||
|
||||
- Harness assertion numbering: A29+ continuing from A24-A28 sequence.
|
||||
- Probe page composition: synthetic HTML inline in `extension-page-harness.ts` vs. real-world navigation — planner's call.
|
||||
- Event-log trigger strategy: synthetic Puppeteer events vs. natural lifecycle observation — planner's call.
|
||||
- Sequencing: Plan 03-05 runs after 03-01..04 (synthesis plan); whether 03-01..04 parallelize within wave 2 depends on `files_modified` overlap audit at plan time.
|
||||
|
||||
### Deferred Ideas (OUT OF SCOPE)
|
||||
|
||||
- rrweb v2 stable upgrade research + implementation (D-P3-03)
|
||||
- Programmatic RAM measurement via `chrome.devtools.Memory` API (D-P3-04 partial defer)
|
||||
- REQ-password-confidentiality v2 candidate — full rrweb v2 `maskInputFn` + `data-sensitive` guards (D-P3-02)
|
||||
- Audit P1 #11/#14/#15 polish (fetch Request→[object Request], navigation URL tracking, rrweb timestamp semantics)
|
||||
- 2 pre-existing ffprobe/ffmpeg vitest flakes
|
||||
- getDisplayMedia cursor visibility refinement
|
||||
- Dark-surface logo contrast
|
||||
- setimmediate polyfill `new Function` in SW chunk
|
||||
- ROADMAP backfill for Plans 01-08..01-13
|
||||
|
||||
## Project Constraints (from global CLAUDE.md)
|
||||
|
||||
The user's global `~/.claude/CLAUDE.md` is loaded for this session (no project-local `CLAUDE.md` discovered). Directives that downstream planner + executor MUST honor:
|
||||
|
||||
| Directive | Application to Phase 3 |
|
||||
|-----------|------------------------|
|
||||
| **No `continue` statements — use filtering instead** | Already established in Plan 02-04 (`Object.keys(zip.files).filter((path) => !zip.files[path].dir)` per saved memory). New driveA* / assertA* code follows the filter-pipeline form. |
|
||||
| **Prefer if-else chains over early returns** | The `if (target.type === 'password') return;` at `src/content/index.ts:82` is the verification subject for Plan 03-03 (existing code; not modifying it). New verification code follows if-else chains. |
|
||||
| **No `break` inside loops when loop condition handles it** | A11 buffer-continuity loop precedent (`for (let i = 0; i < attempts; i++)` with no inner break) — follow same shape for any new polling loops. |
|
||||
| **Full-word names; standard acronym exceptions** | Existing harness uses `assertA29`, `driveA29` etc — full-word + standard `AXX` namespacing. |
|
||||
| **TypeScript semantic type aliases over raw types** | rrweb `EventType` enum is the semantic alias (imported from `@rrweb/types`); avoid magic numbers `2`, `3`, `4` in assertion code. |
|
||||
| **Type arrow function parameters explicitly** | Harness already typed (e.g., `(item: chrome.downloads.DownloadItem): void`). New listener/predicate callbacks follow same form. |
|
||||
| **Sanitize error messages: log details server-side, return generic messages to clients** | N/A for verification phase (no production-surface changes). |
|
||||
| **`WORKAROUND(org/repo#issue)` tags** | Apply if any rrweb behavior requires a known-issue workaround (none anticipated — see Q3 below). |
|
||||
|
||||
## Architectural Responsibility Map
|
||||
|
||||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||
|------------|-------------|----------------|-----------|
|
||||
| rrweb DOM event verification | Test harness (page-realm) | Production content script (read-only) | rrweb runs in the content script per `src/content/index.ts:284`; the harness verifies via the existing `GET_RRWEB_EVENTS` round-trip + `events.json` parse — no new prod surface |
|
||||
| Event-log capture verification | Test harness (page-realm) + Puppeteer host | Production content script (read-only) | Production wiring at `src/content/index.ts:60-237` is unchanged; harness drives synthetic browser events + greps the assembled archive's `logs/events.json` |
|
||||
| Password-filter (§10 #8) verification | Test harness (page-realm) | Production content script (read-only at `src/content/index.ts:82`) | Negative-assertion grep on `events.json` for absence of typed-sentinel value |
|
||||
| RAM ceiling (§10 #9) best-effort | Operator (chrome://memory-internals) | Optional `puppeteer.Page.metrics()` scaffolding | SW context is a separate target from `Page` — metrics API on Page does not reach it; operator-driven measurement is the canonical path per D-P3-04 |
|
||||
| §10 sweep aggregation | Plan 03-05 VERIFICATION.md (docs tier) | — | Pure docs synthesis aggregating Phase 1 + 2 + 3 evidence |
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core (existing; no new dependencies)
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| puppeteer | ^25.0.2 | Real-Chrome driver for the UAT harness | Established Plan 01-13 (Approach B); extension-targets API + `Page.metrics` + service-worker `worker.evaluate` all live here |
|
||||
| rrweb | 2.0.0-alpha.4 | Production DOM-event recorder being verified | Pinned per D-P3-03; alpha-pin stable across all phases to date |
|
||||
| @rrweb/types | (sub-package of rrweb 2.0.0-alpha.4) | `EventType` enum + `eventWithTime` type | Already present transitively at `node_modules/@rrweb/types/dist/index.d.ts` |
|
||||
| jszip | ^3.10.1 | Host-side archive parse for events.json + meta.json + zip-layout checks | Established Plan 02-04 (driveA26/A28) |
|
||||
| tsx | ^4.22.1 | TS runner for harness scripts (`tsx tests/uat/harness.test.ts`) | Established Plan 01-13 |
|
||||
| vitest | ^4 | Unit-test layer (Tier-1 grep gate + meta-json validators) | Established Phase 0/1 |
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| `puppeteer.Page.metrics()` | `performance.measureUserAgentSpecificMemory()` inside `worker.evaluate()` | More accurate (per-SW measurement) BUT requires cross-origin-isolation (COOP+COEP headers) which MV3 extensions do not set — would throw `SecurityError`. Skip; not viable for D-P3-04. |
|
||||
| `puppeteer.Page.metrics()` | CDP `Performance.getMetrics` directly via `page.target().createCDPSession()` | Equivalent data, more plumbing; `Page.metrics()` IS the wrapper. No win. |
|
||||
| Manual operator UAT (full Phase 3 closure) | Harness assertions A29+ | Per saved memory `feedback-trust-harness-over-manual-uat.md`, automation covers what automation can cover. Operator UAT reserved for §10 #9 RAM only. |
|
||||
| New rrweb test utility from `packages/rrweb/test/utils.ts` (assertDomSnapshot via MHTML) | Direct grep on `events[].type` for EventType enum values | rrweb's test util is for snapshot-comparison (MHTML diffs); Mokosh's charter is "records without errors" — structural assertion is simpler + matches charter literally. |
|
||||
|
||||
**Installation:** No new packages. Phase 3 is verification-only.
|
||||
|
||||
**Version verification (2026-05-20 via `npm view rrweb dist-tags`):**
|
||||
|
||||
```
|
||||
{
|
||||
beta: '1.0.0-beta.2',
|
||||
latest: '2.0.0-alpha.4', // 2023 release; PHASE-3 PIN
|
||||
alpha: '2.0.0-alpha.20' // 2026-02-03 release; Phase-4 upgrade-research target
|
||||
}
|
||||
```
|
||||
|
||||
`latest` dist-tag still pointing at alpha.4 (4 years old) confirms (a) no v2 stable shipped, (b) the project actively publishes newer alphas but does not promote them to `latest`. Phase 4 upgrade research deferral is correctly framed.
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### System Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ tsx tests/uat/harness.test.ts │ (Puppeteer HOST)
|
||||
│ - launch.ts → chromium + --load-extension=dist-test
|
||||
│ - sequential drive: driveA0..A28 + driveA29..A3X
|
||||
│ - bail-on-first-failure │
|
||||
└────────────┬────────────────────────────┘
|
||||
│ Puppeteer CDP
|
||||
│
|
||||
┌────────────────────┼────────────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌────────────────────────┐
|
||||
│ Page target │ │ Service Worker │ │ Offscreen Document │
|
||||
│ (chrome-extension://<id>/tests/uat/ │ │ (chrome.offscreen) │
|
||||
│ extension-page-harness.html) │ │ │
|
||||
│ │ │ src/background/ │ │ getDisplayMedia │
|
||||
│ window.__mokosh │ │ index.ts │ │ (synthetic via Canvas) │
|
||||
│ Harness.assertA* │ │ │ │ MediaRecorder segments │
|
||||
│ │ │ ↕ │ │ keepalivePort blob URLs│
|
||||
│ chrome.tabs. │ │ chrome.runtime. │ │ │
|
||||
│ sendMessage →│←─│ onMessage │←────────│ port: 'video-keepalive'│
|
||||
│ chrome.downloads.onCreated (cross-realm) │ │
|
||||
└─────────────────┘ └─────────────────┘ └────────────────────────┘
|
||||
▲ │ ▲
|
||||
│ │ chrome.tabs.sendMessage │
|
||||
│ │ (GET_RRWEB_EVENTS) │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ Content Script │ │
|
||||
│ │ src/content/index.ts │ │
|
||||
│ │ │ │
|
||||
│ │ rrweb.record() → rrwebEvents│ │
|
||||
│ │ click/input/nav/error log → userEvents│
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────── New for Phase 3 ────────────────────────┘
|
||||
Plan 03-01: probe HTML (form+table+modal) injected into the harness page tree
|
||||
Plan 03-02: Puppeteer synthetic event triggers + grep events.json from latest zip
|
||||
Plan 03-03: <input type=password> + sentinel typed + grep events.json for absence
|
||||
Plan 03-04: Page.metrics() scaffolding (optional) + operator instructions in VERIFICATION
|
||||
Plan 03-05: aggregator over Phase 1+2+3 evidence into VERIFICATION.md frontmatter
|
||||
```
|
||||
|
||||
### Component Responsibilities
|
||||
|
||||
| File | Responsibility | Phase 3 Touch |
|
||||
|------|---------------|---------------|
|
||||
| `src/content/index.ts` | rrweb wiring + event-log wiring | **READ-ONLY** (verified, not modified) |
|
||||
| `src/background/index.ts:GET_RRWEB_EVENTS` round-trip | SW → CS → SW transport of events for archive assembly | **READ-ONLY** |
|
||||
| `src/shared/types.ts:UserEvent` | Event-log shape contract | **READ-ONLY** (already used in Plan 03-02 grep) |
|
||||
| `tests/uat/extension-page-harness.html` | Harness page DOM scaffold; canonical tokens.css load-bearing for A18/A21 | **MODIFY** (Plan 03-01: add probe HTML below existing scaffold per UI-SPEC) |
|
||||
| `tests/uat/extension-page-harness.ts` | Page-side `assertA*` host (3413 LoC) | **MODIFY** (Plan 03-01..04: append assertA29..A3X stubs/handlers) |
|
||||
| `tests/uat/lib/harness-page-driver.ts` | Host-side `driveA*` host (1849 LoC) | **MODIFY** (Plan 03-01..04: append driveA29..A3X + JSZip-parse of events.json) |
|
||||
| `tests/uat/harness.test.ts` | Orchestrator + FORBIDDEN_HOOK_STRINGS unit-mirror (500 LoC) | **MODIFY** (Plan 03-01..04: import driveA29+; push to drivers[]; banner) |
|
||||
| `tests/background/no-test-hooks-in-prod-bundle.test.ts` | Tier-1 grep gate (12 entries currently) | **EXPECTED NO CHANGE** — A29+ ride existing prod surfaces |
|
||||
| `.planning/phases/03-.../03-05-VERIFICATION.md` | Final phase aggregator | **CREATE** (Plan 03-05; replicate Phase 2 VERIFICATION.md frontmatter shape) |
|
||||
|
||||
### Pattern 1: Approach B Harness Extension (rrweb DOM verification)
|
||||
|
||||
**What:** Page-side `assertA29(): Promise<AssertionResult>` greps `events.json` from the latest archive by EventType enum value; host-side `driveA29(page)` chains off A28's already-completed SAVE or runs its own SAVE.
|
||||
|
||||
**When to use:** §10 #4 rrweb DOM verification (Plan 03-01) — synthetic probe HTML triggers rrweb to record meta + full snapshot + at least one incremental snapshot.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// Source: existing pattern from tests/uat/lib/harness-page-driver.ts:driveA26
|
||||
// (Plan 02-04; established 2026-05-20)
|
||||
|
||||
// EventType enum values from @rrweb/types (verified via
|
||||
// node_modules/@rrweb/types/dist/index.d.ts grep):
|
||||
// DomContentLoaded = 0
|
||||
// Load = 1
|
||||
// FullSnapshot = 2
|
||||
// IncrementalSnapshot = 3
|
||||
// Meta = 4
|
||||
// Custom = 5
|
||||
// Plugin = 6
|
||||
|
||||
async function driveA29(page: Page, downloadsDir: string): Promise<AssertionResult> {
|
||||
// Chain off A28's already-completed SAVE — findLatestZip from harness-page-driver.ts
|
||||
const zipPath = await findLatestZip(downloadsDir);
|
||||
const buf = await fs.readFile(zipPath);
|
||||
const zip = await JSZip.loadAsync(buf);
|
||||
const rrwebRaw = await zip.file('rrweb/session.json')!.async('text');
|
||||
const events: Array<{ type: number; timestamp: number }> = JSON.parse(rrwebRaw);
|
||||
|
||||
// Structural checks (no snapshot-compare; matches charter "records without errors")
|
||||
const hasMeta = events.some((e) => e.type === 4);
|
||||
const hasFullSnapshot = events.some((e) => e.type === 2);
|
||||
const hasIncremental = events.some((e) => e.type === 3);
|
||||
// ... AssertionResult with 4 checks: length>0 + Meta + FullSnapshot + Incremental
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Event-Log Trigger via Puppeteer
|
||||
|
||||
**What:** Use `page.click` + `page.type` + `page.evaluate(() => window.dispatchEvent(...))` + `fetch(...)` from inside an opened tab to trigger all 5 `UserEvent` types; grep `logs/events.json` from the assembled archive.
|
||||
|
||||
**When to use:** §10 #5 event-log verification (Plan 03-02). All 5 types in one drive.
|
||||
|
||||
**Example (host-side; production surfaces only):**
|
||||
|
||||
```typescript
|
||||
// Plan 03-02 driveA30 (illustrative; planner picks A-numbers):
|
||||
// 1. Open a tab on https://example.com (production chrome.tabs.create)
|
||||
// 2. page.type('<input>', 'hello') → input event
|
||||
// 3. page.click('a') → click event
|
||||
// 4. page.goto('https://example.com#frag') → hashchange navigation event
|
||||
// 5. page.evaluate(() => window.dispatchEvent(new ErrorEvent('error',
|
||||
// { message: 'probe', filename: 'x', lineno: 1 }))) → js_error
|
||||
// 6. page.evaluate(() => fetch('https://example.com/404-probe')) → network_error (HTTP 404)
|
||||
// 7. SAVE_ARCHIVE; grep logs/events.json from latest zip
|
||||
// 8. Assert: events.some((e) => e.type === 'click') + ... × 5 types
|
||||
// where UserEvent.type ∈ {'click','input','navigation','js_error','network_error'}
|
||||
// per src/shared/types.ts:124-131
|
||||
```
|
||||
|
||||
### Pattern 3: Password Negative-Assertion
|
||||
|
||||
**What:** Type a sentinel string into `<input type="password">`; assert sentinel is ABSENT from `events.json`.
|
||||
|
||||
**When to use:** §10 #8 PARTIAL closure (Plan 03-03).
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// Plan 03-03 driveA31 (illustrative):
|
||||
// const SENTINEL = 'secret-do-not-log-123'; // fixed; not a real secret
|
||||
// await page.evaluate((s: string) => {
|
||||
// const input = document.querySelector<HTMLInputElement>('#probe-password');
|
||||
// if (input) { input.value = s; input.dispatchEvent(new Event('input', { bubbles: true })); }
|
||||
// }, SENTINEL);
|
||||
// SAVE_ARCHIVE; grep events.json
|
||||
// const eventsRaw = await zip.file('logs/events.json')!.async('text');
|
||||
// const userEvents: UserEvent[] = JSON.parse(eventsRaw);
|
||||
// const containsSentinel = userEvents.some((e) => e.value?.includes(SENTINEL));
|
||||
// expect(containsSentinel).toBe(false); // ABSENCE proves the password filter fires
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **Using rrweb's `assertDomSnapshot` (MHTML diff)** — Mokosh charter is "records without errors", not "matches a specific DOM snapshot". Snapshot-diff is brittle (random ports, timestamps, cursor positions per rrweb test utils). Structural assertion (EventType enum presence) matches charter literally and is order-of-magnitude simpler.
|
||||
- **Asserting rrweb event count** — rrweb emits a variable number of incremental events depending on probe page interactivity. Use `>= 1` floors per type, not exact equality.
|
||||
- **Reading SW heap via `puppeteer.Page.metrics()`** — `Page.metrics()` is bound to the Page target's V8 isolate. The MV3 service worker is a separate target (`target.type() === 'service_worker'`) with its own isolate. Document this limitation explicitly if scaffolding lands per D-P3-04.
|
||||
- **Adding new test-only `__MOKOSH_UAT__`-gated symbols** — Plan 03-01..04 should ride production surfaces only (rrweb.record() already shipped; GET_RRWEB_EVENTS bridge shipped Phase 1; chrome.tabs/chrome.downloads shipped). If a new test-only symbol becomes necessary, it MUST be added to BOTH FORBIDDEN_HOOK_STRINGS inventories (unit-gate at `tests/background/no-test-hooks-in-prod-bundle.test.ts:108` + UAT A0 mirror at `tests/uat/harness.test.ts:115`).
|
||||
- **Stripping the canonical tokens.css link from harness page** — Per Plan 01-12 Wave 6 + UI-SPEC: `<link rel="stylesheet" href="../../src/shared/tokens.css">` is load-bearing for A18 + A21. Probe HTML appends below the existing scaffold; does NOT modify the head.
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| rrweb event type identification | A hand-coded mapping `{ 0: 'DomContentLoaded', 2: 'FullSnapshot', ... }` | `EventType` enum from `@rrweb/types/dist/index.d.ts` | Already shipped in node_modules; values pinned by rrweb; import like `import { EventType } from '@rrweb/types';` — TypeScript-checked at build time. |
|
||||
| Archive zip parsing in tests | A custom zip parser | `JSZip.loadAsync` (same pattern as `tests/uat/lib/zip.ts` + `tests/uat/lib/harness-page-driver.ts:driveA26/A28`) | Host-only (not bundled into harness page); same precedent as Plan 02-04. |
|
||||
| Latest-zip lookup in downloadsDir | Hand-rolled directory poll | `findLatestZip` helper at `tests/uat/lib/harness-page-driver.ts:1395` | Mtime-sort chain; race-free because A24/A25/A27 drivers wait for their zip via stable-size protocol. |
|
||||
| Cross-realm download capture | `chrome.downloads.download` monkey-patch in harness page realm | `chrome.downloads.onCreated` listener (Plan 02-04 A24 pattern at `tests/uat/extension-page-harness.ts:2851`) | Production cross-realm API; canonical capture; Tier-1 inventory unaffected. |
|
||||
| Service worker memory measurement via Page.metrics | Treating `Page.metrics().JSHeapUsedSize` as the SW heap | Operator-driven `chrome://extensions/` service-worker memory display OR `chrome://memory-internals/` | Page.metrics is page-realm only; SW is a separate target. Per D-P3-04, operator instruction is the canonical path. |
|
||||
|
||||
**Key insight:** Phase 3 is mechanical replication of the Plan 02-04 harness-extension template against a new set of production surfaces (rrweb + event-log + password-filter). Custom infrastructure is anti-pattern — every needed pattern already exists in the harness lib.
|
||||
|
||||
## Runtime State Inventory
|
||||
|
||||
> Phase 3 is verification-only — no rename/refactor/migration. This section is included to confirm explicit non-applicability per protocol.
|
||||
|
||||
| Category | Items Found | Action Required |
|
||||
|----------|-------------|------------------|
|
||||
| Stored data | None — verified by `grep -rn "ChromaDB\|Mem0\|user_id" src/` returns 0 hits. No long-lived databases used; rrweb + event-log buffers are in-memory only per CON-buffer-storage. | None |
|
||||
| Live service config | None — verified by `grep -rn "n8n\|Datadog\|Tailscale\|Cloudflare" .` returns 0 hits. Extension is local-only per SPEC §9 "Out of Scope". | None |
|
||||
| OS-registered state | None — Phase 3 introduces no Windows Task Scheduler / pm2 / launchd / systemd registrations. | None |
|
||||
| Secrets/env vars | None — no `.env` or SOPS files in repo; Phase 3 reads no secrets. | None |
|
||||
| Build artifacts | None new — Phase 3 ships zero new bundled modules. Existing `dist/` + `dist-test/` artifacts regenerated by `npm run build` + `npm run build:test` are inputs to verification; no Phase-3-owned build artifacts. | None |
|
||||
|
||||
**Confirmation:** No runtime state to migrate; Phase 3 is verification-only.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: rrweb event timing race
|
||||
**What goes wrong:** `assertA29` reads `events.json` from a zip assembled BEFORE rrweb's `IncrementalSnapshot` has fired (snapshots fire on mutation; static probe HTML produces only Meta + FullSnapshot).
|
||||
**Why it happens:** Synthetic probe page has no DOM mutations between load and SAVE — `IncrementalSnapshot` is mutation-driven.
|
||||
**How to avoid:** Plan 03-01 driveA29 MUST inject at least one DOM mutation (`page.evaluate(() => document.body.appendChild(document.createElement('div')))`) before SAVE, OR drive the modal trigger button click. Probe HTML modal + table both qualify per UI-SPEC.
|
||||
**Warning signs:** A29.3 `hasIncrementalSnapshot` returns false intermittently.
|
||||
|
||||
### Pitfall 2: Service worker memory under-reporting via Page.metrics
|
||||
**What goes wrong:** Plan 03-04 ships `puppeteer.Page.metrics().JSHeapUsedSize` and reports e.g. 4 MB; operator interprets this as "extension RAM is well under 50 MB"; real extension SW RAM (separate target) is 35 MB.
|
||||
**Why it happens:** `Page.metrics()` measures only the page realm; SW lives in a different target.
|
||||
**How to avoid:** If Plan 03-04 ships scaffolding, gate output behind explicit diagnostic copy: `"NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04."` Do NOT treat scaffolding value as authoritative; the operator chrome://memory-internals check is the binding §10 #9 gate.
|
||||
**Warning signs:** Operator sees a green automation number and skips the chrome://memory-internals step.
|
||||
|
||||
### Pitfall 3: chrome.tabs permission gap on probe pages
|
||||
**What goes wrong:** Plan 03-02 opens a tab on `https://example.com` via `chrome.tabs.create` — works because Phase 2 Plan 02-03 added the `tabs` permission (DEC-011 Amendment 1). However, if the probe-page composition uses URLs that violate the `URL_SCHEME_ALLOW` regex at `src/background/tab-url-tracker.ts:79` (`^(https?|chrome-extension):\/\/`), `meta.urls` will be empty and downstream assertions may fail intermittently.
|
||||
**Why it happens:** `file://`, `about:`, `chrome://` URLs are filtered out of `urls[]` by design (F2 fallback path).
|
||||
**How to avoid:** Plan 03-02 probe pages use `https://example.com` or `https://www.iana.org` (already established as Plan 02-04 A27 fixtures). Do NOT use `about:blank` or `file://`.
|
||||
**Warning signs:** A30 passes locally but fails in CI when the test URL is changed to a non-http(s) target.
|
||||
|
||||
### Pitfall 4: rrweb alpha.4 known issue — `genTextAreaValueMutation`
|
||||
**What goes wrong:** rrweb 2.0.0-alpha.4 has an open issue (`rrweb-io/rrweb#1596`) where `genTextAreaValueMutation` doesn't mask the `attribute.value` of a textarea. If the probe page includes a textarea, the value could leak into incremental snapshots EVEN WHEN `maskInputOptions.textarea` is set.
|
||||
**Why it happens:** Known alpha.4 bug; not yet patched in any 2.0 release.
|
||||
**How to avoid:** Plan 03-01 probe HTML uses `<input type="text">` + `<input type="email">` + `<input type="password">`, NOT `<textarea>`. The form-input variety in UI-SPEC Section "Test Fixture Conventions" already excludes textarea. Confirm during planning that no `<textarea>` is introduced.
|
||||
**Warning signs:** A29 passes; a follow-on plan (not Phase 3) introduces a textarea and Plan 03-03 password-filter check passes but textarea leak is unaccounted for.
|
||||
|
||||
### Pitfall 5: Pre-checkpoint bundle gate breakage from Phase 3 changes
|
||||
**What goes wrong:** Plan 03-* adds a new test-only symbol that escapes the Vite `__MOKOSH_UAT__` define-token tree-shake, lands in `dist/`, and breaks Gate 5/6 in the pre-checkpoint bundle gates.
|
||||
**Why it happens:** A planner introduces a new debug-only flag or helper without adding it to FORBIDDEN_HOOK_STRINGS.
|
||||
**How to avoid:** Per Plan 02-04 SUMMARY decision: "FORBIDDEN_HOOK_STRINGS unchanged at 12. A26/A28 are host-side JSZip; A27 uses chrome.tabs.create/update/remove (production APIs)." Phase 3 plans SHOULD stay at 12 entries. If any plan finds a real need for a new symbol, the planner MUST add it to BOTH FORBIDDEN_HOOK_STRINGS inventories AND `__MOKOSH_UAT__`-gate it.
|
||||
**Warning signs:** Tier-1 grep gate fails after Plan 03-* commit; bundle gate 6 shows new occurrences in `dist/`.
|
||||
|
||||
### Pitfall 6: `findLatestZip` race between parallel A29-A3X drivers
|
||||
**What goes wrong:** Plan 03-01..04 plans run in parallel within wave 2 (per "Claude's Discretion" in CONTEXT.md), each does its own SAVE, and `findLatestZip` returns the WRONG zip to a chained-assertion downstream.
|
||||
**Why it happens:** Multiple SAVEs in flight + mtime collision (1s precision on some filesystems).
|
||||
**How to avoid:** Per Phase 2 Wave 2 lesson cited in CONTEXT.md: "plan-checker should catch overlaps". Plan-checker validates `files_modified` overlap audit — if both Plan 03-01 and Plan 03-02 modify the same harness file in different ways, mark wave 2 as SEQUENTIAL. The safer default is: 03-01 → 03-02 → 03-03 → 03-04 sequential within wave 2; 03-05 in wave 3.
|
||||
**Warning signs:** Wave 2 commits land in different order than planned; A30 passes locally but fails when chained after A29.
|
||||
|
||||
## Code Examples
|
||||
|
||||
### A29: Synthetic probe → rrweb event-shape grep
|
||||
|
||||
```typescript
|
||||
// Source: pattern from tests/uat/lib/harness-page-driver.ts:driveA26
|
||||
// (Plan 02-04 host-side; established 2026-05-20)
|
||||
// Plan: 03-01 (illustrative; planner finalizes)
|
||||
|
||||
import { EventType } from '@rrweb/types';
|
||||
|
||||
async function driveA29(page: Page, downloadsDir: string): Promise<AssertionResult> {
|
||||
// Step 1: trigger probe page interactions (form input + modal click)
|
||||
await page.evaluate(() => {
|
||||
const input = document.querySelector<HTMLInputElement>('#probe-text');
|
||||
if (input) {
|
||||
input.value = 'probe';
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
const button = document.querySelector<HTMLButtonElement>('#probe-modal-trigger');
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: wait for at least one IncrementalSnapshot to land
|
||||
// (rrweb buffers events; small delay before SAVE is safe)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Step 3: SAVE — chain off A28's already-completed flow OR own SAVE
|
||||
// (planner decides based on files_modified overlap; see Pitfall 6)
|
||||
// [...] // SAVE_ARCHIVE dispatch via existing pattern
|
||||
|
||||
// Step 4: read latest zip + grep rrweb/session.json
|
||||
const zipPath = await findLatestZip(downloadsDir);
|
||||
const buf = await fsPromises.readFile(zipPath);
|
||||
const zip = await JSZip.loadAsync(buf);
|
||||
const rrwebRaw = await zip.file('rrweb/session.json')!.async('text');
|
||||
const events: Array<{ type: number; timestamp: number }> = JSON.parse(rrwebRaw);
|
||||
|
||||
const checks = [
|
||||
{
|
||||
name: 'A29.1: rrweb session.json contains > 0 events',
|
||||
passed: events.length > 0,
|
||||
expected: '> 0',
|
||||
actual: String(events.length),
|
||||
},
|
||||
{
|
||||
name: 'A29.2: rrweb emitted at least one Meta event (EventType=4)',
|
||||
passed: events.some((e) => e.type === EventType.Meta),
|
||||
expected: 'has Meta',
|
||||
actual: String(events.some((e) => e.type === EventType.Meta)),
|
||||
},
|
||||
{
|
||||
name: 'A29.3: rrweb emitted at least one FullSnapshot (EventType=2)',
|
||||
passed: events.some((e) => e.type === EventType.FullSnapshot),
|
||||
expected: 'has FullSnapshot',
|
||||
actual: String(events.some((e) => e.type === EventType.FullSnapshot)),
|
||||
},
|
||||
{
|
||||
name: 'A29.4: rrweb emitted at least one IncrementalSnapshot (EventType=3)',
|
||||
passed: events.some((e) => e.type === EventType.IncrementalSnapshot),
|
||||
expected: 'has IncrementalSnapshot',
|
||||
actual: String(events.some((e) => e.type === EventType.IncrementalSnapshot)),
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
passed: checks.every((c) => c.passed),
|
||||
name: 'A29 — rrweb DOM events recorded without errors (SPEC §10 #4)',
|
||||
checks,
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### A3X: Optional puppeteer.Page.metrics() RAM scaffolding (Plan 03-04)
|
||||
|
||||
```typescript
|
||||
// Source: pptr.dev/api/puppeteer.page.metrics + existing harness host pattern
|
||||
// Plan: 03-04 (optional per D-P3-04; planner decides "if practical")
|
||||
|
||||
async function driveA3X_OptionalRamScaffold(page: Page): Promise<AssertionResult> {
|
||||
// Page.metrics is page-realm only — JSHeapUsedSize covers V8 isolate of THIS Page,
|
||||
// NOT the MV3 service worker (separate target).
|
||||
const metrics = await page.metrics();
|
||||
const jsHeapMB = metrics.JSHeapUsedSize !== undefined
|
||||
? metrics.JSHeapUsedSize / (1024 * 1024)
|
||||
: -1;
|
||||
|
||||
// 50 MB threshold per SPEC §10 #9; treat as best-effort floor for the page realm
|
||||
// alone. Operator-driven chrome://memory-internals is the binding §10 #9 gate.
|
||||
const checks = [
|
||||
{
|
||||
name: 'A3X.1: Page.metrics returned JSHeapUsedSize',
|
||||
passed: jsHeapMB >= 0,
|
||||
expected: '>= 0',
|
||||
actual: String(jsHeapMB),
|
||||
},
|
||||
{
|
||||
name: 'A3X.2: Page-realm JS heap < 50 MB (NOTE: scaffolding only; SW context excluded)',
|
||||
passed: jsHeapMB < 50,
|
||||
expected: '< 50 MB',
|
||||
actual: `${jsHeapMB.toFixed(2)} MB`,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
passed: checks.every((c) => c.passed),
|
||||
name: 'A3X — RAM scaffolding (best-effort; page-realm only per D-P3-04)',
|
||||
checks,
|
||||
diagnostics: [
|
||||
'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.',
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### VERIFICATION.md frontmatter template (Plan 03-05)
|
||||
|
||||
```yaml
|
||||
---
|
||||
phase: 03-spec-10-smoke-verification-dom-event-log-verification
|
||||
verified: 2026-05-20T<HH-MM-SS>Z
|
||||
status: passed
|
||||
score: 9/9 SPEC §10 criteria
|
||||
overrides_applied: <count>
|
||||
override_notes:
|
||||
- dimension: "SPEC §10 #8 — Password masking (PARTIAL per D-P3-02 charter)"
|
||||
initial_status: "PARTIAL"
|
||||
rationale: |
|
||||
Full rrweb v2 maskInputFn + data-sensitive guards DEFERRED per 2026-05-20 charter
|
||||
'we don't care about privacy hardening. At least here.' REQ-password-confidentiality
|
||||
moved to Out of Scope v1. Existing src/content/index.ts:82 `if (target.type === 'password') return;`
|
||||
filter VERIFIED by Plan 03-03 A31 (sentinel string typed; greps events.json for absence).
|
||||
human_verification:
|
||||
- dimension: "SPEC §10 #9 — Extension RAM ≤ 50 MB"
|
||||
rationale: |
|
||||
Per D-P3-04: operator instructions in VERIFICATION below — load extension, idle 5 min,
|
||||
open chrome://memory-internals OR chrome://extensions service-worker memory display,
|
||||
verify extension background RAM < 50 MB. Page.metrics scaffolding (if shipped by
|
||||
Plan 03-04) measures page-realm only and does not cover the SW target.
|
||||
---
|
||||
```
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Operator UAT for all functional gates | Approach B harness (Plan 01-13) | 2026-05-19 | Plan 01-09 Task 5 retired; operator UAT reserved for brand judgment + RAM-ceiling-style genuinely-non-automatable surfaces |
|
||||
| Snapshot-comparison via MHTML (rrweb-io test util) | Structural EventType enum grep | Phase 3 introduction (this plan) | Order-of-magnitude simpler; matches "records without errors" charter literally |
|
||||
| `data:application/zip;base64,...` download | `blob:chrome-extension://<id>/<uuid>` mint | Plan 02-02 (D-P2-01) | Closes audit P0-6 base64 cap; Plan 03-05 inherits A24 evidence for §10 #6 latency aggregation |
|
||||
| 7-field `meta.json` with `url: string` | 8-field with `urls: string[]` + `schemaVersion: '2'` | Plan 02-03 (D-P2-02 + D-P2-03) | Plan 03-05 inherits A26 evidence for §10 #7 aggregation |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- rrweb 1.x patterns referenced in older blog posts (e.g., `inlineStylesheet: true` as a separate option) — Mokosh uses 2.0.0-alpha.4 where the option lives inside `record()` config and is implicitly enabled. Not relevant for Phase 3 (verification-only).
|
||||
- `chrome.tabCapture` references in pre-D-01 historical commits — fully replaced by `chrome.offscreen` + `getDisplayMedia`; Plan 01-05 closed.
|
||||
|
||||
## Assumptions Log
|
||||
|
||||
> Claims tagged `[ASSUMED]` in this research that the planner / discuss-phase should confirm before locking decisions. Most claims here are `[VERIFIED]` via npm registry + grep + file reads; the table lists residual unknowns.
|
||||
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
|---|-------|---------|---------------|
|
||||
| A1 | rrweb 2.0.0-alpha.4 has no NEW regressions since Phase 1 + 2 closure that would make Plan 03-01..03 flaky | Q3 / Pitfall 4 | LOW — alpha.4 is the same artifact that has powered 9 closed plans + 29/29 UAT GREEN end-to-end. Risk is a latent edge case in textarea handling (issue #1596) — mitigated by probe HTML excluding textarea. |
|
||||
| A2 | The probe-page injection mechanism (synthetic HTML inline in `extension-page-harness.ts`) does not interfere with existing assertions A18+A21 that depend on canonical tokens.css being loaded | Pattern 1 / UI-SPEC | LOW — UI-SPEC explicitly bans the probe HTML from importing tokens.css; head element preserves the existing tokens.css link. Confirm during Plan 03-01 plan-checker pass. |
|
||||
| A3 | `Page.metrics()` JSHeapUsedSize is available in Puppeteer 25.0.2 (project pin) | Pattern A3X / Pitfall 2 | LOW — Page.metrics has been stable since Puppeteer 1.x; documented at pptr.dev/api/puppeteer.page.metrics. Verify by quick smoke before committing scaffolding. |
|
||||
| A4 | `EventType.Meta` (=4) always fires before `EventType.FullSnapshot` (=2) on a fresh recording start | Code Example A29 | LOW — rrweb spec; `Meta` event is the meta header (URL + width + height + timestamp) and is emitted as the first event of a session. If wrong, A29.2 + A29.3 both fail; both checks have independent grep so the failure mode is visible. |
|
||||
| A5 | Plans 03-01..04 can complete the harness orchestrator extension without modifying `tests/uat/extension-page-harness.html` beyond the probe-HTML append | Component Responsibilities | LOW — probe HTML is below the existing `<h1>` and `<pre id="status">`; planner-side confirms the head + tokens.css link remain untouched. |
|
||||
| A6 | Phase 3 plans can stay at FORBIDDEN_HOOK_STRINGS = 12 entries (no new __MOKOSH_UAT__ symbol required) | Pitfall 5 / Standard Stack | MEDIUM — most likely true (rrweb + chrome.tabs + chrome.downloads are already production surfaces), but if Plan 03-04 scaffolding requires a new bridge op (e.g., `get-page-metrics` from offscreen → harness), that would add 1-2 entries. Surface in plan-checker pass. |
|
||||
| A7 | `chrome://memory-internals` is available on Chrome stable 124+ (operator-facing path) | RAM operator instructions / D-P3-04 | LOW — chrome://memory-internals is a stable internal URL since Chrome 80+. The `chrome://extensions/` service-worker memory display (Inspect views > service worker > Memory) is the canonical alternative. |
|
||||
| A8 | The harness host can read the assembled zip's `rrweb/session.json` and `logs/events.json` via existing `findLatestZip` + JSZip pattern (Plan 02-04 precedent) | Don't Hand-Roll / Pattern 1 | VERIFIED — Plan 02-04 driveA26 + driveA28 already do exactly this for meta.json + zip-layout; rrweb/session.json + logs/events.json are sibling entries in the canonical 5-entry layout. |
|
||||
|
||||
**If the table is mostly LOW risk:** confirmed. The only MEDIUM is A6; planner-side flag.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Wave structure: parallel or sequential for Plans 03-01..04?**
|
||||
- What we know: Plan 02-04 ran A24-A28 sequentially within a single plan (one author). Phase 3's 5-plan structure is finer-grained per D-P3-01.
|
||||
- What's unclear: Do Plans 03-01, 03-02, 03-03 modify the SAME `tests/uat/extension-page-harness.ts` + `tests/uat/lib/harness-page-driver.ts` files in ways that would conflict on parallel execution? CONTEXT.md "Claude's Discretion" defers to plan-time `files_modified` overlap audit.
|
||||
- Recommendation: Default sequential (03-01 → 03-02 → 03-03 → 03-04 → 03-05) unless plan-checker confirms zero overlap. Phase 2 lesson cited in CONTEXT.md is "plan-checker should catch overlaps"; honor it.
|
||||
|
||||
2. **A29 chaining: own SAVE vs. chain off A28?**
|
||||
- What we know: A26 + A28 chain off A25's already-completed SAVE per Plan 02-04 (no new SAVE). A27 owns its SAVE because the tab tracker needs the multi-tab onActivated events to fire BEFORE dispatch.
|
||||
- What's unclear: Does the probe-page DOM injection happen before the existing harness page's recorded session, or before a separate session? rrweb starts on `start()` (content-script init) — by the time the harness page is loaded, rrweb is already recording.
|
||||
- Recommendation: A29 chains off A28's already-completed SAVE if the DOM mutations happen during the rolling 30s recording window before A25's SAVE. If timing-fragile, A29 owns its SAVE with `setupFreshRecording` + segment-settle, mirroring A27. Plan 03-01 planner picks based on smoke-test.
|
||||
|
||||
3. **Plan 03-04 Page.metrics scaffolding: include or skip?**
|
||||
- What we know: D-P3-04 says "if practical without research budget". This research budget is exhausted; Page.metrics IS practical (single API call); scaffolding is ~30 lines.
|
||||
- What's unclear: Is the resulting page-realm-only measurement value worth the diagnostic copy overhead? Operator-facing chrome://memory-internals is unaffected.
|
||||
- Recommendation: SHIP the scaffolding (low cost). Output is informational only; explicit diagnostic copy ("page-realm only; SW context excluded") avoids over-interpretation. Tier-1 inventory unaffected (Page.metrics is host-side; not bundled).
|
||||
|
||||
## Environment Availability
|
||||
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
|------------|------------|-----------|---------|----------|
|
||||
| Node.js + npm | All harness tooling | ✓ | (project pin) | — |
|
||||
| Puppeteer ^25 | UAT harness driver | ✓ | 25.0.2 (package.json) | — |
|
||||
| Chrome / Chromium (Puppeteer-bundled) | Real-Chrome target for harness | ✓ | Puppeteer-bundled stable | — |
|
||||
| rrweb 2.0.0-alpha.4 + @rrweb/types | DOM event verification (Plan 03-01) | ✓ | Pinned (package.json:11) | — |
|
||||
| JSZip | Archive parsing in driveA29+ | ✓ | 3.10.1 | — |
|
||||
| tsx | TS runner | ✓ | 4.22.1 | — |
|
||||
| vitest | Unit-test layer (Tier-1 grep gate) | ✓ | ^4 | — |
|
||||
| chrome://memory-internals (operator) | §10 #9 RAM check | ✓ (Chrome stable 80+) | — | chrome://extensions service-worker Memory tab |
|
||||
| ffprobe / ffmpeg | §10 #7 video playback re-verification (already covered in Phase 1 VERIFICATION) | Already verified Phase 1 closure | — | — |
|
||||
|
||||
**Missing dependencies with no fallback:** None. Phase 3 introduces zero new dependencies.
|
||||
|
||||
**Missing dependencies with fallback:** None.
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
> Required per `.planning/config.json:workflow.tdd_mode = true` (workflow.nyquist_validation absent — treat as enabled).
|
||||
|
||||
### Test Framework
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | vitest ^4 (171/171 GREEN post Phase 2) + Puppeteer ^25 UAT harness (29/29 GREEN) |
|
||||
| Config file | `vite.config.ts` + `vite.test.config.ts` (defines `__MOKOSH_UAT__`); `vitest.config.ts` if present (project pins via package.json scripts) |
|
||||
| Quick run command | `npm test` (vitest) for unit tier; `SKIP_PROD_REBUILD=1 HEADLESS=1 npm run test:uat` for harness tier |
|
||||
| Full suite command | `npm test && npm run test:uat` (sequential — UAT depends on `npm run build:test` artifact) |
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| REQ-install-clean | §10 #1 fresh build loads unpacked w/o errors | aggregator (existing tests) | `npm test` covers manifest + i18n + no-remote-fonts | ✅ tests/i18n/manifest-i18n.test.ts + tests/build/no-remote-fonts.test.ts |
|
||||
| REQ-rrweb-dom-buffer | §10 #4 rrweb records meta+full+incremental on probe page | UAT harness (Plan 03-01 A29) | `npm run test:uat` (post-Plan 03-01) | ❌ Wave 0 (A29 to be created) |
|
||||
| REQ-user-event-log | §10 #5 events.json contains 5 UserEvent types | UAT harness (Plan 03-02 A30) | `npm run test:uat` (post-Plan 03-02) | ❌ Wave 0 (A30 to be created) |
|
||||
| (§10 #8 PARTIAL per D-P3-02) | Password sentinel absent from events.json | UAT harness (Plan 03-03 A31 negative-assert) | `npm run test:uat` (post-Plan 03-03) | ❌ Wave 0 (A31 to be created) |
|
||||
| (§10 #9 best-effort per D-P3-04) | RAM scaffolding via Page.metrics OR operator chrome://memory-internals | UAT harness (Plan 03-04 A3X, optional) + manual-only operator step | `npm run test:uat` (post-Plan 03-04) + operator | Optional Wave 0 (A3X may or may not be created) |
|
||||
|
||||
### Sampling Rate
|
||||
- **Per task commit:** `npm test` (~30s for full vitest suite)
|
||||
- **Per wave merge:** `npm run test:uat` (~95s; Plan 02-04 baseline)
|
||||
- **Phase gate:** `npm test && npm run test:uat` both GREEN before `/gsd-verify-work 3`
|
||||
|
||||
### Wave 0 Gaps
|
||||
- [ ] `tests/uat/extension-page-harness.ts` — append probe HTML injection helper + assertA29..A31 (+ optional A3X) — Plan 03-01..04
|
||||
- [ ] `tests/uat/lib/harness-page-driver.ts` — append driveA29..A31 (+ optional A3X) + EventType import — Plan 03-01..04
|
||||
- [ ] `tests/uat/harness.test.ts` — import + drivers[] push + banner — Plan 03-01..04
|
||||
- [ ] `tests/uat/extension-page-harness.html` — append probe HTML below existing `<h1>` + `<pre id="status">`; preserve tokens.css link — Plan 03-01
|
||||
|
||||
*(No new test framework install required. No new vitest test files anticipated — all new assertions live in the UAT harness tier.)*
|
||||
|
||||
## Security Domain
|
||||
|
||||
> `security_enforcement` is not explicitly set in `.planning/config.json` — treating as default-enabled.
|
||||
|
||||
### Applicable ASVS Categories
|
||||
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
|---------------|---------|-----------------|
|
||||
| V2 Authentication | no | Extension is local-only; no auth surface |
|
||||
| V3 Session Management | no | Local-only; no remote sessions |
|
||||
| V4 Access Control | partial | MV3 permissions baseline (DEC-011 + Amendment 1) — verified via existing `manifest-i18n.test.ts` |
|
||||
| V5 Input Validation | yes | `URL_SCHEME_ALLOW` regex at `src/background/tab-url-tracker.ts:79` validates URL schemes; probe page inputs are not user input from a security boundary perspective |
|
||||
| V6 Cryptography | no | No cryptography in Phase 1 v1 surface |
|
||||
| V7 Error Handling and Logging | partial | Logging via `ContentLogger`; no PII expected (passwords filtered) |
|
||||
| V8 Data Protection | partial | Password filter at `src/content/index.ts:82` is the v1 minimum per D-P3-02; full masking deferred to Phase 4+ if charter reverses |
|
||||
|
||||
### Known Threat Patterns for Phase 3
|
||||
|
||||
| Pattern | STRIDE | Standard Mitigation |
|
||||
|---------|--------|---------------------|
|
||||
| Test-only hook surface leaking to production bundle | Information Disclosure | Tier-1 FORBIDDEN_HOOK_STRINGS gate (12 entries; unit-mirror + UAT A0 mirror); `__MOKOSH_UAT__` Vite define-token tree-shake; verified per Plan 02-04 SUMMARY |
|
||||
| Probe page sentinel value (e.g., "secret-do-not-log-123") leaking via grep mismatch | Information Disclosure | Sentinel is a fixed test constant, not a real secret; absence-assertion proves filter fires; logged to event log triggers explicit RED |
|
||||
| New chrome.* permission grant for probe-page composition | Elevation of Privilege | Phase 3 plans explicitly stay at DEC-011 + Amendment 1 baseline (8 permissions); no new manifest changes |
|
||||
| rrweb data buffer growth past 5000 events / 10min cap during probe | Denial of Service | `cleanupOldEvents` at `src/content/index.ts:27-50` already enforces (oldest-dropped); Plan 03-01 probe HTML interaction is brief (~500ms) and far below the cap |
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- npm registry verified 2026-05-20 via `npm view rrweb dist-tags`: `latest=2.0.0-alpha.4, alpha=2.0.0-alpha.20, beta=1.0.0-beta.2`
|
||||
- @rrweb/types EventType enum verified via `node_modules/@rrweb/types/dist/index.d.ts`: `{DomContentLoaded:0, Load:1, FullSnapshot:2, IncrementalSnapshot:3, Meta:4, Custom:5, Plugin:6}`
|
||||
- Plan 02-04 SUMMARY (`.planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md`) — most recent Approach B harness precedent; 29/29 UAT GREEN
|
||||
- Plan 01-13 SUMMARY — Approach B foundation; FORBIDDEN_HOOK_STRINGS pattern
|
||||
- Plan 01-14 SUMMARY — single-assertion plan precedent (A23); 7-file atomic-commit shape
|
||||
- `src/content/index.ts` — production wiring being verified (read-only for Phase 3)
|
||||
- `src/shared/types.ts:124-131` — UserEvent type contract
|
||||
- `tests/uat/extension-page-harness.ts:2851-3413` — assertA24-A28 implementations (latest pattern)
|
||||
- `tests/uat/lib/harness-page-driver.ts` — driveA* + findLatestZip + JSZip pattern
|
||||
- `tests/background/no-test-hooks-in-prod-bundle.test.ts:108-126` — Tier-1 inventory (12 entries)
|
||||
- 03-CONTEXT.md (D-P3-01..04 locked decisions + canonical_refs)
|
||||
- 03-UI-SPEC.md (null-spec; probe-page conventions; FORBIDDEN_HOOK_STRINGS lockstep guidance)
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- Puppeteer official docs: https://pptr.dev/api/puppeteer.page.metrics (Page.metrics + JSHeapUsedSize semantics)
|
||||
- Puppeteer official docs: https://pptr.dev/guides/chrome-extensions (MV3 SW target acquisition pattern)
|
||||
- MDN: https://developer.mozilla.org/en-US/docs/Web/API/Performance/measureUserAgentSpecificMemory (SW memory measurement requires COOP+COEP — not viable for MV3 extensions)
|
||||
- rrweb master/test/utils.ts (reference for assertDomSnapshot pattern — explicitly rejected for charter mismatch)
|
||||
- W3C Screen Capture spec §6.1 (already cited in Plan 01-14; relevant for §10 #9 if RAM scaffolding research expands)
|
||||
|
||||
### Tertiary (LOW confidence — flagged for plan-time validation)
|
||||
- chrome://memory-internals stability across Chrome stable channel (cited at A7 in Assumptions Log; common operator path but not formally guaranteed)
|
||||
- WebSearch findings on rrweb 2.0 stable release status — cross-verified with npm registry (HIGH); separate WebSearch hit on "rrweb 2.0 production ready" returned an unrelated commercial-fork article, not the canonical rrweb-io repo
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH — all dependencies present in package.json; versions verified via `npm view`
|
||||
- Architecture: HIGH — Approach B template from Plan 02-04 is direct precedent; 3 chained-assertion patterns proven
|
||||
- Pitfalls: HIGH for Pitfalls 1-3 (verified against existing harness + rrweb registry); MEDIUM for Pitfalls 4-6 (research-derived; not yet exercised in this codebase)
|
||||
- RAM ceiling (§10 #9): MEDIUM — Page.metrics scope verified; SW heap measurement caveat verified; chrome://memory-internals operator instructions are standard but not version-stamped
|
||||
- rrweb v2 upgrade safety: HIGH — alpha.4 pin status verified directly via npm dist-tags
|
||||
|
||||
**Research date:** 2026-05-20
|
||||
**Valid until:** 2026-06-20 (30 days; stable dependencies; if rrweb 2.0 stable ships in the interim, re-check D-P3-03 deferral)
|
||||
|
||||
## RESEARCH COMPLETE
|
||||
Reference in New Issue
Block a user