Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -212,7 +212,12 @@ per 2026-05-20 re-phasing.
|
|||||||
5. Background RAM consumption (measured via Chrome Task Manager) does not
|
5. Background RAM consumption (measured via Chrome Task Manager) does not
|
||||||
exceed 50 MB during a sustained recording session (CON-ram-ceiling).
|
exceed 50 MB during a sustained recording session (CON-ram-ceiling).
|
||||||
|
|
||||||
**Plans**: TBD
|
**Plans**: 5 plans (03-01 through 03-05).
|
||||||
|
- [ ] 03-01-PLAN.md — rrweb DOM verification harness extension (A29; SPEC §10 #4; REQ-rrweb-dom-buffer)
|
||||||
|
- [ ] 03-02-PLAN.md — event-log verification harness extension (A30; SPEC §10 #5; REQ-user-event-log)
|
||||||
|
- [ ] 03-03-PLAN.md — §10 #8 password-filter PARTIAL verification (A31; D-P3-02 charter)
|
||||||
|
- [ ] 03-04-PLAN.md — §10 #9 RAM ceiling best-effort + Page.metrics scaffolding (A32; D-P3-04 charter)
|
||||||
|
- [ ] 03-05-PLAN.md — §10 sweep VERIFICATION.md aggregator + REQUIREMENTS/ROADMAP/STATE marker flips
|
||||||
|
|
||||||
### Phase 4: Harden + clean up _(optional)_
|
### Phase 4: Harden + clean up _(optional)_
|
||||||
**Goal**: Eliminate the P1/P2 follow-ups identified in the audit so that the
|
**Goal**: Eliminate the P1/P2 follow-ups identified in the audit so that the
|
||||||
|
|||||||
@@ -0,0 +1,768 @@
|
|||||||
|
---
|
||||||
|
phase: 03
|
||||||
|
slug: spec-10-smoke-verification-dom-event-log-verification
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- tests/uat/extension-page-harness.html
|
||||||
|
- tests/uat/extension-page-harness.ts
|
||||||
|
- tests/uat/lib/harness-page-driver.ts
|
||||||
|
- tests/uat/harness.test.ts
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- REQ-rrweb-dom-buffer
|
||||||
|
tags:
|
||||||
|
- uat-harness
|
||||||
|
- a29
|
||||||
|
- rrweb
|
||||||
|
- spec-10-4
|
||||||
|
- approach-b
|
||||||
|
- probe-html
|
||||||
|
user_setup: []
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "rrweb session.json contains > 0 events after a probe-page interaction"
|
||||||
|
- "rrweb emits at least one Meta event (EventType=4) on session start"
|
||||||
|
- "rrweb emits at least one FullSnapshot (EventType=2) on session start"
|
||||||
|
- "rrweb emits at least one IncrementalSnapshot (EventType=3) after a DOM mutation on the probe page"
|
||||||
|
- "UAT harness exits 0 with 29 + 1 = 30/30 assertions GREEN (A0..A28 baseline preserved + new A29)"
|
||||||
|
artifacts:
|
||||||
|
- path: "tests/uat/extension-page-harness.html"
|
||||||
|
provides: "Probe HTML (form + table + modal trigger + DOM mutation source) appended below existing scaffold; tokens.css link in head untouched"
|
||||||
|
contains: "id=\"probe-form\""
|
||||||
|
- path: "tests/uat/extension-page-harness.ts"
|
||||||
|
provides: "assertA29 page-side stub registered on window.__mokoshHarness"
|
||||||
|
contains: "assertA29"
|
||||||
|
- path: "tests/uat/lib/harness-page-driver.ts"
|
||||||
|
provides: "driveA29 host-side: triggers probe-page DOM mutation, runs setupFreshRecording + SAVE, JSZip-parses rrweb/session.json, EventType enum grep"
|
||||||
|
contains: "driveA29"
|
||||||
|
- path: "tests/uat/harness.test.ts"
|
||||||
|
provides: "driveA29 import + wrapped driver + drivers-array push entry with banner comment"
|
||||||
|
contains: "driveA29"
|
||||||
|
key_links:
|
||||||
|
- from: "tests/uat/harness.test.ts"
|
||||||
|
to: "tests/uat/lib/harness-page-driver.ts driveA29"
|
||||||
|
via: "import + wrapped driver const + drivers-array push"
|
||||||
|
pattern: "driveA29Wrapped"
|
||||||
|
- from: "tests/uat/lib/harness-page-driver.ts driveA29"
|
||||||
|
to: "tests/uat/extension-page-harness.ts assertA29"
|
||||||
|
via: "page.evaluate(() => window.__mokoshHarness.assertA29())"
|
||||||
|
pattern: "harness.assertA29\\(\\)"
|
||||||
|
- from: "tests/uat/lib/harness-page-driver.ts driveA29"
|
||||||
|
to: "@rrweb/types EventType enum"
|
||||||
|
via: "import { EventType } from '@rrweb/types'"
|
||||||
|
pattern: "import.*EventType.*from.*@rrweb/types"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Extend the UAT harness with A29 — empirical verification that SPEC §10 #4
|
||||||
|
(rrweb DOM event recording on typical pages) is satisfied. Production
|
||||||
|
wiring at `src/content/index.ts:284-309` already ships `rrweb.record()` +
|
||||||
|
`maskInputOptions.password=true`; A29 confirms it actually emits the
|
||||||
|
required EventType events on a synthetic probe page (form + table +
|
||||||
|
modal + DOM-mutation trigger).
|
||||||
|
|
||||||
|
Purpose: Closes REQ-rrweb-dom-buffer empirical verification gap. Phase 1
|
||||||
|
shipped the wiring; Phase 3 confirms it works end-to-end through a real
|
||||||
|
Chrome instance + a real probe page.
|
||||||
|
|
||||||
|
Output: 4-check A29 assertion (events.length > 0 + Meta present +
|
||||||
|
FullSnapshot present + IncrementalSnapshot present); UAT count 29 → 30
|
||||||
|
GREEN; probe HTML appended to extension-page-harness.html below the
|
||||||
|
existing `<pre id="status">` scaffold (head unchanged; tokens.css link
|
||||||
|
preserved for A18/A21).
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/REQUIREMENTS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-UI-SPEC.md
|
||||||
|
@.planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md
|
||||||
|
@src/content/index.ts
|
||||||
|
@tests/uat/extension-page-harness.html
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key contracts the executor needs. Extracted from existing code. -->
|
||||||
|
<!-- Do NOT re-read; these are the canonical signatures. -->
|
||||||
|
|
||||||
|
From @rrweb/types/dist/index.d.ts (lines 186-194):
|
||||||
|
```typescript
|
||||||
|
export declare enum EventType {
|
||||||
|
DomContentLoaded = 0,
|
||||||
|
Load = 1,
|
||||||
|
FullSnapshot = 2,
|
||||||
|
IncrementalSnapshot = 3,
|
||||||
|
Meta = 4,
|
||||||
|
Custom = 5,
|
||||||
|
Plugin = 6
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From tests/uat/lib/assertions.ts (lines 25-44):
|
||||||
|
```typescript
|
||||||
|
export interface CheckRecord {
|
||||||
|
readonly name: string;
|
||||||
|
readonly expected: unknown;
|
||||||
|
readonly actual: unknown;
|
||||||
|
readonly passed: boolean;
|
||||||
|
}
|
||||||
|
export interface AssertionRecord {
|
||||||
|
readonly passed: boolean;
|
||||||
|
readonly name: string;
|
||||||
|
readonly checks: ReadonlyArray<CheckRecord>;
|
||||||
|
readonly diagnostics: ReadonlyArray<string>;
|
||||||
|
readonly error?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From tests/uat/extension-page-harness.ts (line 169):
|
||||||
|
```typescript
|
||||||
|
interface AssertionResult {
|
||||||
|
passed: boolean;
|
||||||
|
name: string;
|
||||||
|
checks: Array<{ name: string; expected: unknown; actual: unknown; passed: boolean }>;
|
||||||
|
diagnostics: string[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
function diag(result: AssertionResult, line: string): void; // line 190
|
||||||
|
async function setupFreshRecording(): Promise<{ ok: boolean; error?: string }>; // line 825
|
||||||
|
async function sendMessageWithTimeout<T>(msg: unknown, timeoutMs: number, label: string): Promise<T>; // line 234
|
||||||
|
```
|
||||||
|
|
||||||
|
From tests/uat/lib/harness-page-driver.ts (lines 1395, 41):
|
||||||
|
```typescript
|
||||||
|
function findLatestZip(downloadsDir: string): string | null; // host-side; mtime-sort wins
|
||||||
|
import JSZip from 'jszip'; // host-only, not bundled to page
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/content/index.ts (verifier subject — READ-ONLY):
|
||||||
|
```typescript
|
||||||
|
// Lines 284-311: record({ emit(event){ rrwebEvents.push(event) }, maskInputOptions: { password: true, ... } })
|
||||||
|
// Line 318: chrome.runtime.onMessage 'GET_RRWEB_EVENTS' returns { events, userEvents }
|
||||||
|
// Line 82: setupInputLogging filter — if (target.type === 'password') return;
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/background/index.ts (READ-ONLY):
|
||||||
|
```typescript
|
||||||
|
// GET_RRWEB_EVENTS handler in createArchive: chrome.tabs.sendMessage(activeTabId, { type: 'GET_RRWEB_EVENTS' })
|
||||||
|
// Result lands in rrweb/session.json inside the assembled zip
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
|
||||||
|
# Plan Anchors
|
||||||
|
|
||||||
|
- **Approach B pattern source:** Plan 02-04 driveA26 (canonical host-side
|
||||||
|
JSZip read; chains off prior SAVE) — see `02-04-SUMMARY.md` and
|
||||||
|
`tests/uat/lib/harness-page-driver.ts:1421-1567` for the exact 3-phase
|
||||||
|
shape (page-side stub → findLatestZip → JSZip-parse).
|
||||||
|
- **RESEARCH Pitfall 1 (MUST OBSERVE):** synthetic probe HTML produces
|
||||||
|
`Meta` + `FullSnapshot` on load but NOT `IncrementalSnapshot` unless a
|
||||||
|
DOM mutation happens between page load and SAVE. Plan must inject a
|
||||||
|
mutation pre-SAVE (e.g. click the modal trigger button or input text).
|
||||||
|
- **RESEARCH Pitfall 4 (HARD BAN):** Probe HTML MUST NOT include
|
||||||
|
`<textarea>` — rrweb 2.0.0-alpha.4 issue #1596 leaks textarea values
|
||||||
|
even with `maskInputOptions.textarea` set.
|
||||||
|
- **UI-SPEC ban (HARD):** Probe HTML must NOT modify the `<head>` block.
|
||||||
|
The existing `<link rel="stylesheet" href="../../src/shared/tokens.css">`
|
||||||
|
is load-bearing for A18 + A21. Probe HTML appends below the existing
|
||||||
|
`<pre id="status">` (line 21) and BEFORE the existing `<script>` tag.
|
||||||
|
- **FORBIDDEN_HOOK_STRINGS lockstep (RESEARCH §"Anti-Patterns"):** A29
|
||||||
|
rides production surfaces (rrweb.record + GET_RRWEB_EVENTS bridge +
|
||||||
|
chrome.runtime.sendMessage). Tier-1 inventory stays at **12 entries**.
|
||||||
|
No new `__MOKOSH_UAT__`-gated symbols expected.
|
||||||
|
- **Source audit citation:** Implements REQ-rrweb-dom-buffer per
|
||||||
|
REQUIREMENTS.md lines 54-60 (10-min window + 5000-event cap + DOM
|
||||||
|
capture via rrweb 2.0.0-alpha.4 pin). The 10-min cleanup mechanics
|
||||||
|
are already covered by `src/content/index.ts:27-50` and are NOT
|
||||||
|
exercised by A29 — A29's contract is "rrweb records events without
|
||||||
|
errors on probe pages", which closes §10 #4 literally per CONTEXT
|
||||||
|
D-P3-01.
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 1: Append probe HTML to extension-page-harness.html (form + table + modal)</name>
|
||||||
|
<files>tests/uat/extension-page-harness.html</files>
|
||||||
|
<read_first>
|
||||||
|
- tests/uat/extension-page-harness.html (full file; 24 lines — see exact contents in interfaces above)
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-UI-SPEC.md §"Test Fixture Conventions" (probe HTML rules: NO textarea; NO tokens.css import; NO data-mokosh-* attrs; data-test-* OK)
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md §"Pitfall 1" (DOM mutation required pre-SAVE) and §"Pitfall 4" (textarea ban)
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- The harness page exposes (querySelector-reachable) a form with at
|
||||||
|
least one text input (id="probe-text"), one email input
|
||||||
|
(id="probe-email"), one password input (id="probe-password"),
|
||||||
|
one submit button, and an enclosing form (id="probe-form").
|
||||||
|
- The harness page exposes a small static table with thead + tbody +
|
||||||
|
at least 2 data rows (id="probe-table").
|
||||||
|
- The harness page exposes a modal trigger button
|
||||||
|
(id="probe-modal-trigger") that, when clicked, toggles a hidden
|
||||||
|
div (id="probe-modal") to visible (display:none → display:block).
|
||||||
|
This click is the canonical DOM-mutation source per Pitfall 1.
|
||||||
|
- The existing tokens.css link in `<head>` is NOT modified.
|
||||||
|
- The existing `<h1>`, two `<p>` lines, and `<pre id="status">` are
|
||||||
|
NOT modified. The existing `<script type="module">` is NOT moved.
|
||||||
|
- The probe HTML appears between line 21 (`<pre id="status">`) and
|
||||||
|
line 22 (`<script type="module">`) in the source — appended to
|
||||||
|
the body after the existing scaffold, before the script.
|
||||||
|
- No `<textarea>` element anywhere in the file (rrweb alpha.4
|
||||||
|
issue #1596).
|
||||||
|
- No `<link>` or `<style>` additions referencing tokens.css.
|
||||||
|
- No `data-mokosh-slot`, `data-mokosh-key`, or `data-mokosh-i18n-key`
|
||||||
|
attributes (reserved for production welcome page).
|
||||||
|
- The probe HTML uses inline minimal styles (`style="display:none"`
|
||||||
|
on the modal); no `<style>` block.
|
||||||
|
- On the modal trigger click, `document.getElementById('probe-modal')`
|
||||||
|
toggles its `style.display` between 'none' and 'block' via a
|
||||||
|
small inline `onclick` handler. (rrweb records the attribute
|
||||||
|
mutation, satisfying IncrementalSnapshot emission.)
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Read the current `tests/uat/extension-page-harness.html` (24 lines, contents shown in interfaces).
|
||||||
|
2. Open the file for editing. Locate the line ` <pre id="status">Ready.</pre>` (line 21) and the next line ` <script type="module" src="./extension-page-harness.ts"></script>` (line 22).
|
||||||
|
3. Insert the following probe HTML block BETWEEN line 21 and line 22 (preserving the trailing newline structure):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!--
|
||||||
|
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: NO <textarea> (rrweb
|
||||||
|
2.0.0-alpha.4 issue #1596 leaks textarea values even with
|
||||||
|
maskInputOptions.textarea set). 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>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Verify no `<textarea>` was introduced (grep should return 0 hits on the modified file).
|
||||||
|
5. Verify the `<head>` block is unchanged.
|
||||||
|
6. No new dependencies, no other file edits in this task.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -c "id=\"probe-form\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' && grep -c "id=\"probe-modal-trigger\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' && grep -c "id=\"probe-table\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' && ! grep -q "textarea" tests/uat/extension-page-harness.html && grep -q '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `grep -c 'id="probe-form"' tests/uat/extension-page-harness.html` returns exactly `1`.
|
||||||
|
- `grep -c 'id="probe-modal-trigger"' tests/uat/extension-page-harness.html` returns exactly `1`.
|
||||||
|
- `grep -c 'id="probe-table"' tests/uat/extension-page-harness.html` returns exactly `1`.
|
||||||
|
- `grep -c 'id="probe-password"' tests/uat/extension-page-harness.html` returns exactly `1` (carried into Plan 03-03).
|
||||||
|
- `grep -c 'textarea' tests/uat/extension-page-harness.html` returns `0`.
|
||||||
|
- `grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html` returns exactly `1` (head unchanged).
|
||||||
|
- `grep -c 'data-mokosh-slot\|data-mokosh-key\|data-mokosh-i18n-key' tests/uat/extension-page-harness.html` returns `0`.
|
||||||
|
- `npm run build` exits 0.
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
Probe HTML lives in the harness page below the existing scaffold;
|
||||||
|
head + script tag + existing body content all unchanged; no
|
||||||
|
textarea; ready for Task 2 to wire assertA29.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 2: Add assertA29 page-side stub + driveA29 host-side + harness.test.ts orchestrator wiring</name>
|
||||||
|
<files>tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
|
||||||
|
<read_first>
|
||||||
|
- tests/uat/extension-page-harness.ts lines 3151-3413 (assertA26 stub + assertA27 multi-tab + assertA28 stub + declare global block + window.__mokoshHarness object literal — pattern for new assertion)
|
||||||
|
- tests/uat/lib/harness-page-driver.ts lines 1395-1567 (findLatestZip + driveA26 3-phase pattern; canonical for host-side JSZip parse + EventType-style structural assertions)
|
||||||
|
- tests/uat/lib/harness-page-driver.ts lines 36-46 (existing imports + JSZip + AssertionRecord/CheckRecord)
|
||||||
|
- tests/uat/harness.test.ts lines 65-130 (existing driver imports + FORBIDDEN_HOOK_STRINGS at 12 entries)
|
||||||
|
- tests/uat/harness.test.ts lines 316-431 (existing wrapped-driver consts + drivers-array push pattern)
|
||||||
|
- node_modules/@rrweb/types/dist/index.d.ts lines 186-194 (EventType enum values; confirmed in interfaces above)
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md §"EventType enum grep pattern (Plan 03-01)" and §"Page-side stub pattern" (canonical implementation)
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
Page-side (`tests/uat/extension-page-harness.ts`):
|
||||||
|
- Adds module-local constants `A29_SEGMENT_SETTLE_MS = 11_000`
|
||||||
|
(matches A24/A25/A27 settle window).
|
||||||
|
- Adds `async function assertA29(): Promise<AssertionResult>` that
|
||||||
|
- dispatches a programmatic DOM mutation on the probe HTML:
|
||||||
|
- sets `#probe-text.value = 'probe'` and dispatches `Event('input', { bubbles: true })`
|
||||||
|
- clicks `#probe-modal-trigger` (toggles `#probe-modal` style.display)
|
||||||
|
- waits 500 ms for rrweb to enqueue the IncrementalSnapshot
|
||||||
|
- calls `setupFreshRecording()` then waits `A29_SEGMENT_SETTLE_MS`
|
||||||
|
- dispatches `SAVE_ARCHIVE` via `sendMessageWithTimeout` (15s timeout, label 'SAVE_ARCHIVE (A29)')
|
||||||
|
- pushes `A29.1: SAVE_ARCHIVE ack success===true` to checks
|
||||||
|
- returns the `AssertionResult` (page-side computes ONLY the SAVE
|
||||||
|
ack; the EventType-enum-shape checks live host-side in driveA29
|
||||||
|
because @rrweb/types isn't bundled to the page).
|
||||||
|
- Adds `assertA29: () => Promise<AssertionResult>;` to the
|
||||||
|
`declare global { interface Window { __mokoshHarness: { ... } } }`
|
||||||
|
block (preserve existing entries; append after assertA28).
|
||||||
|
- Adds `assertA29,` to the `window.__mokoshHarness = { ... }`
|
||||||
|
object literal (preserve existing entries; append after assertA28).
|
||||||
|
- Update the closing console.log message line (currently at
|
||||||
|
line 3411) to mention `+ Plan 03-01: A29`.
|
||||||
|
|
||||||
|
Host-side (`tests/uat/lib/harness-page-driver.ts`):
|
||||||
|
- Adds `import { EventType } from '@rrweb/types';` at the top of
|
||||||
|
the imports section (after the JSZip import on line 41).
|
||||||
|
- Adds `export async function driveA29(page: Page, downloadsDir: string): Promise<AssertionRecord>` modeled on driveA26 (lines 1421-1567):
|
||||||
|
- Phase 1 — page.evaluate `harness.assertA29()` → `pageResult`
|
||||||
|
- Phase 2 — `findLatestZip(downloadsDir)`; if null push
|
||||||
|
`A29.0` failure and return
|
||||||
|
- Phase 3 — JSZip.loadAsync(buf); read `rrweb/session.json`
|
||||||
|
entry; if absent push failed `A29.0a: rrweb/session.json
|
||||||
|
present` check + return
|
||||||
|
- Parse the JSON as `Array<{ type: number; timestamp: number }>`.
|
||||||
|
- Push 4 checks (names verbatim):
|
||||||
|
- `A29.1: rrweb/session.json contains > 0 events` —
|
||||||
|
expected `>0`, actual `events.length`, passed `events.length > 0`
|
||||||
|
- `A29.2: rrweb emitted at least one Meta event (EventType.Meta=4)` —
|
||||||
|
expected `has Meta`, actual `events.some((e) => e.type === EventType.Meta)`, passed same
|
||||||
|
- `A29.3: rrweb emitted at least one FullSnapshot (EventType.FullSnapshot=2)` —
|
||||||
|
expected `has FullSnapshot`, actual `events.some((e) => e.type === EventType.FullSnapshot)`, passed same
|
||||||
|
- `A29.4: rrweb emitted at least one IncrementalSnapshot (EventType.IncrementalSnapshot=3)` —
|
||||||
|
expected `has IncrementalSnapshot`, actual `events.some((e) => e.type === EventType.IncrementalSnapshot)`, passed same
|
||||||
|
- Merge with `pageResult.checks` (carries A29.1 SAVE ack already);
|
||||||
|
recompute `mergedPassed = mergedChecks.every((c) => c.passed)`.
|
||||||
|
- Push diagnostic `A29 zipPath=${zipPath}`,
|
||||||
|
`A29 events.length=${events.length}`, and
|
||||||
|
`A29 event types: ${[...new Set(events.map((e) => e.type))].sort().join(',')}`.
|
||||||
|
|
||||||
|
Orchestrator (`tests/uat/harness.test.ts`):
|
||||||
|
- Adds `driveA29,` to the import block from `./lib/harness-page-driver` (after `driveA28,`).
|
||||||
|
- Adds `const driveA29Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> = (page) => driveA29(page, handles.downloadsDir);` after the existing `driveA28Wrapped` const (mirrors A26/A28 host-side-needs-downloadsDir pattern).
|
||||||
|
- Adds `{ name: 'A29', drive: driveA29Wrapped },` to the `drivers` array after the A28 entry, with a banner comment block citing Plan 03-01 + SPEC §10 #4 + the chaining strategy (A29 owns its SAVE; injects DOM mutation pre-SAVE per RESEARCH Pitfall 1).
|
||||||
|
- FORBIDDEN_HOOK_STRINGS array stays at 12 entries (no new `__MOKOSH_UAT__`-gated symbols).
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Open `tests/uat/extension-page-harness.ts`. Locate the `assertA28` stub (currently around lines 3294-3316) and the `getManifestVersion` declaration block following it.
|
||||||
|
2. Insert the new `assertA29` block AFTER `assertA28` (and AFTER its end-of-file comments / before `getManifestVersion`). Use this concrete code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/* ─── Plan 03-01 Task 2 — A29 (rrweb DOM verification; SPEC §10 #4) ─
|
||||||
|
*
|
||||||
|
* A29 — REQ-rrweb-dom-buffer empirical: rrweb's record() (already
|
||||||
|
* wired at src/content/index.ts:285) emits Meta + FullSnapshot
|
||||||
|
* + at least one IncrementalSnapshot when the harness page
|
||||||
|
* contains the probe HTML (form + table + modal) AND the driver
|
||||||
|
* injects a DOM mutation before SAVE (RESEARCH Pitfall 1:
|
||||||
|
* static probe HTML emits Meta + FullSnapshot but not
|
||||||
|
* IncrementalSnapshot without mutation).
|
||||||
|
*
|
||||||
|
* Page side: dispatch the probe-page DOM mutations (input value +
|
||||||
|
* modal toggle), settle, setupFreshRecording, settle one segment,
|
||||||
|
* dispatch SAVE_ARCHIVE, push A29.1 ack check. The host-side driveA29
|
||||||
|
* does the EventType-enum-shape grep against rrweb/session.json from
|
||||||
|
* the assembled zip (matches A26's chained-assertion pattern; JSZip
|
||||||
|
* + @rrweb/types are host-only deps).
|
||||||
|
*
|
||||||
|
* FORBIDDEN_HOOK_STRINGS impact: NONE. A29 rides production rrweb
|
||||||
|
* wiring (record() at src/content/index.ts:285 + GET_RRWEB_EVENTS
|
||||||
|
* round-trip at src/background/index.ts → src/content/index.ts:318)
|
||||||
|
* + the existing setupFreshRecording / sendMessageWithTimeout
|
||||||
|
* helpers. Tier-1 inventory stays at 12 entries.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** SAVE_ARCHIVE dispatch timeout for A29 — matches A24/A25/A27. */
|
||||||
|
const A29_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||||
|
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
|
||||||
|
const A29_SEGMENT_SETTLE_MS = 11_000;
|
||||||
|
/** Settle window between DOM mutation and SAVE so rrweb's
|
||||||
|
* IncrementalSnapshot lands in the in-memory buffer before
|
||||||
|
* GET_RRWEB_EVENTS fires. */
|
||||||
|
const A29_MUTATION_SETTLE_MS = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A29 — rrweb DOM event recording empirical (SPEC §10 #4).
|
||||||
|
*
|
||||||
|
* Page-side dispatches the probe-page mutation (input.value + modal
|
||||||
|
* toggle), settles, runs setupFreshRecording + segment-settle, then
|
||||||
|
* dispatches SAVE_ARCHIVE. Only the SAVE ack lives in the
|
||||||
|
* page-side checks; the EventType-enum-shape grep is host-side
|
||||||
|
* because @rrweb/types is host-only.
|
||||||
|
*
|
||||||
|
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
|
||||||
|
* driveA29 appends A29.0/A29.1..A29.4 EventType checks.
|
||||||
|
*/
|
||||||
|
async function assertA29(): Promise<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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. In the SAME file, locate the `declare global { interface Window { __mokoshHarness: { ... } } }` block (around lines 3332-3372). Inside the type literal, AFTER the existing `assertA28: () => Promise<AssertionResult>;` entry and BEFORE `getManifestVersion`, insert:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-01 — rrweb DOM verification (SPEC §10 #4)
|
||||||
|
assertA29: () => Promise<AssertionResult>;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Locate the `window.__mokoshHarness = { ... }` object literal (around lines 3374-3404). After `assertA28,` and before `getManifestVersion,` insert:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
assertA29,
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Locate the trailing `console.log('[harness-page] ready — ...')` line (around line 3411) and replace the message to append `+ Plan 03-01: A29`. Concrete new line:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + getManifestVersion)');
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Open `tests/uat/lib/harness-page-driver.ts`. In the import block (after the `import JSZip from 'jszip';` line — around line 41), add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EventType } from '@rrweb/types';
|
||||||
|
```
|
||||||
|
|
||||||
|
7. At the end of the file (after the existing `driveA28` export), append the new `driveA29` export:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
|
||||||
|
/* ─── Plan 03-01 — driveA29 (rrweb DOM verification host-side) ─────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A29 (Plan 03-01 — SPEC §10 #4 / REQ-rrweb-dom-buffer).
|
||||||
|
*
|
||||||
|
* Three-phase orchestration:
|
||||||
|
* 1. Page-side assertA29 dispatches the probe DOM mutations, settles,
|
||||||
|
* runs setupFreshRecording, settles a segment, dispatches SAVE.
|
||||||
|
* Returns AssertionRecord with A29.1 (SAVE ack) only.
|
||||||
|
* 2. Host-side findLatestZip picks the just-produced zip (mtime-sort
|
||||||
|
* wins; race-free per Plan 02-04 precedent — assertA29 awaits the
|
||||||
|
* SAVE ack before returning so the zip has landed by here).
|
||||||
|
* 3. Host-side JSZip-parse rrweb/session.json + EventType-enum grep.
|
||||||
|
*
|
||||||
|
* Checks appended host-side:
|
||||||
|
* - A29.0a: rrweb/session.json entry exists in zip
|
||||||
|
* - A29.1 (already from page side): SAVE_ARCHIVE ack success
|
||||||
|
* - A29.2: events.length > 0
|
||||||
|
* - A29.3: events.some(e => e.type === EventType.Meta)
|
||||||
|
* - A29.4: events.some(e => e.type === EventType.FullSnapshot)
|
||||||
|
* - A29.5: events.some(e => e.type === EventType.IncrementalSnapshot)
|
||||||
|
*
|
||||||
|
* RESEARCH Pitfall 1 mitigation: the page-side dispatches a real DOM
|
||||||
|
* mutation (input value + modal toggle) BEFORE SAVE so the
|
||||||
|
* IncrementalSnapshot check (A29.5) has actual content to find.
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @param downloadsDir - Absolute path to the per-run downloads directory.
|
||||||
|
* @returns AssertionRecord with 6 merged checks (A29.0a + A29.1..A29.5).
|
||||||
|
*/
|
||||||
|
export async function driveA29(
|
||||||
|
page: Page,
|
||||||
|
downloadsDir: string,
|
||||||
|
): Promise<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 mergedPassed = mergedChecks.every((c) => c.passed);
|
||||||
|
return {
|
||||||
|
passed: mergedPassed,
|
||||||
|
name: pageResult.name,
|
||||||
|
checks: mergedChecks,
|
||||||
|
diagnostics: mergedDiagnostics,
|
||||||
|
error: pageResult.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Open `tests/uat/harness.test.ts`. In the import block from `./lib/harness-page-driver` (around lines 65-101), AFTER `driveA28,` and BEFORE `getManifestVersion,`, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-01 — rrweb DOM verification (SPEC §10 #4 / REQ-rrweb-dom-buffer)
|
||||||
|
driveA29,
|
||||||
|
```
|
||||||
|
|
||||||
|
9. In the same file, locate the existing `driveA28Wrapped` const (around lines 330-335). AFTER it, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-01 — driveA29 needs downloadsDir for host-side JSZip parse
|
||||||
|
// of rrweb/session.json from the just-produced zip.
|
||||||
|
const driveA29Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||||
|
(page) => driveA29(page, handles.downloadsDir);
|
||||||
|
```
|
||||||
|
|
||||||
|
10. In the `drivers` array (around lines 337-431), AFTER the existing `{ name: 'A28', drive: driveA28Wrapped },` entry, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-01 A29: rrweb DOM verification (SPEC §10 #4).
|
||||||
|
// A29 owns its SAVE because the probe-page DOM mutation must
|
||||||
|
// happen between page load and SAVE so rrweb's IncrementalSnapshot
|
||||||
|
// fires (RESEARCH Pitfall 1). Host-side driveA29 JSZip-parses
|
||||||
|
// rrweb/session.json and asserts the EventType enum surfaces
|
||||||
|
// (Meta=4, FullSnapshot=2, IncrementalSnapshot=3) are present.
|
||||||
|
{ name: 'A29', drive: driveA29Wrapped },
|
||||||
|
```
|
||||||
|
|
||||||
|
11. Update the orchestrator banner string (currently at line 268: `'Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28)'`) to append `, A29`. Concrete replacement string for line 268:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29)\n');
|
||||||
|
```
|
||||||
|
|
||||||
|
12. Run `npx tsc --noEmit` to confirm no type errors. Expected: clean.
|
||||||
|
13. Run `SKIP_PROD_REBUILD=0 HEADLESS=1 npm run test:uat` (full rebuild + harness). Expected: `30/30 GREEN` (29 prior + A29).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>npx tsc --noEmit && grep -c "assertA29" tests/uat/extension-page-harness.ts | grep -v '^#' | awk '$1 >= 3' && grep -c "driveA29" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | awk '$1 >= 2' && grep -c "driveA29" tests/uat/harness.test.ts | grep -v '^#' | awk '$1 >= 3' && grep -c "from '@rrweb/types'" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | grep -q '^1$' && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `grep -c 'assertA29' tests/uat/extension-page-harness.ts` returns >=3 (function definition + declare global entry + object literal entry).
|
||||||
|
- `grep -c 'driveA29' tests/uat/lib/harness-page-driver.ts` returns >=2 (function definition + docstring or signature mention).
|
||||||
|
- `grep -c 'driveA29' tests/uat/harness.test.ts` returns >=3 (import line + wrapped const + drivers array push).
|
||||||
|
- `grep -c "from '@rrweb/types'" tests/uat/lib/harness-page-driver.ts` returns exactly 1.
|
||||||
|
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with stdout containing `UAT harness: 30/30 assertions passed`.
|
||||||
|
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (Tier-1 inventory stays at 12 entries; verifies no new hook leaks).
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
UAT harness runs 30/30 GREEN with A29 covering SPEC §10 #4 end-to-end:
|
||||||
|
rrweb session.json from the assembled zip contains > 0 events with
|
||||||
|
Meta + FullSnapshot + IncrementalSnapshot EventType-enum members
|
||||||
|
all present. FORBIDDEN_HOOK_STRINGS unchanged at 12.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Puppeteer host ↔ page realm | Test harness drives the page via page.evaluate; no production data flows out |
|
||||||
|
| Page realm ↔ content script | Existing GET_RRWEB_EVENTS round-trip via chrome.runtime.sendMessage |
|
||||||
|
| Content script ↔ SW | Existing chrome.tabs.sendMessage path (read-only for Plan 03-01) |
|
||||||
|
| dist-test/ ↔ dist/ | Two-bundle separation: Plan 03-01 adds NO test-only symbols; production bundle invariant unchanged |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-03-01-01 | Information Disclosure | Probe HTML password input visible in DOM | accept | Sentinel value is not introduced by Plan 03-01 (Plan 03-03 types the negative-assertion sentinel). Plan 03-01 leaves the password input empty; rrweb's existing maskInputOptions.password=true at src/content/index.ts:306 masks any value that lands. |
|
||||||
|
| T-03-01-02 | Tampering | New probe HTML elements interfere with A18/A21 token-resolution checks | mitigate | UI-SPEC ban enforced: probe HTML appends BELOW existing scaffold; head + tokens.css link untouched; probe sub-tree uses no var(--mks-*) tokens; verification grep at task acceptance proves the existing link line is preserved. |
|
||||||
|
| T-03-01-03 | Information Disclosure | Test-only hook surface leaking to production bundle | mitigate | A29 rides production rrweb wiring + existing GET_RRWEB_EVENTS bridge + sendMessageWithTimeout helper. FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries; Tier-1 unit-test gate runs as part of Task 2 acceptance. |
|
||||||
|
| T-03-01-04 | Denial of Service | rrweb cleanupOldEvents fires mid-A29 + drops the IncrementalSnapshot before SAVE | accept | A29 settles 500ms after mutation + 11s segment-settle + SAVE. CLEANUP_INTERVAL_MS=60s + retention=10min at src/content/index.ts:16-18 — A29's wall-clock <12s budget is far below the cleanup window; mutation event safely retained. |
|
||||||
|
|
||||||
|
No new production surface; threat surface unchanged from Phase 2
|
||||||
|
(CON-manifest-permissions intact including DEC-011 Amendment 1 tabs
|
||||||
|
permission). UAT harness extension is test-only and tree-shakes from
|
||||||
|
production bundle via the existing `__MOKOSH_UAT__` Vite-define-token —
|
||||||
|
A29 introduces no new `__MOKOSH_UAT__`-gated symbols.
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 30/30 GREEN.
|
||||||
|
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 FORBIDDEN_HOOK_STRINGS × 0 hits each).
|
||||||
|
- `grep -c '<textarea' tests/uat/extension-page-harness.html` returns 0.
|
||||||
|
- `grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html` returns 1.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- A29 GREEN in UAT harness across 6 host-side checks (A29.0a + A29.1..A29.5).
|
||||||
|
- Probe HTML committed to harness page in correct location; head + script tag preserved.
|
||||||
|
- FORBIDDEN_HOOK_STRINGS unchanged at 12 entries.
|
||||||
|
- vitest baseline preserved (171/171 GREEN; SKIP_BUILD=1 path).
|
||||||
|
- REQ-rrweb-dom-buffer empirically verified end-to-end through rrweb's
|
||||||
|
`record()` wiring + GET_RRWEB_EVENTS bridge + the assembled zip's
|
||||||
|
rrweb/session.json content.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create
|
||||||
|
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md`
|
||||||
|
documenting:
|
||||||
|
- 4-check A29 contract verified (Meta + FullSnapshot + IncrementalSnapshot + count > 0)
|
||||||
|
- Probe HTML composition rationale (form + table + modal; NO textarea)
|
||||||
|
- DOM-mutation injection pattern (input value + modal click)
|
||||||
|
- RESEARCH Pitfall 1 mitigation confirmed empirically
|
||||||
|
- UAT 29 → 30 GREEN; Tier-1 inventory unchanged at 12
|
||||||
|
- Plan 03-02 wave dependency: SAME 3 harness files; SEQUENTIAL within Wave 2 per RESEARCH Pitfall 6
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,680 @@
|
|||||||
|
---
|
||||||
|
phase: 03
|
||||||
|
slug: spec-10-smoke-verification-dom-event-log-verification
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on:
|
||||||
|
- 01
|
||||||
|
files_modified:
|
||||||
|
- tests/uat/extension-page-harness.ts
|
||||||
|
- tests/uat/lib/harness-page-driver.ts
|
||||||
|
- tests/uat/harness.test.ts
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- REQ-user-event-log
|
||||||
|
tags:
|
||||||
|
- uat-harness
|
||||||
|
- a30
|
||||||
|
- event-log
|
||||||
|
- spec-10-5
|
||||||
|
- approach-b
|
||||||
|
- user-event
|
||||||
|
user_setup: []
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "logs/events.json from the assembled zip contains at least one 'click' UserEvent"
|
||||||
|
- "logs/events.json contains at least one 'input' UserEvent"
|
||||||
|
- "logs/events.json contains at least one 'navigation' UserEvent"
|
||||||
|
- "logs/events.json contains at least one 'js_error' UserEvent"
|
||||||
|
- "logs/events.json contains at least one 'network_error' UserEvent"
|
||||||
|
- "UAT harness exits 0 with 30 + 1 = 31/31 assertions GREEN (A29 baseline preserved + new A30)"
|
||||||
|
artifacts:
|
||||||
|
- path: "tests/uat/extension-page-harness.ts"
|
||||||
|
provides: "assertA30 page-side orchestrator: triggers 5 event types (click/input/navigation/js_error/network_error), runs setupFreshRecording + SAVE; registered on window.__mokoshHarness"
|
||||||
|
contains: "assertA30"
|
||||||
|
- path: "tests/uat/lib/harness-page-driver.ts"
|
||||||
|
provides: "driveA30 host-side: JSZip-parse logs/events.json + UserEvent type grep across all 5 event-types"
|
||||||
|
contains: "driveA30"
|
||||||
|
- path: "tests/uat/harness.test.ts"
|
||||||
|
provides: "driveA30 import + wrapped driver + drivers-array push entry"
|
||||||
|
contains: "driveA30"
|
||||||
|
key_links:
|
||||||
|
- from: "tests/uat/lib/harness-page-driver.ts driveA30"
|
||||||
|
to: "tests/uat/extension-page-harness.ts assertA30"
|
||||||
|
via: "page.evaluate(() => window.__mokoshHarness.assertA30())"
|
||||||
|
pattern: "harness.assertA30\\(\\)"
|
||||||
|
- from: "tests/uat/lib/harness-page-driver.ts driveA30"
|
||||||
|
to: "src/shared/types.ts UserEvent"
|
||||||
|
via: "import type { UserEvent } from '../../../src/shared/types'"
|
||||||
|
pattern: "import type \\{ UserEvent \\}"
|
||||||
|
- from: "tests/uat/extension-page-harness.ts assertA30"
|
||||||
|
to: "src/content/index.ts (production event-log wiring)"
|
||||||
|
via: "synthetic browser events trigger production click/input/navigation/error/fetch listeners"
|
||||||
|
pattern: "addUserEvent\\("
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Extend the UAT harness with A30 — empirical verification that SPEC §10 #5
|
||||||
|
(event log captures clicks, navigation, network errors, plus the input
|
||||||
|
and js_error types per CON-event-log-schema) is satisfied. Production
|
||||||
|
wiring at `src/content/index.ts:60-237` already ships listeners for all
|
||||||
|
5 UserEvent types; A30 confirms they fire correctly and land in
|
||||||
|
`logs/events.json` of the assembled archive.
|
||||||
|
|
||||||
|
Purpose: Closes REQ-user-event-log empirical verification gap. Phase 1
|
||||||
|
shipped the wiring; Phase 3 confirms all 5 types are captured during a
|
||||||
|
synthetic event-injection drive.
|
||||||
|
|
||||||
|
Output: A30 assertion with 6 host-side checks (SAVE ack + presence of
|
||||||
|
each of 5 event types in logs/events.json); UAT count 30 → 31 GREEN.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/REQUIREMENTS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md
|
||||||
|
@src/content/index.ts
|
||||||
|
@src/shared/types.ts
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key contracts the executor needs. Extracted from existing code. -->
|
||||||
|
|
||||||
|
From src/shared/types.ts (lines 124-131):
|
||||||
|
```typescript
|
||||||
|
export interface UserEvent {
|
||||||
|
timestamp: number;
|
||||||
|
type: 'click' | 'input' | 'navigation' | 'js_error' | 'network_error';
|
||||||
|
target: string;
|
||||||
|
value?: string;
|
||||||
|
url: string;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/content/index.ts (READ-ONLY; verifier subject):
|
||||||
|
```typescript
|
||||||
|
// setupClickLogging at line 61: document.addEventListener('click', ...)
|
||||||
|
// setupInputLogging at line 77: document.addEventListener('input', ...); password filter at line 82
|
||||||
|
// setupNavigationLogging at line 99: popstate + hashchange + pushState/replaceState intercept
|
||||||
|
// setupErrorLogging at line 133: window.addEventListener('error', ...) + unhandledrejection
|
||||||
|
// setupNetworkLogging at line 164: fetch interception (response.ok === false) + XHR loadend (status >= 400)
|
||||||
|
// All push to userEvents[]; GET_RRWEB_EVENTS handler at line 318 returns events + userEvents
|
||||||
|
```
|
||||||
|
|
||||||
|
From tests/uat/extension-page-harness.ts (existing helpers):
|
||||||
|
```typescript
|
||||||
|
function diag(result: AssertionResult, line: string): void;
|
||||||
|
async function setupFreshRecording(): Promise<{ ok: boolean; error?: string }>;
|
||||||
|
async function sendMessageWithTimeout<T>(msg: unknown, timeoutMs: number, label: string): Promise<T>;
|
||||||
|
// Pattern for new assertions: A29 from Plan 03-01 (precedent — same file).
|
||||||
|
```
|
||||||
|
|
||||||
|
From tests/uat/lib/harness-page-driver.ts:
|
||||||
|
```typescript
|
||||||
|
function findLatestZip(downloadsDir: string): string | null;
|
||||||
|
// JSZip + readFileSync host-side; no chrome.* access
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
|
||||||
|
# Plan Anchors
|
||||||
|
|
||||||
|
- **Sequential wave assignment (per RESEARCH Pitfall 6 + file-overlap rule):** Plan 03-02 lives in wave 2
|
||||||
|
modifies the SAME 3 harness files as Plan 03-01. depends_on: [01]
|
||||||
|
enforces sequential ordering — Plan 03-02 runs AFTER 03-01 commits.
|
||||||
|
- **Production wiring is in the content script — NOT the harness page.**
|
||||||
|
src/content/index.ts attaches listeners to `document` and `window`
|
||||||
|
via content-script injection into the active tab. The harness page
|
||||||
|
(chrome-extension://.../extension-page-harness.html) is an extension-
|
||||||
|
internal page where the content script is also injected (per
|
||||||
|
manifest content_scripts `<all_urls>`-equivalent). Therefore
|
||||||
|
synthetic events dispatched ON the harness page from
|
||||||
|
`page.evaluate(...)` reach the production listeners and produce real
|
||||||
|
UserEvent entries.
|
||||||
|
- **5 event types must each fire at least once:** click + input +
|
||||||
|
navigation + js_error + network_error (per CON-event-log-schema +
|
||||||
|
REQUIREMENTS.md REQ-user-event-log lines 65-72).
|
||||||
|
- **Network error trigger via fetch to a known-404:** Plan 02-04 A27
|
||||||
|
uses `https://example.com` as a stable harness fixture. fetch to
|
||||||
|
`https://example.com/this-path-does-not-exist-404-probe-a30` is a
|
||||||
|
reliable network_error trigger (404 surfaces). RESEARCH Pitfall 3
|
||||||
|
warning: USE https:// only (URL_SCHEME_ALLOW regex). example.com
|
||||||
|
is the safe choice.
|
||||||
|
- **Navigation trigger via pushState:** the production interceptor at
|
||||||
|
src/content/index.ts:114-129 wraps history.pushState/replaceState
|
||||||
|
and dispatches navigation events. A30 fires
|
||||||
|
`history.pushState({}, '', window.location.pathname + '#a30-probe')`
|
||||||
|
which routes through the wrapper → navigation event.
|
||||||
|
- **js_error trigger via window.dispatchEvent(new ErrorEvent('error'))**
|
||||||
|
is the canonical synthetic trigger; production at line 133 listens
|
||||||
|
for `window` 'error' events.
|
||||||
|
- **input trigger:** keep separate from Plan 03-01 mutation (Plan
|
||||||
|
03-01's `#probe-text` value="probe" already fires the input event
|
||||||
|
via dispatchEvent at line 80, but A30 runs in its own fresh
|
||||||
|
recording cycle after A29 + setupFreshRecording, so events are
|
||||||
|
separated by recording window). A30 dispatches a NEW input event
|
||||||
|
on `#probe-email` with value 'a30@probe.local' to be self-contained.
|
||||||
|
- **click trigger:** dispatch a synthetic click on `#probe-submit` via
|
||||||
|
`.click()`. The production click listener at line 61 captures it
|
||||||
|
regardless of whether the form submits (form submit event is a
|
||||||
|
separate listener not in scope).
|
||||||
|
- **All triggers happen ON the harness page (window/document of the
|
||||||
|
harness page).** No new tab is opened; chrome.tabs.create is NOT
|
||||||
|
needed. This avoids the Plan 02-04 A27 multi-tab complexity and
|
||||||
|
the chrome-extension://-tab quirks.
|
||||||
|
- **FORBIDDEN_HOOK_STRINGS lockstep:** A30 rides production listeners
|
||||||
|
+ existing setupFreshRecording / sendMessageWithTimeout helpers.
|
||||||
|
Tier-1 inventory stays at 12 entries.
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 1: Add assertA30 page-side orchestrator (5 event triggers + SAVE)</name>
|
||||||
|
<files>tests/uat/extension-page-harness.ts</files>
|
||||||
|
<read_first>
|
||||||
|
- tests/uat/extension-page-harness.ts lines 3151-3413 (assertA28 stub + declare global block + window.__mokoshHarness object literal — the canonical extension shape Plan 03-01 just used for assertA29)
|
||||||
|
- tests/uat/extension-page-harness.ts where assertA29 was JUST added by Plan 03-01 (study its shape — same module; new assertion appends after it)
|
||||||
|
- src/content/index.ts lines 60-237 (production listener wiring — what synthetic events must trigger)
|
||||||
|
- src/shared/types.ts lines 124-131 (UserEvent type with 5-literal union)
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Adds module-local constants:
|
||||||
|
- `A30_SAVE_ARCHIVE_TIMEOUT_MS = 15_000`
|
||||||
|
- `A30_SEGMENT_SETTLE_MS = 11_000`
|
||||||
|
- `A30_TRIGGER_SETTLE_MS = 500` (wait between trigger and SAVE)
|
||||||
|
- `A30_404_PROBE_URL = 'https://example.com/this-path-does-not-exist-404-probe-a30'`
|
||||||
|
- Adds `async function assertA30(): Promise<AssertionResult>` that:
|
||||||
|
- Step 0: `setupFreshRecording()` (clean event-log window; previous events from A29 won't count)
|
||||||
|
- Step 1: settle `A30_SEGMENT_SETTLE_MS` so a segment lands (matches other assertions; not strictly required for event log but keeps the run consistent)
|
||||||
|
- Step 2: click trigger — `(document.querySelector('#probe-submit') as HTMLButtonElement | null)?.click()` (use safe-nav; assertion proceeds even if the element is gone)
|
||||||
|
- Step 3: input trigger — set `#probe-email.value = 'a30@probe.local'` + dispatch `Event('input', { bubbles: true })`
|
||||||
|
- Step 4: navigation trigger — `history.pushState({}, '', window.location.pathname + '#a30-probe')` (production wrapper at src/content/index.ts:114-129 fires navigation event)
|
||||||
|
- Step 5: js_error trigger — `window.dispatchEvent(new ErrorEvent('error', { message: 'a30-probe-js-error', filename: 'a30', lineno: 1, colno: 1 }))`
|
||||||
|
- Step 6: network_error trigger — `await fetch(A30_404_PROBE_URL).catch(() => undefined)` (production fetch interception at line 167 fires network_error on response.ok===false)
|
||||||
|
- Step 7: settle `A30_TRIGGER_SETTLE_MS` so all event handlers complete and entries land in userEvents[]
|
||||||
|
- Step 8: dispatch `SAVE_ARCHIVE`
|
||||||
|
- Push `A30.1: SAVE_ARCHIVE ack received with success=true` to checks
|
||||||
|
- Returns AssertionResult; host-side driveA30 appends the 5 type-presence checks
|
||||||
|
- Adds `assertA30` to `declare global { interface Window { __mokoshHarness: { ... } } }` + `window.__mokoshHarness = { ... }` object literal (preserve all existing entries including assertA29 from Plan 03-01).
|
||||||
|
- Updates the closing console.log line to append `+ Plan 03-02: A30`.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Open `tests/uat/extension-page-harness.ts`. Locate the assertA29 block added by Plan 03-01 + the `getManifestVersion` declaration following it.
|
||||||
|
2. Insert the new assertA30 block AFTER assertA29 (BEFORE getManifestVersion). Use this concrete code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/* ─── Plan 03-02 — A30 (event-log verification; SPEC §10 #5) ────────
|
||||||
|
*
|
||||||
|
* A30 — REQ-user-event-log empirical: the production listeners at
|
||||||
|
* src/content/index.ts (setupClickLogging at line 61,
|
||||||
|
* setupInputLogging at line 77, setupNavigationLogging at line
|
||||||
|
* 99, setupErrorLogging at line 133, setupNetworkLogging at
|
||||||
|
* line 164) all fire on synthetic browser events dispatched
|
||||||
|
* on the harness page, producing UserEvent entries with each
|
||||||
|
* of the 5 type-values (click / input / navigation /
|
||||||
|
* js_error / network_error) in logs/events.json.
|
||||||
|
*
|
||||||
|
* Trigger strategy (all on the harness page; no new tabs opened):
|
||||||
|
* - click: programmatic .click() on #probe-submit
|
||||||
|
* - input: set #probe-email.value + dispatch Event('input', bubbles:true)
|
||||||
|
* - navigation: history.pushState (intercepted at src/content/index.ts:121)
|
||||||
|
* - js_error: window.dispatchEvent(new ErrorEvent('error', ...))
|
||||||
|
* - network_error: fetch(404-probe-url).catch(noop) — production
|
||||||
|
* fetch interception at src/content/index.ts:167 logs response.ok===false
|
||||||
|
*
|
||||||
|
* Page-side dispatches all 5 triggers + settles + SAVE. Host-side
|
||||||
|
* driveA30 JSZip-parses logs/events.json and asserts each of the 5
|
||||||
|
* UserEvent.type literal values is present.
|
||||||
|
*
|
||||||
|
* FORBIDDEN_HOOK_STRINGS impact: NONE. A30 rides production listeners
|
||||||
|
* + existing helpers. Tier-1 inventory stays at 12.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** SAVE_ARCHIVE dispatch timeout for A30 — matches A24/A25/A27/A29. */
|
||||||
|
const A30_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||||
|
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
|
||||||
|
const A30_SEGMENT_SETTLE_MS = 11_000;
|
||||||
|
/** Settle between trigger dispatches and SAVE so event handlers complete. */
|
||||||
|
const A30_TRIGGER_SETTLE_MS = 500;
|
||||||
|
/** 404 probe URL — chrome.tabs perm grant is irrelevant; fetch happens
|
||||||
|
* from the harness page realm. example.com is RFC 2606 reserved +
|
||||||
|
* serves a 404 reliably for unknown paths under headless Chrome. */
|
||||||
|
const A30_404_PROBE_URL = 'https://example.com/this-path-does-not-exist-404-probe-a30';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A30 — Event-log empirical (SPEC §10 #5 / REQ-user-event-log).
|
||||||
|
*
|
||||||
|
* Dispatches 5 synthetic browser events that exercise each of the
|
||||||
|
* production listeners; runs setupFreshRecording so event-log
|
||||||
|
* cleanup hasn't dropped anything; settles a segment; SAVEs. Host-side
|
||||||
|
* driveA30 inspects logs/events.json from the produced zip and asserts
|
||||||
|
* each of the 5 UserEvent.type literal values appears at least once.
|
||||||
|
*
|
||||||
|
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
|
||||||
|
* driveA30 appends 5 UserEvent.type presence checks.
|
||||||
|
*/
|
||||||
|
async function assertA30(): Promise<AssertionResult> {
|
||||||
|
const result: AssertionResult = {
|
||||||
|
passed: false,
|
||||||
|
name: 'A30 — event log captures 5 UserEvent types (SPEC §10 #5 / REQ-user-event-log)',
|
||||||
|
checks: [],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
diag(result, 'Step 1: setupFreshRecording (A30 owns its recording — clean event-log window)');
|
||||||
|
const setupResp = await setupFreshRecording();
|
||||||
|
if (!setupResp.ok) {
|
||||||
|
throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
|
||||||
|
}
|
||||||
|
diag(result, 'Step 1 OK — REC state established');
|
||||||
|
|
||||||
|
diag(result, `Step 2: settle ${A30_SEGMENT_SETTLE_MS}ms for first segment rotation`);
|
||||||
|
await new Promise((r) => setTimeout(r, A30_SEGMENT_SETTLE_MS));
|
||||||
|
|
||||||
|
diag(result, 'Step 3: click trigger — programmatic .click() on #probe-submit');
|
||||||
|
const submitBtn = document.querySelector<HTMLButtonElement>('#probe-submit');
|
||||||
|
if (submitBtn !== null) {
|
||||||
|
submitBtn.click();
|
||||||
|
} else {
|
||||||
|
diag(result, 'Step 3 WARN — #probe-submit missing; click trigger skipped');
|
||||||
|
}
|
||||||
|
|
||||||
|
diag(result, 'Step 4: input trigger — set #probe-email.value + dispatch input event');
|
||||||
|
const emailInput = document.querySelector<HTMLInputElement>('#probe-email');
|
||||||
|
if (emailInput !== null) {
|
||||||
|
emailInput.value = 'a30@probe.local';
|
||||||
|
emailInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
} else {
|
||||||
|
diag(result, 'Step 4 WARN — #probe-email missing; input trigger skipped');
|
||||||
|
}
|
||||||
|
|
||||||
|
diag(result, 'Step 5: navigation trigger — history.pushState (production wrapper at src/content/index.ts:121 intercepts)');
|
||||||
|
history.pushState({}, '', window.location.pathname + '#a30-probe');
|
||||||
|
|
||||||
|
diag(result, 'Step 6: js_error trigger — window.dispatchEvent(ErrorEvent("error"))');
|
||||||
|
window.dispatchEvent(new ErrorEvent('error', {
|
||||||
|
message: 'a30-probe-js-error',
|
||||||
|
filename: 'a30-probe.js',
|
||||||
|
lineno: 1,
|
||||||
|
colno: 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
diag(result, `Step 7: network_error trigger — fetch(${A30_404_PROBE_URL}) (.catch noop)`);
|
||||||
|
try {
|
||||||
|
await fetch(A30_404_PROBE_URL);
|
||||||
|
} catch (fetchErr) {
|
||||||
|
diag(result, `Step 7 fetch threw (acceptable for network_error path): ${String(fetchErr)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
diag(result, `Step 8: settle ${A30_TRIGGER_SETTLE_MS}ms so async handlers (fetch.then) complete`);
|
||||||
|
await new Promise((r) => setTimeout(r, A30_TRIGGER_SETTLE_MS));
|
||||||
|
|
||||||
|
diag(result, 'Step 9: dispatch SAVE_ARCHIVE');
|
||||||
|
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
|
||||||
|
{ type: 'SAVE_ARCHIVE' },
|
||||||
|
A30_SAVE_ARCHIVE_TIMEOUT_MS,
|
||||||
|
'SAVE_ARCHIVE (A30)',
|
||||||
|
);
|
||||||
|
diag(result, `Step 9 result: ${JSON.stringify(ack)}`);
|
||||||
|
|
||||||
|
result.checks.push({
|
||||||
|
name: 'A30.1: SAVE_ARCHIVE ack received with success=true',
|
||||||
|
expected: true,
|
||||||
|
actual: ack.success,
|
||||||
|
passed: ack.success === true,
|
||||||
|
});
|
||||||
|
|
||||||
|
result.passed = result.checks.every((c) => c.passed);
|
||||||
|
} catch (err) {
|
||||||
|
result.error = err instanceof Error ? err.message : String(err);
|
||||||
|
diag(result, `THREW: ${result.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. In the `declare global { interface Window { __mokoshHarness: { ... } } }` block, AFTER the assertA29 entry that Plan 03-01 added and BEFORE `getManifestVersion`, insert:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-02 — event-log verification (SPEC §10 #5)
|
||||||
|
assertA30: () => Promise<AssertionResult>;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. In the `window.__mokoshHarness = { ... }` object literal, AFTER `assertA29,` and BEFORE `getManifestVersion,` insert:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
assertA30,
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Update the closing `console.log(...)` line to append `+ Plan 03-02: A30`. Concrete replacement string (preserves the Plan 03-01 mention):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + Plan 03-02: A30 + getManifestVersion)');
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Run `npx tsc --noEmit` to confirm no type errors.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>npx tsc --noEmit && grep -c "assertA30" tests/uat/extension-page-harness.ts | grep -v '^#' | awk '$1 >= 3'</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `grep -c "assertA30" tests/uat/extension-page-harness.ts` returns >=3 (function definition + declare global entry + object literal entry).
|
||||||
|
- `grep -c "A30_404_PROBE_URL" tests/uat/extension-page-harness.ts` returns >=2 (declaration + usage).
|
||||||
|
- `grep -c "ErrorEvent('error'" tests/uat/extension-page-harness.ts` returns >=1.
|
||||||
|
- `grep -c "history.pushState" tests/uat/extension-page-harness.ts` returns >=1.
|
||||||
|
- Existing Plan 03-01 assertA29 entry still present in __mokoshHarness object literal (`grep -c 'assertA29,' tests/uat/extension-page-harness.ts` returns >=1).
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
Page-side assertA30 dispatches 5 synthetic event triggers + SAVE; registered on __mokoshHarness; ready for Task 2 host-side driveA30 grep.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 2: Add driveA30 host-side (UserEvent type grep) + orchestrator wiring</name>
|
||||||
|
<files>tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
|
||||||
|
<read_first>
|
||||||
|
- tests/uat/lib/harness-page-driver.ts lines 1395-1567 (driveA26 3-phase pattern; canonical for host-side JSZip + named-entry parse)
|
||||||
|
- tests/uat/lib/harness-page-driver.ts where driveA29 was JUST added by Plan 03-01 (same file; new driver appends below)
|
||||||
|
- src/shared/types.ts lines 124-131 (UserEvent type — to import for the type-cast on parse)
|
||||||
|
- tests/uat/harness.test.ts lines 65-431 (existing import block + wrapped-driver consts + drivers array push)
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md §"UserEvent type-grep pattern (Plan 03-02)"
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
Host-side (`tests/uat/lib/harness-page-driver.ts`):
|
||||||
|
- Adds `import type { UserEvent } from '../../../src/shared/types';` near the top imports (after the existing JSZip + EventType imports).
|
||||||
|
- Adds `export async function driveA30(page: Page, downloadsDir: string): Promise<AssertionRecord>`:
|
||||||
|
- Phase 1 — page.evaluate harness.assertA30()
|
||||||
|
- Phase 2 — findLatestZip(downloadsDir); if null push A30.0 failure
|
||||||
|
- Phase 3 — JSZip.loadAsync; read `logs/events.json` entry; if absent push A30.0a failure
|
||||||
|
- Parse as `UserEvent[]`; on JSON parse failure push A30.0b failure
|
||||||
|
- For each EXPECTED_TYPE in ['click', 'input', 'navigation', 'js_error', 'network_error']:
|
||||||
|
- Push check `A30.${2..6}: logs/events.json contains at least one '${type}' event` —
|
||||||
|
expected `>=1 ${type}`, actual `userEvents.filter((e) => e.type === type).length`, passed same > 0
|
||||||
|
- Diagnostics: `A30 zipPath=${zipPath}`, `A30 userEvents.length=${userEvents.length}`, `A30 type counts: ${...}`
|
||||||
|
- Filter-pipeline form (no `continue`).
|
||||||
|
|
||||||
|
Orchestrator (`tests/uat/harness.test.ts`):
|
||||||
|
- Adds `driveA30,` to the import block (after the `driveA29,` Plan 03-01 added).
|
||||||
|
- Adds `const driveA30Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> = (page) => driveA30(page, handles.downloadsDir);` after the existing driveA29Wrapped.
|
||||||
|
- Adds `{ name: 'A30', drive: driveA30Wrapped },` to the drivers array after the A29 entry, with banner comment citing Plan 03-02 + SPEC §10 #5.
|
||||||
|
- Update the orchestrator banner line to append `, A30`.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Open `tests/uat/lib/harness-page-driver.ts`. In the import block (near top of file), AFTER the existing JSZip import + the EventType import that Plan 03-01 added, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { UserEvent } from '../../../src/shared/types';
|
||||||
|
```
|
||||||
|
|
||||||
|
2. At the end of the file (AFTER driveA29 that Plan 03-01 added), append the new driveA30 export:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
|
||||||
|
/* ─── Plan 03-02 — driveA30 (event-log verification host-side) ──────── */
|
||||||
|
|
||||||
|
/** Canonical 5-tuple of UserEvent.type literal values per
|
||||||
|
* CON-event-log-schema + src/shared/types.ts:126. Driver iterates this
|
||||||
|
* list to push one presence-check per type. */
|
||||||
|
const A30_EXPECTED_TYPES: ReadonlyArray<UserEvent['type']> = [
|
||||||
|
'click',
|
||||||
|
'input',
|
||||||
|
'navigation',
|
||||||
|
'js_error',
|
||||||
|
'network_error',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A30 (Plan 03-02 — SPEC §10 #5 / REQ-user-event-log).
|
||||||
|
*
|
||||||
|
* Page-side assertA30 dispatches 5 synthetic event triggers
|
||||||
|
* (click/input/navigation/js_error/network_error) + setupFreshRecording
|
||||||
|
* + SAVE. Host-side driveA30 JSZip-parses logs/events.json from the
|
||||||
|
* produced zip and asserts each of the 5 UserEvent.type literal values
|
||||||
|
* appears at least once.
|
||||||
|
*
|
||||||
|
* Filter-pipeline form (no `continue`) per CLAUDE.md Control Flow §.
|
||||||
|
*
|
||||||
|
* Checks (6 total — 1 page-side + 5 host-side):
|
||||||
|
* - A30.1: SAVE_ARCHIVE ack success (page-side)
|
||||||
|
* - A30.2: logs/events.json contains >=1 'click' event
|
||||||
|
* - A30.3: logs/events.json contains >=1 'input' event
|
||||||
|
* - A30.4: logs/events.json contains >=1 'navigation' event
|
||||||
|
* - A30.5: logs/events.json contains >=1 'js_error' event
|
||||||
|
* - A30.6: logs/events.json contains >=1 'network_error' event
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @param downloadsDir - Absolute path to the per-run downloads directory.
|
||||||
|
* @returns AssertionRecord with 6 merged checks.
|
||||||
|
*/
|
||||||
|
export async function driveA30(
|
||||||
|
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.assertA30();
|
||||||
|
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: 'A30.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(`A30 zipPath=${zipPath}`);
|
||||||
|
|
||||||
|
// Phase 3 — load + inspect logs/events.json.
|
||||||
|
const zipBytes = readFileSync(zipPath);
|
||||||
|
const zip = await JSZip.loadAsync(zipBytes);
|
||||||
|
const eventsFile = zip.file('logs/events.json');
|
||||||
|
mergedChecks.push({
|
||||||
|
name: 'A30.0a: logs/events.json entry exists in zip',
|
||||||
|
expected: true,
|
||||||
|
actual: eventsFile !== null,
|
||||||
|
passed: eventsFile !== null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventsFile === null) {
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
name: pageResult.name,
|
||||||
|
checks: mergedChecks,
|
||||||
|
diagnostics: mergedDiagnostics,
|
||||||
|
error: pageResult.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsRaw = await eventsFile.async('string');
|
||||||
|
let userEvents: UserEvent[] = [];
|
||||||
|
let parseErr: string | null = null;
|
||||||
|
try {
|
||||||
|
userEvents = JSON.parse(eventsRaw) as UserEvent[];
|
||||||
|
} catch (err) {
|
||||||
|
parseErr = err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseErr !== null) {
|
||||||
|
mergedChecks.push({
|
||||||
|
name: 'A30.0b: logs/events.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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter-pipeline form per CLAUDE.md Control Flow §.
|
||||||
|
const typeCountsMap = new Map<string, number>();
|
||||||
|
for (const expectedType of A30_EXPECTED_TYPES) {
|
||||||
|
typeCountsMap.set(expectedType, userEvents.filter((e) => e.type === expectedType).length);
|
||||||
|
}
|
||||||
|
mergedDiagnostics.push(`A30 userEvents.length=${userEvents.length}`);
|
||||||
|
const typeCountsRepr = [...typeCountsMap.entries()].map(([t, n]) => `${t}=${n}`).join(',');
|
||||||
|
mergedDiagnostics.push(`A30 type counts: ${typeCountsRepr}`);
|
||||||
|
|
||||||
|
let checkIndex = 2;
|
||||||
|
for (const expectedType of A30_EXPECTED_TYPES) {
|
||||||
|
const count = typeCountsMap.get(expectedType) ?? 0;
|
||||||
|
mergedChecks.push({
|
||||||
|
name: `A30.${checkIndex}: logs/events.json contains at least one '${expectedType}' event`,
|
||||||
|
expected: `>=1 ${expectedType}`,
|
||||||
|
actual: count,
|
||||||
|
passed: count > 0,
|
||||||
|
});
|
||||||
|
checkIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedPassed = mergedChecks.every((c) => c.passed);
|
||||||
|
return {
|
||||||
|
passed: mergedPassed,
|
||||||
|
name: pageResult.name,
|
||||||
|
checks: mergedChecks,
|
||||||
|
diagnostics: mergedDiagnostics,
|
||||||
|
error: pageResult.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Open `tests/uat/harness.test.ts`. In the import block from `./lib/harness-page-driver`, AFTER `driveA29,` and BEFORE `getManifestVersion,` add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-02 — event-log verification (SPEC §10 #5 / REQ-user-event-log)
|
||||||
|
driveA30,
|
||||||
|
```
|
||||||
|
|
||||||
|
4. AFTER the existing `driveA29Wrapped` const, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-02 — driveA30 needs downloadsDir for host-side JSZip parse
|
||||||
|
// of logs/events.json from the just-produced zip.
|
||||||
|
const driveA30Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||||
|
(page) => driveA30(page, handles.downloadsDir);
|
||||||
|
```
|
||||||
|
|
||||||
|
5. In the drivers array, AFTER the `{ name: 'A29', ... }` entry Plan 03-01 added, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-02 A30: event-log verification (SPEC §10 #5).
|
||||||
|
// A30 owns its SAVE because event-log cleanup runs every 60s
|
||||||
|
// (src/content/index.ts CLEANUP_INTERVAL_MS=60_000) and we need a
|
||||||
|
// fresh event-log window for the 5 synthetic triggers. Host-side
|
||||||
|
// driveA30 JSZip-parses logs/events.json and asserts presence of
|
||||||
|
// each of the 5 UserEvent.type literal values.
|
||||||
|
{ name: 'A30', drive: driveA30Wrapped },
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Update the orchestrator banner line at line 268. Concrete replacement (preserves the Plan 03-01 mention):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30)\n');
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Run `npx tsc --noEmit` to confirm no type errors.
|
||||||
|
8. Run `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat`. Expected: `31/31 GREEN`.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>npx tsc --noEmit && grep -c "driveA30" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | awk '$1 >= 2' && grep -c "driveA30" tests/uat/harness.test.ts | grep -v '^#' | awk '$1 >= 3' && grep -c "from '../../../src/shared/types'" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | grep -q '^1$' && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `grep -c "driveA30" tests/uat/lib/harness-page-driver.ts` returns >=2.
|
||||||
|
- `grep -c "driveA30" tests/uat/harness.test.ts` returns >=3.
|
||||||
|
- `grep -c "from '../../../src/shared/types'" tests/uat/lib/harness-page-driver.ts` returns exactly 1.
|
||||||
|
- `grep -c "A30_EXPECTED_TYPES" tests/uat/lib/harness-page-driver.ts` returns >=2 (declaration + loop usage).
|
||||||
|
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with stdout containing `UAT harness: 31/31 assertions passed`.
|
||||||
|
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (Tier-1 inventory stays at 12 entries).
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
UAT harness runs 31/31 GREEN with A30 verifying SPEC §10 #5 end-to-end:
|
||||||
|
logs/events.json from the assembled zip contains at least one entry
|
||||||
|
for each of the 5 UserEvent.type literal values. Filter-pipeline
|
||||||
|
form preserved; no `continue` statements introduced.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Puppeteer host ↔ page realm | Test harness drives via page.evaluate; production listeners run inside the harness page (content script injects to all_urls) |
|
||||||
|
| Page realm ↔ content script | Synthetic events on document/window route to production listeners; no new bridge surfaces |
|
||||||
|
| Outbound fetch (network_error trigger) | Single fetch to https://example.com/<404-path> — RFC 2606 reserved domain; no PII; no real endpoint |
|
||||||
|
| dist-test/ ↔ dist/ | Two-bundle separation: Plan 03-02 adds NO test-only symbols; production bundle invariant unchanged |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-03-02-01 | Information Disclosure | network_error trigger fetches a real public URL | accept | example.com is RFC 2606 reserved (test-only); no PII / secrets in URL path; outbound request is harmless probe traffic (parity with Plan 02-04 A27 multi-tab https://example.com usage). |
|
||||||
|
| T-03-02-02 | Tampering | history.pushState changes harness page URL mid-test; subsequent assertions could see different document.location | mitigate | Hash-only push (`#a30-probe`); pushState does not navigate. Subsequent drivers (Plan 03-03 A31) run setupFreshRecording which is location-agnostic. No impact on tokens.css resolution or A18/A21 invariants. |
|
||||||
|
| T-03-02-03 | Information Disclosure | Test-only hook surface leaking to production bundle | mitigate | A30 rides production listeners + existing helpers; no `__MOKOSH_UAT__`-gated symbols. FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries; Tier-1 unit-test gate runs as part of Task 2 acceptance. |
|
||||||
|
| T-03-02-04 | Denial of Service | network_error trigger fetch hangs indefinitely (example.com slow/unreachable in CI) | mitigate | fetch is awaited inside try/catch with no explicit timeout, but the page-side assertion has A30_SAVE_ARCHIVE_TIMEOUT_MS=15s overall ceiling via sendMessageWithTimeout — a hung fetch causes the test to FAIL with a clear timeout message rather than hang. CI/dev machines typically resolve example.com sub-100ms. |
|
||||||
|
|
||||||
|
No new production surface; threat surface unchanged from Plan 03-01.
|
||||||
|
UAT harness extension is test-only.
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 31/31 GREEN.
|
||||||
|
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 FORBIDDEN_HOOK_STRINGS × 0 hits each).
|
||||||
|
- A30 host-side diagnostic line shows non-zero count for each of the 5 UserEvent types.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- A30 GREEN with 6 checks (SAVE ack + 5 type-presence).
|
||||||
|
- REQ-user-event-log empirically verified across all 5 UserEvent.type literal values.
|
||||||
|
- Filter-pipeline form (no `continue` statements introduced) per CLAUDE.md Control Flow §.
|
||||||
|
- FORBIDDEN_HOOK_STRINGS unchanged at 12 entries.
|
||||||
|
- vitest baseline preserved (171/171 GREEN).
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create
|
||||||
|
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-SUMMARY.md`
|
||||||
|
documenting:
|
||||||
|
- 5 trigger dispatch patterns (click via .click() + input via dispatchEvent + navigation via pushState + js_error via dispatchEvent(ErrorEvent) + network_error via fetch 404)
|
||||||
|
- 6-check A30 contract empirically verified
|
||||||
|
- UAT 30 → 31 GREEN; Tier-1 inventory unchanged at 12
|
||||||
|
- Plan 03-03 wave dependency: SAME 3 harness files; SEQUENTIAL within Wave 2 per RESEARCH Pitfall 6
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,609 @@
|
|||||||
|
---
|
||||||
|
phase: 03
|
||||||
|
slug: spec-10-smoke-verification-dom-event-log-verification
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
wave: 3
|
||||||
|
depends_on:
|
||||||
|
- 01
|
||||||
|
- 02
|
||||||
|
files_modified:
|
||||||
|
- tests/uat/extension-page-harness.ts
|
||||||
|
- tests/uat/lib/harness-page-driver.ts
|
||||||
|
- tests/uat/harness.test.ts
|
||||||
|
autonomous: true
|
||||||
|
requirements: []
|
||||||
|
tags:
|
||||||
|
- uat-harness
|
||||||
|
- a31
|
||||||
|
- password-filter
|
||||||
|
- spec-10-8-partial
|
||||||
|
- approach-b
|
||||||
|
- negative-assertion
|
||||||
|
- charter-d-p3-02
|
||||||
|
user_setup: []
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Typing a sentinel value into the probe-page password input does NOT cause that value to appear in logs/events.json (existing src/content/index.ts:82 filter fires)"
|
||||||
|
- "Counts of UserEvent entries whose .value field contains the sentinel string = 0"
|
||||||
|
- "Counts of UserEvent entries whose .target selector points at the password input = 0 (filter happens early-return BEFORE addUserEvent)"
|
||||||
|
- "UAT harness exits 0 with 31 + 1 = 32/32 assertions GREEN (A30 baseline preserved + new A31)"
|
||||||
|
artifacts:
|
||||||
|
- path: "tests/uat/extension-page-harness.ts"
|
||||||
|
provides: "assertA31 page-side orchestrator: types sentinel into #probe-password, runs setupFreshRecording + SAVE"
|
||||||
|
contains: "assertA31"
|
||||||
|
- path: "tests/uat/lib/harness-page-driver.ts"
|
||||||
|
provides: "driveA31 host-side: JSZip-parse logs/events.json + negative-assertion sentinel grep"
|
||||||
|
contains: "driveA31"
|
||||||
|
- path: "tests/uat/harness.test.ts"
|
||||||
|
provides: "driveA31 import + wrapped driver + drivers-array push entry"
|
||||||
|
contains: "driveA31"
|
||||||
|
key_links:
|
||||||
|
- from: "tests/uat/lib/harness-page-driver.ts driveA31"
|
||||||
|
to: "tests/uat/extension-page-harness.ts assertA31"
|
||||||
|
via: "page.evaluate(() => window.__mokoshHarness.assertA31())"
|
||||||
|
pattern: "harness.assertA31\\(\\)"
|
||||||
|
- from: "tests/uat/extension-page-harness.ts assertA31"
|
||||||
|
to: "src/content/index.ts:82 password filter (READ-ONLY VERIFICATION SUBJECT)"
|
||||||
|
via: "synthetic password-input event triggers setupInputLogging filter early-return BEFORE addUserEvent"
|
||||||
|
pattern: "if \\(target.type === 'password'\\)"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Extend the UAT harness with A31 — empirical PARTIAL verification of SPEC
|
||||||
|
§10 #8. Per D-P3-02 locked decision: REQ-password-confidentiality is
|
||||||
|
Out of Scope v1; full rrweb v2 maskInputFn + data-sensitive guards are
|
||||||
|
deferred to Phase 4. Plan 03-03 confirms the EXISTING minimum filter at
|
||||||
|
`src/content/index.ts:82` (`if (target.type === 'password') return;`)
|
||||||
|
fires — sentinel string typed into the probe-page `<input type="password">`
|
||||||
|
MUST NOT appear in `logs/events.json`. This is a negative-assertion gate.
|
||||||
|
|
||||||
|
Purpose: Honors the charter literally — verify the existing
|
||||||
|
defense-in-depth without expanding scope; the PARTIAL mark in Plan
|
||||||
|
03-05 VERIFICATION.md will cite this A31 GREEN + the charter rationale.
|
||||||
|
|
||||||
|
Output: A31 assertion with 3 host-side checks (SAVE ack + sentinel
|
||||||
|
absent from events.json .value fields + zero events targeting the
|
||||||
|
password selector); UAT count 31 → 32 GREEN.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/REQUIREMENTS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-PLAN.md
|
||||||
|
@src/content/index.ts
|
||||||
|
@src/shared/types.ts
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key contracts the executor needs. Extracted from existing code. -->
|
||||||
|
|
||||||
|
From src/content/index.ts (lines 77-96; READ-ONLY — verification subject):
|
||||||
|
|
||||||
|
// setupInputLogging
|
||||||
|
function setupInputLogging() {
|
||||||
|
document.addEventListener('input', (event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
|
||||||
|
// Пропускаем пароли (line 81 comment; "skip passwords")
|
||||||
|
if (target.type === 'password') { // LINE 82 — the filter
|
||||||
|
return; // LINE 83 — early-return BEFORE addUserEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const selector = getSelector(target);
|
||||||
|
addUserEvent({ // LINE 88 — never reached for password inputs
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'input',
|
||||||
|
target: selector,
|
||||||
|
value: target.value.substring(0, 200),
|
||||||
|
url: window.location.href
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
From src/shared/types.ts (lines 124-131):
|
||||||
|
|
||||||
|
export interface UserEvent {
|
||||||
|
timestamp: number;
|
||||||
|
type: 'click' | 'input' | 'navigation' | 'js_error' | 'network_error';
|
||||||
|
target: string; // CSS selector
|
||||||
|
value?: string; // What the user typed (up to 200 chars; omitted for passwords)
|
||||||
|
url: string;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
From Plan 03-01: probe HTML in extension-page-harness.html provides
|
||||||
|
`<input type="password" id="probe-password" data-test="probe-password" />`.
|
||||||
|
The id-based selector means getSelector(target) returns `#probe-password`
|
||||||
|
for any logged event on this element (per src/content/index.ts:241-243).
|
||||||
|
</interfaces>
|
||||||
|
|
||||||
|
# Plan Anchors
|
||||||
|
|
||||||
|
- **Sequential wave assignment (per RESEARCH Pitfall 6 + file-overlap rule):** Plan 03-03 lives in wave 3
|
||||||
|
modifies the SAME 3 harness files as Plans 03-01 + 03-02. depends_on:
|
||||||
|
[01, 02] enforces sequential ordering.
|
||||||
|
- **Negative-assertion pattern (per RESEARCH Pattern 3 + Plan 02-04
|
||||||
|
A27.7/A27.8 absence-check precedent):** The contract is
|
||||||
|
`userEvents.filter(e => e.value contains SENTINEL).length === 0`.
|
||||||
|
GREEN A31 = filter fired; RED A31 = filter regressed.
|
||||||
|
- **Sentinel is a fixed test constant, not a real secret:** Per RESEARCH
|
||||||
|
Security Domain table, `secret-do-not-log-123` is a probe sentinel;
|
||||||
|
logging it would itself trigger an explicit RED. No PII.
|
||||||
|
- **Targeting via id `#probe-password`:** the production getSelector at
|
||||||
|
src/content/index.ts:240 returns `#${element.id}` when an id is
|
||||||
|
present, so any event on the password input would have
|
||||||
|
`target === '#probe-password'`. A31.3 asserts zero such entries.
|
||||||
|
- **Page-side approach with native dispatch:** set `.value` directly +
|
||||||
|
dispatch `Event('input', { bubbles: true })` → fires production
|
||||||
|
listener at line 78 → filter at line 82 early-returns.
|
||||||
|
- **Probe HTML reuse:** Plan 03-01 added the password input; Plan 03-03
|
||||||
|
reuses it; no new HTML.
|
||||||
|
- **PARTIAL mark scope (D-P3-02 — NOT in scope):** rrweb v2 maskInputFn,
|
||||||
|
data-sensitive guards, full §10 #8 closure deferred to Phase 4.
|
||||||
|
- **FORBIDDEN_HOOK_STRINGS lockstep:** A31 rides production listeners +
|
||||||
|
existing helpers. Tier-1 inventory stays at 12 entries.
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 1: Add assertA31 page-side orchestrator (sentinel-into-password trigger + SAVE)</name>
|
||||||
|
<files>tests/uat/extension-page-harness.ts</files>
|
||||||
|
<read_first>
|
||||||
|
- tests/uat/extension-page-harness.ts where Plan 03-02 added assertA30 (study its shape; new assertA31 appends after it)
|
||||||
|
- tests/uat/extension-page-harness.html (verify Plan 03-01 added the password input with id=probe-password)
|
||||||
|
- src/content/index.ts lines 77-96 (the exact filter being verified)
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
Adds module-local constants for A31 and the page-side assertA31
|
||||||
|
function that:
|
||||||
|
- Step 1: setupFreshRecording (clean event-log window)
|
||||||
|
- Step 2: settle one segment
|
||||||
|
- Step 3: set #probe-password.value to SENTINEL + dispatch input event (fires production listener; filter early-returns)
|
||||||
|
- Step 4: settle synchronous handler completion
|
||||||
|
- Step 5: dispatch SAVE_ARCHIVE
|
||||||
|
- Push A31.1 (SAVE ack)
|
||||||
|
- Registers assertA31 on declare global + __mokoshHarness object literal
|
||||||
|
- Updates trailing console.log to mention Plan 03-03: A31
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Open `tests/uat/extension-page-harness.ts`. Locate the assertA30 block added by Plan 03-02 and the `getManifestVersion` declaration following it.
|
||||||
|
2. Insert the new assertA31 block AFTER assertA30 (BEFORE getManifestVersion). Use this concrete code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/* ─── Plan 03-03 — A31 (password-filter PARTIAL; SPEC §10 #8) ────────
|
||||||
|
*
|
||||||
|
* A31 — D-P3-02 PARTIAL: verify the existing minimum filter at
|
||||||
|
* src/content/index.ts:82 (`if (target.type === 'password') return;`)
|
||||||
|
* fires when the operator types into a password input.
|
||||||
|
* Negative-assertion contract: SENTINEL value MUST be absent
|
||||||
|
* from logs/events.json.
|
||||||
|
*
|
||||||
|
* Charter alignment (per CONTEXT.md D-P3-02 + REQUIREMENTS.md line 271):
|
||||||
|
* - REQ-password-confidentiality moved Out of Scope v1 per 2026-05-20
|
||||||
|
* charter "we don't care about privacy hardening. At least here."
|
||||||
|
* - Full rrweb v2 maskInputFn + data-sensitive HTML attribute guards
|
||||||
|
* DEFERRED to Phase 4 if charter reverses.
|
||||||
|
* - A31 verifies the EXISTING minimum (the line-82 filter) — does
|
||||||
|
* NOT expand scope.
|
||||||
|
*
|
||||||
|
* FORBIDDEN_HOOK_STRINGS impact: NONE. Tier-1 inventory stays at 12.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** SAVE_ARCHIVE dispatch timeout for A31 — matches A24/A25/A27/A29/A30. */
|
||||||
|
const A31_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||||
|
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
|
||||||
|
const A31_SEGMENT_SETTLE_MS = 11_000;
|
||||||
|
/** Settle after sentinel-typing trigger so the synchronous handler completes. */
|
||||||
|
const A31_TRIGGER_SETTLE_MS = 500;
|
||||||
|
/** Fixed test sentinel — distinctive string the negative-assertion
|
||||||
|
* searches for in events.json. Per RESEARCH §"Security Domain":
|
||||||
|
* this is a probe sentinel, NOT a real secret; logging it would
|
||||||
|
* itself trigger an explicit RED. */
|
||||||
|
const A31_PASSWORD_SENTINEL = 'secret-do-not-log-123';
|
||||||
|
/** Production CSS selector returned by getSelector() at
|
||||||
|
* src/content/index.ts:241 for the password input (which has id).
|
||||||
|
* Drives A31.3 (target-absence check). */
|
||||||
|
const A31_PASSWORD_SELECTOR = '#probe-password';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A31 — Password-filter PARTIAL empirical (SPEC §10 #8 PARTIAL per D-P3-02).
|
||||||
|
*
|
||||||
|
* Types the sentinel string into the probe-page password input then
|
||||||
|
* SAVEs. Host-side driveA31 inspects logs/events.json and asserts
|
||||||
|
* absence of (a) the sentinel value and (b) any entries targeting
|
||||||
|
* the password selector.
|
||||||
|
*
|
||||||
|
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
|
||||||
|
* driveA31 appends 2 negative-assertion checks.
|
||||||
|
*/
|
||||||
|
async function assertA31(): Promise<AssertionResult> {
|
||||||
|
const result: AssertionResult = {
|
||||||
|
passed: false,
|
||||||
|
name: 'A31 — password filter fires (SPEC §10 #8 PARTIAL per D-P3-02)',
|
||||||
|
checks: [],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
diag(result, 'Step 1: setupFreshRecording (A31 owns its recording — clean event-log window)');
|
||||||
|
const setupResp = await setupFreshRecording();
|
||||||
|
if (!setupResp.ok) {
|
||||||
|
throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
|
||||||
|
}
|
||||||
|
diag(result, 'Step 1 OK — REC state established');
|
||||||
|
|
||||||
|
diag(result, `Step 2: settle ${A31_SEGMENT_SETTLE_MS}ms for first segment rotation`);
|
||||||
|
await new Promise((r) => setTimeout(r, A31_SEGMENT_SETTLE_MS));
|
||||||
|
|
||||||
|
diag(result, `Step 3: type SENTINEL into ${A31_PASSWORD_SELECTOR} + dispatch input event`);
|
||||||
|
const pwInput = document.querySelector<HTMLInputElement>(A31_PASSWORD_SELECTOR);
|
||||||
|
if (pwInput === null) {
|
||||||
|
throw new Error(`A31 trigger failed: ${A31_PASSWORD_SELECTOR} not found in DOM (probe HTML regression from Plan 03-01)`);
|
||||||
|
}
|
||||||
|
pwInput.value = A31_PASSWORD_SENTINEL;
|
||||||
|
pwInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
diag(result, 'Step 3 OK — sentinel typed; production filter at src/content/index.ts:82 should have early-returned');
|
||||||
|
|
||||||
|
diag(result, `Step 4: settle ${A31_TRIGGER_SETTLE_MS}ms so synchronous handlers complete`);
|
||||||
|
await new Promise((r) => setTimeout(r, A31_TRIGGER_SETTLE_MS));
|
||||||
|
|
||||||
|
diag(result, 'Step 5: dispatch SAVE_ARCHIVE');
|
||||||
|
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
|
||||||
|
{ type: 'SAVE_ARCHIVE' },
|
||||||
|
A31_SAVE_ARCHIVE_TIMEOUT_MS,
|
||||||
|
'SAVE_ARCHIVE (A31)',
|
||||||
|
);
|
||||||
|
diag(result, `Step 5 result: ${JSON.stringify(ack)}`);
|
||||||
|
|
||||||
|
result.checks.push({
|
||||||
|
name: 'A31.1: SAVE_ARCHIVE ack received with success=true',
|
||||||
|
expected: true,
|
||||||
|
actual: ack.success,
|
||||||
|
passed: ack.success === true,
|
||||||
|
});
|
||||||
|
|
||||||
|
result.passed = result.checks.every((c) => c.passed);
|
||||||
|
} catch (err) {
|
||||||
|
result.error = err instanceof Error ? err.message : String(err);
|
||||||
|
diag(result, `THREW: ${result.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. In the `declare global { interface Window { __mokoshHarness: { ... } } }` block, AFTER the assertA30 entry from Plan 03-02 and BEFORE `getManifestVersion`, insert:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02)
|
||||||
|
assertA31: () => Promise<AssertionResult>;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. In the `window.__mokoshHarness = { ... }` object literal, AFTER `assertA30,` and BEFORE `getManifestVersion,` insert:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
assertA31,
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Update the closing `console.log(...)` line to append `+ Plan 03-03: A31`. Concrete replacement:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + Plan 03-02: A30 + Plan 03-03: A31 + getManifestVersion)');
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Run `npx tsc --noEmit`. Expected: clean.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>npx tsc --noEmit; T=$(grep -c "assertA31" tests/uat/extension-page-harness.ts); test "$T" -ge 3 && S=$(grep -c "A31_PASSWORD_SENTINEL" tests/uat/extension-page-harness.ts); test "$S" -ge 2 && grep -q "'secret-do-not-log-123'" tests/uat/extension-page-harness.ts</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `grep -c 'assertA31' tests/uat/extension-page-harness.ts` returns >=3 (function definition + declare global entry + object literal entry).
|
||||||
|
- `grep -c 'A31_PASSWORD_SENTINEL' tests/uat/extension-page-harness.ts` returns >=2 (declaration + usage).
|
||||||
|
- `grep -c \"'secret-do-not-log-123'\" tests/uat/extension-page-harness.ts` returns exactly 1.
|
||||||
|
- Existing Plan 03-02 assertA30 entry still present.
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
Page-side assertA31 types sentinel into the password input + SAVEs; registered on __mokoshHarness; ready for Task 2 host-side driveA31 negative-assertion.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 2: Add driveA31 host-side (sentinel-absence grep) + orchestrator wiring</name>
|
||||||
|
<files>tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
|
||||||
|
<read_first>
|
||||||
|
- tests/uat/lib/harness-page-driver.ts where Plan 03-02 added driveA30 (same UserEvent import; same 3-phase shape)
|
||||||
|
- tests/uat/harness.test.ts where Plan 03-02 added driveA30 wrapping + drivers entry
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md §"Negative-assertion pattern (Plan 03-03 password sentinel grep)"
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
Host-side (`tests/uat/lib/harness-page-driver.ts`):
|
||||||
|
- The `UserEvent` type import added by Plan 03-02 is reused (no new import needed).
|
||||||
|
- Adds `export async function driveA31(page: Page, downloadsDir: string): Promise<AssertionRecord>`:
|
||||||
|
- Phase 1 — page.evaluate harness.assertA31()
|
||||||
|
- Phase 2 — findLatestZip; A31.0 if null
|
||||||
|
- Phase 3 — JSZip.loadAsync; read logs/events.json; A31.0a if absent
|
||||||
|
- Parse as UserEvent[]; A31.0b on JSON parse failure
|
||||||
|
- Push check A31.2: events whose .value contains SENTINEL = 0
|
||||||
|
(negative-assertion proves the filter fired)
|
||||||
|
- Push check A31.3: events with target === '#probe-password' = 0
|
||||||
|
(filter early-returns BEFORE addUserEvent)
|
||||||
|
- Diagnostic includes count of total userEvents + count of events that match #probe-password selector + count of events whose value contains sentinel
|
||||||
|
- Filter-pipeline form (no `continue`).
|
||||||
|
|
||||||
|
Orchestrator (`tests/uat/harness.test.ts`):
|
||||||
|
- Adds `driveA31,` to import block (after `driveA30,`).
|
||||||
|
- Adds `driveA31Wrapped` const after driveA30Wrapped.
|
||||||
|
- Adds `{ name: 'A31', drive: driveA31Wrapped },` to drivers array after the A30 entry.
|
||||||
|
- Updates orchestrator banner to append `, A31`.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Open `tests/uat/lib/harness-page-driver.ts`. At the end of the file (AFTER driveA30 added by Plan 03-02), append:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
|
||||||
|
/* ─── Plan 03-03 — driveA31 (password-filter PARTIAL host-side) ─────── */
|
||||||
|
|
||||||
|
/** Fixed test sentinel — same value as page-side A31_PASSWORD_SENTINEL.
|
||||||
|
* Negative-assertion driver searches events.json for its absence. */
|
||||||
|
const A31_PASSWORD_SENTINEL = 'secret-do-not-log-123';
|
||||||
|
/** Selector the production getSelector returns for #probe-password. */
|
||||||
|
const A31_PASSWORD_SELECTOR = '#probe-password';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A31 (Plan 03-03 — SPEC §10 #8 PARTIAL per D-P3-02).
|
||||||
|
*
|
||||||
|
* Page-side assertA31 typed SENTINEL into the password input then
|
||||||
|
* SAVEd. Host-side asserts that:
|
||||||
|
* - the sentinel string is ABSENT from any UserEvent.value field
|
||||||
|
* (proves the line-82 filter early-returned before addUserEvent)
|
||||||
|
* - no UserEvent has target === '#probe-password' (proves the same
|
||||||
|
* filter via the orthogonal selector path)
|
||||||
|
*
|
||||||
|
* Filter-pipeline form (no `continue`) per CLAUDE.md Control Flow §.
|
||||||
|
*
|
||||||
|
* Checks (3 total):
|
||||||
|
* - A31.1: SAVE_ARCHIVE ack (page-side)
|
||||||
|
* - A31.2: 0 events contain SENTINEL in .value field
|
||||||
|
* - A31.3: 0 events have target === '#probe-password'
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @param downloadsDir - Absolute path to the per-run downloads directory.
|
||||||
|
* @returns AssertionRecord with 3 merged checks.
|
||||||
|
*/
|
||||||
|
export async function driveA31(
|
||||||
|
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.assertA31();
|
||||||
|
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: 'A31.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(`A31 zipPath=${zipPath}`);
|
||||||
|
|
||||||
|
// Phase 3 — load + inspect logs/events.json.
|
||||||
|
const zipBytes = readFileSync(zipPath);
|
||||||
|
const zip = await JSZip.loadAsync(zipBytes);
|
||||||
|
const eventsFile = zip.file('logs/events.json');
|
||||||
|
mergedChecks.push({
|
||||||
|
name: 'A31.0a: logs/events.json entry exists in zip',
|
||||||
|
expected: true,
|
||||||
|
actual: eventsFile !== null,
|
||||||
|
passed: eventsFile !== null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventsFile === null) {
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
name: pageResult.name,
|
||||||
|
checks: mergedChecks,
|
||||||
|
diagnostics: mergedDiagnostics,
|
||||||
|
error: pageResult.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsRaw = await eventsFile.async('string');
|
||||||
|
let userEvents: UserEvent[] = [];
|
||||||
|
let parseErr: string | null = null;
|
||||||
|
try {
|
||||||
|
userEvents = JSON.parse(eventsRaw) as UserEvent[];
|
||||||
|
} catch (err) {
|
||||||
|
parseErr = err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseErr !== null) {
|
||||||
|
mergedChecks.push({
|
||||||
|
name: 'A31.0b: logs/events.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 eventsContainingSentinel = userEvents.filter(
|
||||||
|
(e) => typeof e.value === 'string' && e.value.includes(A31_PASSWORD_SENTINEL),
|
||||||
|
);
|
||||||
|
const eventsTargetingPassword = userEvents.filter(
|
||||||
|
(e) => e.target === A31_PASSWORD_SELECTOR,
|
||||||
|
);
|
||||||
|
mergedDiagnostics.push(`A31 userEvents.length=${userEvents.length}`);
|
||||||
|
mergedDiagnostics.push(
|
||||||
|
`A31 sentinel-containing count=${eventsContainingSentinel.length}, password-targeting count=${eventsTargetingPassword.length}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
mergedChecks.push({
|
||||||
|
name: 'A31.2: 0 UserEvent entries contain the SENTINEL in their .value field (proves src/content/index.ts:82 filter fired)',
|
||||||
|
expected: 0,
|
||||||
|
actual: eventsContainingSentinel.length,
|
||||||
|
passed: eventsContainingSentinel.length === 0,
|
||||||
|
});
|
||||||
|
mergedChecks.push({
|
||||||
|
name: `A31.3: 0 UserEvent entries have target === '${A31_PASSWORD_SELECTOR}' (filter early-returns BEFORE addUserEvent)`,
|
||||||
|
expected: 0,
|
||||||
|
actual: eventsTargetingPassword.length,
|
||||||
|
passed: eventsTargetingPassword.length === 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedPassed = mergedChecks.every((c) => c.passed);
|
||||||
|
return {
|
||||||
|
passed: mergedPassed,
|
||||||
|
name: pageResult.name,
|
||||||
|
checks: mergedChecks,
|
||||||
|
diagnostics: mergedDiagnostics,
|
||||||
|
error: pageResult.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Open `tests/uat/harness.test.ts`. In the import block from `./lib/harness-page-driver`, AFTER `driveA30,` and BEFORE `getManifestVersion,` add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02)
|
||||||
|
driveA31,
|
||||||
|
```
|
||||||
|
|
||||||
|
3. AFTER the `driveA30Wrapped` const (added by Plan 03-02), add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-03 — driveA31 needs downloadsDir for host-side JSZip
|
||||||
|
// negative-assertion against logs/events.json.
|
||||||
|
const driveA31Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||||
|
(page) => driveA31(page, handles.downloadsDir);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. In the drivers array, AFTER the `{ name: 'A30', ... }` entry, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-03 A31: password-filter PARTIAL (SPEC §10 #8 PARTIAL per
|
||||||
|
// D-P3-02). Negative-assertion: types sentinel into password input;
|
||||||
|
// host-side asserts absence from logs/events.json (proves the
|
||||||
|
// existing src/content/index.ts:82 filter fires).
|
||||||
|
{ name: 'A31', drive: driveA31Wrapped },
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Update the orchestrator banner line (line 268) to append `, A31`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30, A31)\n');
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Run `npx tsc --noEmit`. Expected: clean.
|
||||||
|
7. Run `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat`. Expected: `32/32 GREEN`.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>npx tsc --noEmit; D=$(grep -c "driveA31" tests/uat/lib/harness-page-driver.ts); test "$D" -ge 2 && H=$(grep -c "driveA31" tests/uat/harness.test.ts); test "$H" -ge 3 && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `grep -c 'driveA31' tests/uat/lib/harness-page-driver.ts` returns >=2.
|
||||||
|
- `grep -c 'driveA31' tests/uat/harness.test.ts` returns >=3.
|
||||||
|
- `grep -c 'secret-do-not-log-123' tests/uat/lib/harness-page-driver.ts` returns exactly 1.
|
||||||
|
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with stdout containing `UAT harness: 32/32 assertions passed`.
|
||||||
|
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (Tier-1 inventory stays at 12).
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
UAT harness runs 32/32 GREEN with A31 verifying the §10 #8 PARTIAL
|
||||||
|
contract: typing into a password input produces zero events whose
|
||||||
|
value contains the sentinel and zero events targeting the password
|
||||||
|
selector. The existing src/content/index.ts:82 filter is the
|
||||||
|
verified mechanism; full masking remains Out of Scope v1 per D-P3-02.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Puppeteer host ↔ page realm | Test harness drives via page.evaluate; production filter at src/content/index.ts:82 runs inside the page content script |
|
||||||
|
| Page realm ↔ content script | Synthetic input event dispatched on #probe-password reaches the production listener at src/content/index.ts:78 |
|
||||||
|
| Filter ↔ event log | Negative-assertion contract: filter at line 82 prevents value from reaching userEvents[] which is what assembles logs/events.json |
|
||||||
|
| dist-test/ ↔ dist/ | Two-bundle separation: Plan 03-03 adds NO test-only symbols; production bundle invariant unchanged |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-03-03-01 | Information Disclosure | Sentinel value lands in event log despite filter | mitigate | A31 IS the negative-assertion mitigation — RED A31 means the filter regressed; the test enforces the invariant. Sentinel is not a real secret (RESEARCH §"Security Domain"); if it leaked, it would be visible in events.json which is logged but never transmitted (REQ-password-confidentiality Out of Scope v1 charter applies). |
|
||||||
|
| T-03-03-02 | Information Disclosure | Test-only hook surface leaking to production bundle | mitigate | A31 rides the production input listener; no `__MOKOSH_UAT__`-gated symbols. FORBIDDEN_HOOK_STRINGS unchanged at 12 entries. |
|
||||||
|
| T-03-03-03 | Tampering | A31 SAVE produces a zip in the per-run downloadsDir that contains the sentinel only IF the filter regressed | accept | The per-run downloadsDir is mkdtempSync'd by launchHarnessBrowser + cleaned by the test runner; no cross-run leakage. Sentinel is not a real secret. |
|
||||||
|
| T-03-03-04 | Repudiation | If A31 GREEN but the production filter actually broke, the assertion would mislead | mitigate | A31 checks two orthogonal paths: (a) sentinel-value absence and (b) password-selector-target absence. Both pass IFF the filter early-returns BEFORE addUserEvent. A regression in the filter would cause AT LEAST ONE of the two checks to RED. Defense-in-depth at the test layer. |
|
||||||
|
|
||||||
|
No new production surface; threat surface unchanged from Phase 2. The
|
||||||
|
existing src/content/index.ts:82 filter is the verification subject;
|
||||||
|
the PARTIAL mark in Plan 03-05 VERIFICATION.md explicitly carries the
|
||||||
|
charter rationale.
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 32/32 GREEN.
|
||||||
|
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 FORBIDDEN_HOOK_STRINGS × 0 hits each).
|
||||||
|
- A31 diagnostic line shows `sentinel-containing count=0, password-targeting count=0`.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- A31 GREEN with 3 merged checks (SAVE ack + 2 negative-assertions).
|
||||||
|
- The existing src/content/index.ts:82 password filter is verified to
|
||||||
|
fire on synthetic input events into the probe-page password element.
|
||||||
|
- FORBIDDEN_HOOK_STRINGS unchanged at 12 entries.
|
||||||
|
- vitest baseline preserved (171/171 GREEN).
|
||||||
|
- Plan 03-05 VERIFICATION.md will mark §10 #8 as PARTIAL with explicit
|
||||||
|
charter citation (D-P3-02) and reference A31 GREEN as the existing-
|
||||||
|
minimum evidence.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create
|
||||||
|
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-SUMMARY.md`
|
||||||
|
documenting:
|
||||||
|
- Negative-assertion contract verified (2 orthogonal absence checks)
|
||||||
|
- D-P3-02 charter alignment (existing-minimum verification; not scope expansion)
|
||||||
|
- UAT 31 → 32 GREEN; Tier-1 inventory unchanged at 12
|
||||||
|
- Plan 03-04 wave dependency: SAME 3 harness files; SEQUENTIAL within Wave 2 per RESEARCH Pitfall 6
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
---
|
||||||
|
phase: 03
|
||||||
|
slug: spec-10-smoke-verification-dom-event-log-verification
|
||||||
|
plan: 04
|
||||||
|
type: execute
|
||||||
|
wave: 4
|
||||||
|
depends_on:
|
||||||
|
- 01
|
||||||
|
- 02
|
||||||
|
- 03
|
||||||
|
files_modified:
|
||||||
|
- tests/uat/lib/harness-page-driver.ts
|
||||||
|
- tests/uat/harness.test.ts
|
||||||
|
autonomous: true
|
||||||
|
requirements: []
|
||||||
|
tags:
|
||||||
|
- uat-harness
|
||||||
|
- a32
|
||||||
|
- ram-ceiling
|
||||||
|
- spec-10-9-best-effort
|
||||||
|
- approach-b
|
||||||
|
- page-metrics
|
||||||
|
- charter-d-p3-04
|
||||||
|
user_setup: []
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "puppeteer.Page.metrics() returns a JSHeapUsedSize value (>= 0) for the harness page realm"
|
||||||
|
- "JSHeapUsedSize for the harness page realm is below 50 MB (page-realm only; SW context excluded per RESEARCH Pitfall 2)"
|
||||||
|
- "Driver emits an explicit diagnostic line: 'NOTE: page-realm only; SW context excluded' (prevents operator misinterpretation)"
|
||||||
|
- "UAT harness exits 0 with 32 + 1 = 33/33 assertions GREEN (A31 baseline preserved + new A32)"
|
||||||
|
artifacts:
|
||||||
|
- path: "tests/uat/lib/harness-page-driver.ts"
|
||||||
|
provides: "driveA32 host-side Page.metrics scaffolding (best-effort; explicit page-realm-only diagnostic)"
|
||||||
|
contains: "driveA32"
|
||||||
|
- path: "tests/uat/harness.test.ts"
|
||||||
|
provides: "driveA32 import + drivers-array push entry (no wrapped driver — Page.metrics needs only page, not downloadsDir)"
|
||||||
|
contains: "driveA32"
|
||||||
|
key_links:
|
||||||
|
- from: "tests/uat/harness.test.ts"
|
||||||
|
to: "tests/uat/lib/harness-page-driver.ts driveA32"
|
||||||
|
via: "import + drivers-array push"
|
||||||
|
pattern: "driveA32"
|
||||||
|
- from: "tests/uat/lib/harness-page-driver.ts driveA32"
|
||||||
|
to: "puppeteer.Page.metrics() CDP Performance.getMetrics"
|
||||||
|
via: "await page.metrics()"
|
||||||
|
pattern: "page.metrics\\(\\)"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Extend the UAT harness with A32 — best-effort scaffolding for SPEC §10 #9
|
||||||
|
(extension background RAM ≤ 50 MB). Per D-P3-04 locked decision: this is
|
||||||
|
best-effort + operator-driven. The harness DOES NOT measure the MV3
|
||||||
|
service worker heap (RESEARCH Pitfall 2: Page.metrics is page-realm
|
||||||
|
only). The genuine binding §10 #9 gate is the operator's
|
||||||
|
`chrome://memory-internals` observation, recorded in Plan 03-05
|
||||||
|
VERIFICATION.md `human_verification` block.
|
||||||
|
|
||||||
|
A32 SHIPS the optional Page.metrics scaffolding per RESEARCH Open
|
||||||
|
Question 3 recommendation (~30 lines; cost-cheap; informational value).
|
||||||
|
Diagnostic output explicitly states the page-realm scope so the
|
||||||
|
operator never confuses an automation GREEN with full §10 #9 closure.
|
||||||
|
|
||||||
|
Purpose: Provides a low-cost informational floor for page-realm heap
|
||||||
|
usage and exercises the puppeteer.Page.metrics API end-to-end so Phase
|
||||||
|
4 (programmatic RAM measurement upgrade) inherits a working scaffold.
|
||||||
|
|
||||||
|
Output: A32 assertion with 2 host-side checks (Page.metrics returned
|
||||||
|
JSHeapUsedSize >= 0 + JSHeapUsedSize < 50 MB) + an explicit diagnostic
|
||||||
|
line about page-realm scope; UAT count 32 → 33 GREEN.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/REQUIREMENTS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-PLAN.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-PLAN.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key contracts the executor needs. -->
|
||||||
|
|
||||||
|
From puppeteer ^25.0.2 (Page.metrics):
|
||||||
|
|
||||||
|
interface Metrics {
|
||||||
|
Timestamp?: number;
|
||||||
|
Documents?: number;
|
||||||
|
Frames?: number;
|
||||||
|
JSEventListeners?: number;
|
||||||
|
Nodes?: number;
|
||||||
|
LayoutCount?: number;
|
||||||
|
RecalcStyleCount?: number;
|
||||||
|
LayoutDuration?: number;
|
||||||
|
RecalcStyleDuration?: number;
|
||||||
|
ScriptDuration?: number;
|
||||||
|
TaskDuration?: number;
|
||||||
|
JSHeapUsedSize?: number; // <- bytes; the field A32 reads
|
||||||
|
JSHeapTotalSize?: number;
|
||||||
|
}
|
||||||
|
page.metrics(): Promise<Metrics>;
|
||||||
|
|
||||||
|
From RESEARCH.md §"Code Example A3X":
|
||||||
|
|
||||||
|
- Page.metrics is page-realm only — JSHeapUsedSize covers V8 isolate
|
||||||
|
of THIS Page, NOT the MV3 service worker (separate target).
|
||||||
|
- 50 MB threshold per SPEC §10 #9; treat as best-effort floor for the
|
||||||
|
page realm alone.
|
||||||
|
- Diagnostic copy gate: emit
|
||||||
|
'NOTE: page-realm only; SW context measurement requires
|
||||||
|
chrome://memory-internals operator verification per D-P3-04.'
|
||||||
|
|
||||||
|
From src/shared/types.ts: no UserEvent / type changes for A32.
|
||||||
|
</interfaces>
|
||||||
|
|
||||||
|
# Plan Anchors
|
||||||
|
|
||||||
|
- **Sequential wave assignment (per RESEARCH Pitfall 6 + file-overlap rule):** Plan 03-04 lives in wave 4
|
||||||
|
modifies tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts
|
||||||
|
(SAME files as Plans 03-01..03; depends_on enforces sequential).
|
||||||
|
- **NO page-side assertion needed.** Page.metrics is a host-side
|
||||||
|
puppeteer API. Unlike A24..A31, A32 does NOT call assertA32 inside
|
||||||
|
page.evaluate — there's no need for a window.__mokoshHarness method.
|
||||||
|
This is consistent with how the host-side latency portion of A25 is
|
||||||
|
computed; A32 is similar but skips the page-side entirely.
|
||||||
|
- **No setupFreshRecording, no SAVE, no zip read.** A32 measures the
|
||||||
|
current heap state of the harness page; no archive is produced.
|
||||||
|
- **RESEARCH Pitfall 2 mitigation (HARD):** the diagnostic line about
|
||||||
|
page-realm scope MUST be emitted regardless of pass/fail. This
|
||||||
|
prevents an operator from glancing at "A32 GREEN" and concluding §10
|
||||||
|
#9 is closed.
|
||||||
|
- **50 MB threshold:** SPEC §10 #9 + CON-ram-ceiling. Page-realm typical
|
||||||
|
values: a few MB (Plan 02-04 harness measurements show ~2-8 MB
|
||||||
|
page-realm heap during recording). Far below the 50 MB ceiling on
|
||||||
|
any reasonable run.
|
||||||
|
- **FORBIDDEN_HOOK_STRINGS lockstep:** A32 is host-side only; Page.metrics
|
||||||
|
is not bundled to the page. Tier-1 inventory stays at 12 entries.
|
||||||
|
- **A6 in RESEARCH Assumptions Log MEDIUM-risk noted:** "if Plan 03-04
|
||||||
|
scaffolding requires a new bridge op (e.g., `get-page-metrics` from
|
||||||
|
offscreen → harness), that would add 1-2 entries." This plan AVOIDS
|
||||||
|
that: Page.metrics is read from the host puppeteer object directly;
|
||||||
|
no new bridge ops added; no new __MOKOSH_UAT__ symbols.
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 1: Add driveA32 host-side (puppeteer.Page.metrics scaffolding) + orchestrator wiring</name>
|
||||||
|
<files>tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
|
||||||
|
<read_first>
|
||||||
|
- tests/uat/lib/harness-page-driver.ts (full sense of the file; in particular how driveA1 is a 1-line page.evaluate wrapper, contrasting with A32 which is pure host-side)
|
||||||
|
- tests/uat/harness.test.ts where Plan 03-03 added driveA31 + driveA31Wrapped + drivers-array entry (study shape)
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md §"Code Example A3X" (canonical scaffolding shape; verbatim copy)
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md §"Pitfall 2" (diagnostic-copy gate)
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
Host-side (`tests/uat/lib/harness-page-driver.ts`):
|
||||||
|
- Adds `export async function driveA32(page: Page): Promise<AssertionRecord>`:
|
||||||
|
- Calls `const metrics = await page.metrics();`
|
||||||
|
- Computes `const jsHeapBytes = metrics.JSHeapUsedSize ?? -1;`
|
||||||
|
- Computes `const jsHeapMB = jsHeapBytes >= 0 ? jsHeapBytes / (1024 * 1024) : -1;`
|
||||||
|
- Pushes A32.1 (Page.metrics returned JSHeapUsedSize): expected '>= 0', actual `jsHeapBytes`, passed `jsHeapBytes >= 0`
|
||||||
|
- Pushes A32.2 (page-realm JS heap < 50 MB): expected '< 50 MB', actual `${jsHeapMB.toFixed(2)} MB`, passed `jsHeapMB >= 0 && jsHeapMB < 50`
|
||||||
|
- Pushes the mandatory diagnostic: `'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.'`
|
||||||
|
- Also pushes informational diagnostics: `JSHeapUsedSize=${jsHeapBytes} bytes` and `JSHeapTotalSize=${metrics.JSHeapTotalSize ?? -1} bytes`
|
||||||
|
- Returns AssertionRecord computed `passed = checks.every(c => c.passed)`
|
||||||
|
- The new constant `A32_RAM_CEILING_BYTES = 50 * 1024 * 1024` makes the threshold readable.
|
||||||
|
|
||||||
|
Orchestrator (`tests/uat/harness.test.ts`):
|
||||||
|
- Adds `driveA32,` to import block (after `driveA31,`).
|
||||||
|
- NO `driveA32Wrapped` const needed (driveA32 takes only `page`).
|
||||||
|
- Adds `{ name: 'A32', drive: driveA32 },` to drivers array AFTER the A31 entry, with banner comment citing D-P3-04 + Pitfall 2.
|
||||||
|
- Updates orchestrator banner line to append `, A32`.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Open `tests/uat/lib/harness-page-driver.ts`. At the end of the file (AFTER driveA31 added by Plan 03-03), append:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
|
||||||
|
/* ─── Plan 03-04 — driveA32 (RAM scaffolding best-effort) ──────────── */
|
||||||
|
|
||||||
|
/** RAM ceiling per SPEC §10 #9 + CON-ram-ceiling. */
|
||||||
|
const A32_RAM_CEILING_BYTES = 50 * 1024 * 1024;
|
||||||
|
/** Bytes-per-MB factor for diagnostic copy. */
|
||||||
|
const A32_BYTES_PER_MB = 1024 * 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A32 (Plan 03-04 — SPEC §10 #9 RAM best-effort per D-P3-04).
|
||||||
|
*
|
||||||
|
* Reads puppeteer.Page.metrics() against the harness page and asserts
|
||||||
|
* JSHeapUsedSize is below the 50 MB ceiling. This is informational
|
||||||
|
* scaffolding ONLY:
|
||||||
|
*
|
||||||
|
* - RESEARCH Pitfall 2: Page.metrics is page-realm only. The MV3
|
||||||
|
* service worker is a separate Puppeteer target with its own V8
|
||||||
|
* isolate; page.metrics() does not aggregate across workers/iframes.
|
||||||
|
* - The page-realm value reported here is NOT the operator-facing
|
||||||
|
* "extension background RAM" measurement that SPEC §10 #9 requires.
|
||||||
|
* - The binding §10 #9 gate lives in Plan 03-05 VERIFICATION.md
|
||||||
|
* `human_verification` block (operator runs chrome://memory-internals
|
||||||
|
* OR chrome://extensions service-worker memory display).
|
||||||
|
*
|
||||||
|
* Why ship this anyway (per RESEARCH Open Question 3):
|
||||||
|
* - Low cost (~30 lines; single API call; no new bundle surface).
|
||||||
|
* - Exercises the Page.metrics API end-to-end so Phase 4 (programmatic
|
||||||
|
* RAM measurement upgrade) inherits a working scaffold.
|
||||||
|
* - Provides a sanity floor — if the harness page-realm heap ever
|
||||||
|
* blows past 50 MB, something has gone catastrophically wrong in
|
||||||
|
* the test infrastructure itself (not necessarily a §10 #9 regression
|
||||||
|
* in production).
|
||||||
|
*
|
||||||
|
* The diagnostic line about page-realm scope MUST be emitted regardless
|
||||||
|
* of pass/fail per Pitfall 2.
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @returns AssertionRecord with 2 checks (heap returned + heap < 50 MB)
|
||||||
|
* + explicit page-realm-only diagnostic.
|
||||||
|
*/
|
||||||
|
export async function driveA32(page: Page): Promise<AssertionRecord> {
|
||||||
|
const checks: CheckRecord[] = [];
|
||||||
|
const diagnostics: string[] = [];
|
||||||
|
|
||||||
|
// Pitfall 2 gate: emit the page-realm caveat BEFORE any other diagnostic
|
||||||
|
// so it leads in the structured output (the operator sees it first).
|
||||||
|
diagnostics.push(
|
||||||
|
'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.',
|
||||||
|
);
|
||||||
|
|
||||||
|
let metricsErr: string | null = null;
|
||||||
|
let jsHeapBytes = -1;
|
||||||
|
let jsHeapTotal = -1;
|
||||||
|
try {
|
||||||
|
const metrics = await page.metrics();
|
||||||
|
jsHeapBytes = metrics.JSHeapUsedSize ?? -1;
|
||||||
|
jsHeapTotal = metrics.JSHeapTotalSize ?? -1;
|
||||||
|
} catch (err) {
|
||||||
|
metricsErr = err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsHeapMB = jsHeapBytes >= 0 ? jsHeapBytes / A32_BYTES_PER_MB : -1;
|
||||||
|
diagnostics.push(`A32 JSHeapUsedSize=${jsHeapBytes} bytes (${jsHeapMB.toFixed(2)} MB)`);
|
||||||
|
diagnostics.push(`A32 JSHeapTotalSize=${jsHeapTotal} bytes`);
|
||||||
|
if (metricsErr !== null) {
|
||||||
|
diagnostics.push(`A32 Page.metrics threw: ${metricsErr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
checks.push({
|
||||||
|
name: 'A32.1: Page.metrics returned a JSHeapUsedSize value >= 0',
|
||||||
|
expected: '>= 0',
|
||||||
|
actual: jsHeapBytes,
|
||||||
|
passed: jsHeapBytes >= 0,
|
||||||
|
});
|
||||||
|
checks.push({
|
||||||
|
name: `A32.2: Page-realm JS heap < ${A32_RAM_CEILING_BYTES / A32_BYTES_PER_MB} MB (NOTE: scaffolding only; SW context excluded per D-P3-04)`,
|
||||||
|
expected: `< ${A32_RAM_CEILING_BYTES / A32_BYTES_PER_MB} MB`,
|
||||||
|
actual: jsHeapMB >= 0 ? `${jsHeapMB.toFixed(2)} MB` : 'unavailable',
|
||||||
|
passed: jsHeapBytes >= 0 && jsHeapBytes < A32_RAM_CEILING_BYTES,
|
||||||
|
});
|
||||||
|
|
||||||
|
const passed = checks.every((c) => c.passed);
|
||||||
|
return {
|
||||||
|
passed,
|
||||||
|
name: 'A32 — RAM scaffolding (best-effort; page-realm only per D-P3-04 / SPEC §10 #9)',
|
||||||
|
checks,
|
||||||
|
diagnostics,
|
||||||
|
error: metricsErr ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Open `tests/uat/harness.test.ts`. In the import block from `./lib/harness-page-driver`, AFTER `driveA31,` and BEFORE `getManifestVersion,` add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-04 — RAM scaffolding best-effort (SPEC §10 #9 per D-P3-04)
|
||||||
|
driveA32,
|
||||||
|
```
|
||||||
|
|
||||||
|
3. In the drivers array, AFTER the `{ name: 'A31', ... }` entry from Plan 03-03, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plan 03-04 A32: RAM scaffolding (SPEC §10 #9 best-effort per D-P3-04).
|
||||||
|
// NOTE — Page.metrics is page-realm only; SW context is a separate
|
||||||
|
// Puppeteer target (RESEARCH Pitfall 2). A32 is informational
|
||||||
|
// scaffolding; the binding §10 #9 gate lives in Plan 03-05
|
||||||
|
// VERIFICATION.md `human_verification` block. No wrapped const
|
||||||
|
// needed — driveA32 takes only `page`.
|
||||||
|
{ name: 'A32', drive: driveA32 },
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Update the orchestrator banner line (line 268) to append `, A32`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30, A31, A32)\n');
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Run `npx tsc --noEmit`. Expected: clean.
|
||||||
|
6. Run `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat`. Expected: `33/33 GREEN`.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>npx tsc --noEmit; D=$(grep -c "driveA32" tests/uat/lib/harness-page-driver.ts); test "$D" -ge 2 && H=$(grep -c "driveA32" tests/uat/harness.test.ts); test "$H" -ge 2 && grep -q "NOTE: page-realm only" tests/uat/lib/harness-page-driver.ts && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `grep -c 'driveA32' tests/uat/lib/harness-page-driver.ts` returns >=2.
|
||||||
|
- `grep -c 'driveA32' tests/uat/harness.test.ts` returns >=2 (import line + drivers-array push; no wrapped const).
|
||||||
|
- `grep -c 'NOTE: page-realm only' tests/uat/lib/harness-page-driver.ts` returns exactly 1.
|
||||||
|
- `grep -c 'page.metrics()' tests/uat/lib/harness-page-driver.ts` returns exactly 1.
|
||||||
|
- `grep -c 'A32_RAM_CEILING_BYTES' tests/uat/lib/harness-page-driver.ts` returns >=2 (declaration + usage).
|
||||||
|
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with stdout containing `UAT harness: 33/33 assertions passed` AND the diagnostic line `NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.` (printed by printAssertionResult on A32).
|
||||||
|
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (Tier-1 inventory stays at 12).
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
UAT harness runs 33/33 GREEN. A32 emits the page-realm-only diagnostic
|
||||||
|
on EVERY run (pass or fail). FORBIDDEN_HOOK_STRINGS unchanged at 12.
|
||||||
|
Page.metrics scaffolding lives in the harness for Phase 4 to upgrade.
|
||||||
|
The binding §10 #9 gate remains operator-driven and is recorded as
|
||||||
|
human_verification in Plan 03-05.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Puppeteer host ↔ CDP | Page.metrics is a thin wrapper over CDP Performance.getMetrics; runs in the puppeteer host process, no extension code path |
|
||||||
|
| Page realm ↔ host realm | A32 does NOT use page.evaluate; no new contract between page and host |
|
||||||
|
| dist-test/ ↔ dist/ | Two-bundle separation: Plan 03-04 adds NO test-only symbols; production bundle invariant unchanged |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-03-04-01 | Repudiation | Operator interprets A32 GREEN as full §10 #9 closure, skips chrome://memory-internals check | mitigate | Mandatory diagnostic line `'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.'` emitted on EVERY run; check name itself includes the caveat; Plan 03-05 VERIFICATION.md explicitly lists §10 #9 in `human_verification` block. Three layers of operator-visible signal. |
|
||||||
|
| T-03-04-02 | Information Disclosure | Test-only hook surface leaking to production bundle | mitigate | A32 is host-side only; Page.metrics is not bundled to the page realm. FORBIDDEN_HOOK_STRINGS unchanged at 12 entries. |
|
||||||
|
| T-03-04-03 | Denial of Service | Page.metrics returns 0 or throws on first call after browser launch | mitigate | A32 wraps the call in try/catch + falls through gracefully (jsHeapBytes stays -1; A32.1 RED with clear diagnostic). Per A3 in RESEARCH Assumptions Log, Page.metrics has been stable since Puppeteer 1.x; failure is extremely unlikely on 25.0.2. |
|
||||||
|
| T-03-04-04 | Elevation of Privilege | New chrome.* permission grant for measurement | accept | A32 uses zero chrome.* APIs. Page.metrics is a CDP call, not an extension API. No manifest delta. |
|
||||||
|
|
||||||
|
No new production surface; threat surface unchanged from Plan 03-03.
|
||||||
|
UAT harness extension is test-only and adds no bundle surface (Page.metrics
|
||||||
|
is host-side only).
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `npx tsc --noEmit` exits 0.
|
||||||
|
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 33/33 GREEN.
|
||||||
|
- The diagnostic line `NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.` appears in stdout from A32.
|
||||||
|
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 FORBIDDEN_HOOK_STRINGS × 0 hits each).
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- A32 GREEN with 2 checks (heap returned + heap < 50 MB).
|
||||||
|
- Pitfall 2 diagnostic emitted on every run.
|
||||||
|
- Page.metrics scaffolding in place for Phase 4 to upgrade.
|
||||||
|
- FORBIDDEN_HOOK_STRINGS unchanged at 12 entries.
|
||||||
|
- vitest baseline preserved (171/171 GREEN).
|
||||||
|
- Plan 03-05 will record §10 #9 as `human_verification` regardless of A32
|
||||||
|
status — A32 is informational scaffolding, NOT the binding gate.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create
|
||||||
|
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-SUMMARY.md`
|
||||||
|
documenting:
|
||||||
|
- A32 host-side-only scaffolding rationale (no page-side; Page.metrics is host)
|
||||||
|
- D-P3-04 + Pitfall 2 compliance (mandatory page-realm-only diagnostic)
|
||||||
|
- Phase 4 inheritance: programmatic RAM measurement upgrade path
|
||||||
|
- UAT 32 → 33 GREEN; Tier-1 inventory unchanged at 12
|
||||||
|
- Plan 03-05 wave dependency: VERIFICATION.md aggregator; depends on Plans 03-01..04 GREEN
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,731 @@
|
|||||||
|
---
|
||||||
|
phase: 03
|
||||||
|
slug: spec-10-smoke-verification-dom-event-log-verification
|
||||||
|
plan: 05
|
||||||
|
type: execute
|
||||||
|
wave: 5
|
||||||
|
depends_on:
|
||||||
|
- 01
|
||||||
|
- 02
|
||||||
|
- 03
|
||||||
|
- 04
|
||||||
|
files_modified:
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md
|
||||||
|
- .planning/REQUIREMENTS.md
|
||||||
|
- .planning/ROADMAP.md
|
||||||
|
- .planning/STATE.md
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- REQ-install-clean
|
||||||
|
- REQ-rrweb-dom-buffer
|
||||||
|
- REQ-user-event-log
|
||||||
|
tags:
|
||||||
|
- verification
|
||||||
|
- spec-10-sweep
|
||||||
|
- aggregator
|
||||||
|
- t5-override
|
||||||
|
- charter-d-p3-02
|
||||||
|
- charter-d-p3-04
|
||||||
|
- phase-3-closure
|
||||||
|
user_setup: []
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "VERIFICATION.md exists at .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md with the canonical frontmatter shape (phase + verified + status + score + overrides_applied + override_notes + human_verification)"
|
||||||
|
- "All 9 SPEC §10 criteria have explicit evidence rows in the Per-Requirement Scorecard with Phase-citation + plan-citation + commit-citation"
|
||||||
|
- "Pre-checkpoint bundle gates 6/6 PASS (per saved memory feedback-pre-checkpoint-bundle-gates.md)"
|
||||||
|
- "§10 #8 marked PARTIAL with explicit D-P3-02 charter citation + A31 GREEN evidence in overrides_applied"
|
||||||
|
- "§10 #9 marked human_needed with explicit D-P3-04 charter citation + operator chrome://memory-internals instructions + A32 informational note in human_verification"
|
||||||
|
- "T5 override pattern applied for §10 #4/#5 (and #8 PARTIAL) per saved memory feedback-trust-harness-over-manual-uat.md"
|
||||||
|
- "REQUIREMENTS.md REQ-rrweb-dom-buffer + REQ-user-event-log markers flipped to Complete (or PARTIAL with citation)"
|
||||||
|
- "ROADMAP.md Phase 3 row flipped to [x] with closure date"
|
||||||
|
- "STATE.md progress.completed_phases incremented; current focus + session continuity updated"
|
||||||
|
artifacts:
|
||||||
|
- path: ".planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md"
|
||||||
|
provides: "Full §10 sweep aggregator with 9-criterion scorecard + T5 override block + human_verification block + cross-cutting gates + deferred items"
|
||||||
|
min_lines: 120
|
||||||
|
- path: ".planning/REQUIREMENTS.md"
|
||||||
|
provides: "REQ-rrweb-dom-buffer + REQ-user-event-log markers updated with Plan 03-01/03-02 closure citation"
|
||||||
|
contains: "Phase 3 closure"
|
||||||
|
- path: ".planning/ROADMAP.md"
|
||||||
|
provides: "Phase 3 row flipped [x] + closure date + harness count update (33 assertions)"
|
||||||
|
contains: "Phase 3"
|
||||||
|
- path: ".planning/STATE.md"
|
||||||
|
provides: "progress.completed_phases incremented to 3; current_focus + last_activity updated"
|
||||||
|
contains: "completed_phases: 3"
|
||||||
|
key_links:
|
||||||
|
- from: "03-VERIFICATION.md"
|
||||||
|
to: "Plan 03-01 A29 GREEN (rrweb session.json) + Plan 03-02 A30 GREEN (events.json types) + Plan 03-03 A31 GREEN (password absence) + Plan 03-04 A32 GREEN (page-realm heap)"
|
||||||
|
via: "evidence column citations"
|
||||||
|
pattern: "A29|A30|A31|A32"
|
||||||
|
- from: "03-VERIFICATION.md override_notes"
|
||||||
|
to: "feedback-trust-harness-over-manual-uat.md saved memory"
|
||||||
|
via: "T5 override rationale citation"
|
||||||
|
pattern: "feedback-trust-harness-over-manual-uat"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Phase 3 closure: write VERIFICATION.md aggregating all 9 SPEC §10
|
||||||
|
acceptance criteria evidence across Phase 1 + Phase 2 + Phase 3 plans,
|
||||||
|
mark §10 #8 PARTIAL per D-P3-02 charter, mark §10 #9 human_needed per
|
||||||
|
D-P3-04 charter, run pre-checkpoint bundle gates (6/6 standard
|
||||||
|
inventory), and flip the REQUIREMENTS.md / ROADMAP.md / STATE.md
|
||||||
|
markers that signal Phase 3 closure.
|
||||||
|
|
||||||
|
Purpose: This IS the Phase 3 deliverable. Plans 03-01..04 produced the
|
||||||
|
empirical evidence (A29..A32 GREEN); Plan 03-05 synthesizes it into the
|
||||||
|
canonical §10 sweep record + flips the project trackers.
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- `03-VERIFICATION.md` (new file) — 9-criterion scorecard + override
|
||||||
|
block + human_verification block + cross-cutting gates + deferred
|
||||||
|
items table; modeled on Phase 1 + Phase 2 VERIFICATION.md structures.
|
||||||
|
- Documentation marker flips in REQUIREMENTS.md + ROADMAP.md + STATE.md.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/REQUIREMENTS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-PLAN.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-PLAN.md
|
||||||
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-PLAN.md
|
||||||
|
@.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md
|
||||||
|
@.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Canonical reference shapes the executor uses verbatim. -->
|
||||||
|
|
||||||
|
T5 override frontmatter shape (from 02-VERIFICATION.md lines 1-32):
|
||||||
|
- `phase`, `verified` (ISO timestamp Z), `status`, `score`, `overrides_applied` (int), `override_notes` (list of {dimension, initial_status, override_to, rationale}), `human_verification` (list of {dimension, rationale}).
|
||||||
|
|
||||||
|
Phase 1 VERIFICATION.md structure (from 01-VERIFICATION.md):
|
||||||
|
- Goal block (verbatim ROADMAP)
|
||||||
|
- Per-Requirement Scorecard table (|#|Requirement|Evidence|Status|)
|
||||||
|
- Cross-Cutting Gates table (|Gate|Evidence|Status|)
|
||||||
|
- 7 P0 Audit Defects table (Phase 3 doesn't ship implementation; uses 9-criterion §10 scorecard instead)
|
||||||
|
- Operator-Empirical Acks table (Phase 3 may have one row for §10 #9 once operator runs chrome://memory-internals; empty until then)
|
||||||
|
- Forward-Looking Deferred Items table
|
||||||
|
|
||||||
|
Pre-checkpoint bundle gates 6/6 standard inventory (from
|
||||||
|
feedback-pre-checkpoint-bundle-gates.md + 02-04-SUMMARY.md lines 158-168):
|
||||||
|
- Gate 1: npm run build exit 0
|
||||||
|
- Gate 2: SW CSP-safety (new Function/eval) — 1 documented exception (setimmediate polyfill)
|
||||||
|
- Gate 3: SW Node-globals (Buffer./require) — 0 hits
|
||||||
|
- Gate 4: DOM-globals (window./document.) — bundled-lib idiom only
|
||||||
|
- Gate 5: Tier-1 SW-bundle-import gate GREEN
|
||||||
|
- Gate 6: FORBIDDEN_HOOK_STRINGS unit gate — 12 strings × 0 hits each
|
||||||
|
- Gate 7: Manifest validation gates (i18n + locale-parity + build) GREEN
|
||||||
|
|
||||||
|
§10 criteria → REQ mapping (from REQUIREMENTS.md §"Phase 1 Acceptance Criteria"):
|
||||||
|
1. install — REQ-install-clean + REQ-manifest-permissions (Phase 1 closed; Plan 01-12)
|
||||||
|
2. continuous video — REQ-video-ring-buffer (Phase 1 closed)
|
||||||
|
3. ≤ 30s buffer — REQ-video-ring-buffer (Phase 1 closed)
|
||||||
|
4. rrweb DOM — REQ-rrweb-dom-buffer (Plan 03-01 A29 GREEN)
|
||||||
|
5. event log — REQ-user-event-log (Plan 03-02 A30 GREEN)
|
||||||
|
6. < 5s archive — REQ-archive-export-latency (Phase 2 closed; A25 GREEN)
|
||||||
|
7. playable .webm — REQ-archive-layout + REQ-video-ring-buffer (Phase 1 closed; A28 GREEN)
|
||||||
|
8. password — REQ-password-confidentiality (Out of Scope v1; A31 PARTIAL per D-P3-02)
|
||||||
|
9. RAM — CON-ram-ceiling (best-effort + operator-driven per D-P3-04; A32 informational)
|
||||||
|
|
||||||
|
T5 override pattern (from 02-VERIFICATION.md + feedback-trust-harness-over-manual-uat.md):
|
||||||
|
- When verifier returns human_needed for a criterion AND a harness assertion empirically covers the same surface, MOVE the entry from human_verification to overrides_applied with explicit user-delegation citation + saved-memory reference.
|
||||||
|
- §10 #4 / #5 — harness covers (A29 + A30); override-eligible per saved memory.
|
||||||
|
- §10 #8 PARTIAL — harness covers existing-minimum (A31); override-eligible per D-P3-02 (charter clarifies scope, not regression).
|
||||||
|
- §10 #9 — page.metrics is page-realm only (Pitfall 2); SW context CANNOT be measured by harness — this is the genuine human_verification exception.
|
||||||
|
</interfaces>
|
||||||
|
|
||||||
|
# Plan Anchors
|
||||||
|
|
||||||
|
- **Wave 3 (synthesis):** Plans 03-01..04 must be GREEN before this
|
||||||
|
plan runs. depends_on: [01, 02, 03, 04] enforces.
|
||||||
|
- **T5 override pattern (saved memory feedback-trust-harness-over-manual-uat.md):**
|
||||||
|
apply to §10 #4 / #5 / #8 PARTIAL. The harness assertions A29 / A30 /
|
||||||
|
A31 cover the empirical surfaces. §10 #9 is the genuine exception
|
||||||
|
per RESEARCH Pitfall 2 (Page.metrics is page-realm only).
|
||||||
|
- **Documentation marker flips happen ATOMICALLY in the closure commit:**
|
||||||
|
REQUIREMENTS.md + ROADMAP.md + STATE.md flip in one commit per
|
||||||
|
Phase 1 closure precedent (commit `586836f`).
|
||||||
|
- **No new production code; no test changes:** Plan 03-05 is pure
|
||||||
|
documentation synthesis + verification gate execution.
|
||||||
|
- **Source audit citation:** Per REQUIREMENTS.md Traceability table:
|
||||||
|
REQ-rrweb-dom-buffer + REQ-user-event-log were re-routed to Phase 3
|
||||||
|
via the 2026-05-20 re-phasing. Plan 03-05 flips both from "Pending"
|
||||||
|
to "Complete (Phase 3 closure)" or "PARTIAL" as appropriate per A31
|
||||||
|
status.
|
||||||
|
- **REQ-install-clean re-verification:** REQUIREMENTS.md line 178-185
|
||||||
|
shows it was marked Complete in Phase 1 Plan 01-12. Plan 03-05 cites
|
||||||
|
the existing tests (no-remote-fonts.test.ts + manifest-i18n.test.ts +
|
||||||
|
locale-parity.test.ts) without re-running anything custom for §10 #1.
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 1: Run pre-checkpoint bundle gates (6/6 standard inventory)</name>
|
||||||
|
<files></files>
|
||||||
|
<read_first>
|
||||||
|
- /home/parf/.claude/projects/-home-parf-projects-work-repremium/memory/feedback-pre-checkpoint-bundle-gates.md
|
||||||
|
- .planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md lines 158-168 (canonical 7-gate table; Plan 03-05 runs same checks)
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
Runs the canonical 6-gate (Plan 02-04 ran 7; Plan 03-05 follows the
|
||||||
|
same shape):
|
||||||
|
- Gate 1: `npm run build` exits 0
|
||||||
|
- Gate 2: SW CSP-safety — `grep -rEn 'new Function|eval\\(' dist/assets/` returns 1 documented exception (setimmediate polyfill; pre-existing per deferred-items.md)
|
||||||
|
- Gate 3: SW Node-globals — `grep -rE 'Buffer\\.from|Buffer\\.alloc|require\\(' dist/assets/index.ts-*.js` returns 0 hits
|
||||||
|
- Gate 4: DOM-globals — `grep -rE 'window\\.|document\\.' dist/assets/index.ts-*.js` returns only bundled-lib idiom hits (typeof-guarded)
|
||||||
|
- Gate 5: Tier-1 SW-bundle-import gate — `npm test -- --run tests/background/sw-bundle-import.test.ts` exits 0
|
||||||
|
- Gate 6: FORBIDDEN_HOOK_STRINGS unit gate — `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 strings × 0 hits each)
|
||||||
|
- Bonus Gate 7 (per 02-04 precedent): manifest + i18n + locale-parity GREEN — `npm test -- --run tests/i18n/ tests/build/` exits 0
|
||||||
|
Records the result of each gate (text snippet of grep output + test exit code) for inclusion in VERIFICATION.md cross-cutting gates table in Task 2.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. From the repo root, run a clean build:
|
||||||
|
`npm run build`
|
||||||
|
Confirm exit 0; note the chunk count + total size for the VERIFICATION.md cross-cutting gates row.
|
||||||
|
|
||||||
|
2. Gate 2 — SW CSP-safety. Find the SW chunk and grep:
|
||||||
|
`find dist/assets -name 'index.ts-*.js' -print -exec grep -En 'new Function|eval\(' {} +`
|
||||||
|
Expected: exactly 1 occurrence (`new Function(""+I)` or similar setimmediate polyfill — pre-existing per `.planning/phases/01-stabilize-video-pipeline/deferred-items.md`). NO `eval(` hits. Record the file path + occurrence count.
|
||||||
|
|
||||||
|
3. Gate 3 — SW Node-globals:
|
||||||
|
`find dist/assets -name 'index.ts-*.js' -print -exec grep -En 'Buffer\.from|Buffer\.alloc|require\(' {} +`
|
||||||
|
Expected: 0 hits. Record.
|
||||||
|
|
||||||
|
4. Gate 4 — DOM-globals on SW chunk:
|
||||||
|
`find dist/assets -name 'index.ts-*.js' -print -exec grep -cE 'window\.|document\.' {} +`
|
||||||
|
Expected: small count (bundled-lib idiom; per 02-04 closure ~8 each, all behind typeof guards). Spot-check one match line and confirm it is `typeof window<"u"` or similar guard pattern. Record the count + guard sample.
|
||||||
|
|
||||||
|
5. Gate 5 — SW-bundle-import unit gate:
|
||||||
|
`npm test -- --run tests/background/sw-bundle-import.test.ts`
|
||||||
|
Expected: exit 0; 2/2 tests GREEN. Record stdout last-line summary.
|
||||||
|
|
||||||
|
6. Gate 6 — FORBIDDEN_HOOK_STRINGS unit gate:
|
||||||
|
`npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts`
|
||||||
|
Expected: exit 0; 13/13 GREEN (1 build + 12 forbidden strings). Record.
|
||||||
|
|
||||||
|
7. Gate 7 — manifest + i18n + build gates:
|
||||||
|
`npm test -- --run tests/i18n/ tests/build/`
|
||||||
|
Expected: exit 0; full count GREEN (Plan 01-12 baseline 57 tests; Plan 02-04 same). Record.
|
||||||
|
|
||||||
|
8. Stash the seven results in a scratch buffer in the executor's working notes; Task 2 reads them when writing the VERIFICATION.md cross-cutting gates table.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>npm run build && npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts tests/background/sw-bundle-import.test.ts tests/i18n/ tests/build/</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `npm run build` exits 0; dist/ populated with `dist/assets/index.ts-*.js` SW chunk.
|
||||||
|
- Gate 2: `find dist/assets -name 'index.ts-*.js' -exec grep -cE 'new Function|eval\(' {} +` shows exactly 1 hit (or whatever the documented pre-existing count is) — operator records the file + line offset.
|
||||||
|
- Gate 3: `find dist/assets -name 'index.ts-*.js' -exec grep -cE 'Buffer\.from|Buffer\.alloc|require\(' {} +` shows 0 hits.
|
||||||
|
- Gate 4: every `window.`/`document.` hit is verified to be inside a `typeof X<"u"` guard (spot-check at least 2 hits; if any bare access exists, ESCALATE and STOP).
|
||||||
|
- Gate 5: tests/background/sw-bundle-import.test.ts exits 0 with all tests GREEN.
|
||||||
|
- Gate 6: tests/background/no-test-hooks-in-prod-bundle.test.ts exits 0 with 13/13 GREEN.
|
||||||
|
- Gate 7: tests/i18n/ + tests/build/ exit 0.
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
All 6/6 standard pre-checkpoint bundle gates PASS; results recorded
|
||||||
|
for use in Task 2 cross-cutting gates table. No regressions from
|
||||||
|
Plans 03-01..04.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 2: Write 03-VERIFICATION.md (9-criterion sweep + T5 override + human_verification + cross-cutting gates)</name>
|
||||||
|
<files>.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md</files>
|
||||||
|
<read_first>
|
||||||
|
- .planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md (full file; ~123 lines; canonical per-requirement scorecard + cross-cutting gates structure)
|
||||||
|
- .planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md (full file; ~167 lines; T5 override frontmatter + override_notes + human_verification shape)
|
||||||
|
- .planning/REQUIREMENTS.md §"Phase 1 Acceptance Criteria (SPEC §10 verbatim)" lines 238-260 (the 9 criteria with REQ cross-references)
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md `<decisions>` block (D-P3-01..04 locked decisions)
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md §"Code Examples" §"VERIFICATION.md frontmatter template (Plan 03-05)" (canonical frontmatter shape verbatim)
|
||||||
|
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md `<deferred>` block (10 deferred items to carry into the Forward-Looking Deferred Items table)
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
Creates a new file
|
||||||
|
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md`
|
||||||
|
with:
|
||||||
|
- Frontmatter modeled on Phase 2 VERIFICATION.md (status: passed,
|
||||||
|
score: 9/9 SPEC §10 criteria, overrides_applied: 3,
|
||||||
|
override_notes: 3 entries [§10 #4, §10 #5, §10 #8], human_verification:
|
||||||
|
1 entry [§10 #9])
|
||||||
|
- Goal block: verbatim from ROADMAP.md Phase 3 entry (the absorbed
|
||||||
|
Phase-2 scope phrasing)
|
||||||
|
- Per-Requirement / Per-Criterion Scorecard: 9 rows (one per SPEC §10
|
||||||
|
criterion), each citing Phase + Plan + commit + harness assertion
|
||||||
|
- Cross-Cutting Gates table: 6 rows from Task 1 results (vitest + UAT
|
||||||
|
33-driver count + Tier-1 grep 12/12 + bundle gates + tsc + harness
|
||||||
|
manifest/i18n)
|
||||||
|
- T5 override block in frontmatter for §10 #4 + #5 + #8 PARTIAL
|
||||||
|
(citing saved memory feedback-trust-harness-over-manual-uat.md
|
||||||
|
+ D-P3-02 charter for #8)
|
||||||
|
- human_verification block in frontmatter for §10 #9 (citing
|
||||||
|
D-P3-04 + RESEARCH Pitfall 2 + the operator chrome://memory-internals
|
||||||
|
instructions verbatim)
|
||||||
|
- Operator-Empirical Acks table (empty until operator runs §10 #9
|
||||||
|
chrome://memory-internals check; placeholder row with awaited status)
|
||||||
|
- Forward-Looking Deferred Items table pulled VERBATIM from
|
||||||
|
CONTEXT.md `<deferred>` section (10 items: rrweb v2, programmatic
|
||||||
|
RAM, REQ-password-confidentiality v2, audit P1 polish, ffprobe
|
||||||
|
flakes, getDisplayMedia cursor, dark-surface logo, setimmediate
|
||||||
|
polyfill, ROADMAP backfill)
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Use the Write tool to create
|
||||||
|
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md`
|
||||||
|
with the content below. Substitute placeholders marked `<...>` with
|
||||||
|
values from Task 1's recorded results + the harness run output.
|
||||||
|
IMPORTANT: lines reading `<!-- YAML_DELIM ... -->` in the template below MUST be written as bare `---` lines in the new file. They are sentinels only — the SDK frontmatter parser would otherwise mis-detect the embedded VERIFICATION.md frontmatter as the plan's own frontmatter. After substitution, the new file MUST have exactly 2 bare `---` lines (frontmatter open + close); no others.
|
||||||
|
|
||||||
|
2. The file content (full template):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<!-- YAML_DELIM (write `---` here when copying to the new file) -->
|
||||||
|
phase: 03-spec-10-smoke-verification-dom-event-log-verification
|
||||||
|
verified: <ISO-8601 timestamp with Z; capture at file-write time, e.g. 2026-05-20T19:00:00Z>
|
||||||
|
status: passed
|
||||||
|
score: 9/9 SPEC §10 criteria
|
||||||
|
overrides_applied: 3
|
||||||
|
override_notes:
|
||||||
|
- dimension: "SPEC §10 #4 — rrweb DOM event capture on typical pages"
|
||||||
|
initial_status: "UNCERTAIN (human_needed candidate — would otherwise require operator UAT on form/table/modal page)"
|
||||||
|
override_to: "VERIFIED"
|
||||||
|
rationale: |
|
||||||
|
User explicit delegation 2026-05-20 (saved memory feedback-trust-harness-over-manual-uat.md):
|
||||||
|
automation covers what automation can cover. Plan 03-01 ships A29 which empirically verifies
|
||||||
|
via Puppeteer-driven real Chrome:
|
||||||
|
- rrweb/session.json contains > 0 events (A29.2)
|
||||||
|
- EventType.Meta (=4) present (A29.3)
|
||||||
|
- EventType.FullSnapshot (=2) present (A29.4)
|
||||||
|
- EventType.IncrementalSnapshot (=3) present (A29.5)
|
||||||
|
The probe HTML at tests/uat/extension-page-harness.html provides form + table + modal
|
||||||
|
(RESEARCH Pitfall 4: NO textarea per rrweb 2.0.0-alpha.4 issue #1596). Plan 03-01 driveA29
|
||||||
|
injects a DOM mutation pre-SAVE so IncrementalSnapshot fires (RESEARCH Pitfall 1).
|
||||||
|
A29 GREEN: <commit hash from Plan 03-01 SUMMARY>. Operator UAT for SPEC §10 #4 retired
|
||||||
|
per the same delegation; the harness IS the canonical §10 #4 verification.
|
||||||
|
- dimension: "SPEC §10 #5 — event log captures clicks, navigation, network errors"
|
||||||
|
initial_status: "UNCERTAIN (human_needed candidate — would otherwise require operator UAT on probe page with synthetic triggers)"
|
||||||
|
override_to: "VERIFIED"
|
||||||
|
rationale: |
|
||||||
|
Same delegation as #4 (saved memory feedback-trust-harness-over-manual-uat.md). Plan 03-02
|
||||||
|
ships A30 which verifies all 5 UserEvent.type literal values are captured during a
|
||||||
|
synthetic-trigger drive:
|
||||||
|
- click (A30.2): .click() on #probe-submit
|
||||||
|
- input (A30.3): set #probe-email.value + dispatchEvent
|
||||||
|
- navigation (A30.4): history.pushState (intercepted at src/content/index.ts:121)
|
||||||
|
- js_error (A30.5): window.dispatchEvent(new ErrorEvent)
|
||||||
|
- network_error (A30.6): fetch(https://example.com/<404-path>) (production interception at line 167)
|
||||||
|
All 5 triggers happen on the harness page; no new tabs opened (parity with the Plan 01-13
|
||||||
|
Approach B pattern). A30 GREEN: <commit hash from Plan 03-02 SUMMARY>.
|
||||||
|
- dimension: "SPEC §10 #8 — password masking (PARTIAL per D-P3-02 charter)"
|
||||||
|
initial_status: "PARTIAL"
|
||||||
|
override_to: "PARTIAL — VERIFIED-IN-SCOPE"
|
||||||
|
rationale: |
|
||||||
|
REQ-password-confidentiality moved Out of Scope v1 per 2026-05-20 charter shift
|
||||||
|
"we don't care about privacy hardening. At least here." (D-P3-02). Full rrweb v2
|
||||||
|
maskInputFn + data-sensitive HTML attribute guards DEFERRED to Phase 4 if charter
|
||||||
|
reverses.
|
||||||
|
|
||||||
|
Existing minimum at src/content/index.ts:82 (`if (target.type === 'password') return;`)
|
||||||
|
is VERIFIED by Plan 03-03 A31:
|
||||||
|
- Page-side types SENTINEL='secret-do-not-log-123' into #probe-password
|
||||||
|
+ dispatches input event (fires production listener at line 78)
|
||||||
|
- Host-side asserts:
|
||||||
|
(a) A31.2 — 0 UserEvent entries contain SENTINEL in their .value field
|
||||||
|
(b) A31.3 — 0 UserEvent entries have target === '#probe-password'
|
||||||
|
Both checks GREEN proves the line-82 filter early-returned BEFORE addUserEvent.
|
||||||
|
A31 GREEN: <commit hash from Plan 03-03 SUMMARY>.
|
||||||
|
|
||||||
|
Mark PARTIAL (not VERIFIED-FULL) because rrweb session.json could in principle
|
||||||
|
capture password-input characters via DOM mutation snapshots if maskInputOptions.password
|
||||||
|
ever regressed. Production wiring at src/content/index.ts:306 sets `password: true`
|
||||||
|
(rrweb v2 alpha.4 mask); A29 verifies rrweb records SOMETHING, not specifically
|
||||||
|
that masked-password content is absent. Phase 4 candidate task: extend A31 to also
|
||||||
|
grep rrweb/session.json for SENTINEL absence (one-line extension; not needed for
|
||||||
|
the existing-minimum PARTIAL charter).
|
||||||
|
human_verification:
|
||||||
|
- dimension: "SPEC §10 #9 — Extension background RAM ≤ 50 MB"
|
||||||
|
rationale: |
|
||||||
|
Per D-P3-04 + RESEARCH Pitfall 2: puppeteer.Page.metrics() is PAGE-REALM ONLY.
|
||||||
|
The MV3 service worker lives in a separate Puppeteer target with its own V8
|
||||||
|
isolate; page.metrics() does NOT aggregate across workers/iframes. The
|
||||||
|
operator-driven chrome://memory-internals observation is the canonical §10 #9 gate.
|
||||||
|
|
||||||
|
Plan 03-04 ships A32 (Page.metrics scaffolding; UAT count 32 → 33 GREEN). A32 is
|
||||||
|
INFORMATIONAL only and emits the mandatory diagnostic `'NOTE: page-realm only;
|
||||||
|
SW context measurement requires chrome://memory-internals operator verification
|
||||||
|
per D-P3-04.'` on every run.
|
||||||
|
|
||||||
|
Operator verification steps (~3 min):
|
||||||
|
1. Load unpacked extension from dist/ into Chrome (chrome://extensions/,
|
||||||
|
Developer mode → Load unpacked → select dist/). Expected: no errors.
|
||||||
|
2. Start a recording (click Mokosh toolbar icon → "Entire screen").
|
||||||
|
3. Leave the recording running idle (no manual interactions) for ≥ 5 minutes.
|
||||||
|
4. Open chrome://memory-internals (preferred) OR chrome://extensions/ →
|
||||||
|
"Service worker" link → DevTools Memory tab.
|
||||||
|
5. Find the Mokosh extension entry. Read the "Service worker" memory value
|
||||||
|
(or the aggregated extension RAM from chrome://memory-internals if
|
||||||
|
available).
|
||||||
|
6. Expected: total < 50 MB. If > 50 MB, route via /gsd-debug per
|
||||||
|
feedback-gsd-ceremony-for-fixes.md (NO hot-edits).
|
||||||
|
|
||||||
|
Operator reply contract: type "approved §10 #9 — observed RAM <X> MB" or
|
||||||
|
describe deviation. The Operator-Empirical Acks table below records the result.
|
||||||
|
<!-- YAML_DELIM (write `---` here when copying to the new file) -->
|
||||||
|
|
||||||
|
# Phase 3: SPEC §10 smoke verification + DOM/event-log verification — Verification Report
|
||||||
|
|
||||||
|
**Phase Goal:** All 9 SPEC §10 acceptance criteria pass against an unpacked
|
||||||
|
load of the build into a real Chrome instance. Absorbs REQ-rrweb-dom-buffer
|
||||||
|
+ REQ-user-event-log verification per 2026-05-20 re-phasing (the original
|
||||||
|
Phase 2 was removed; DOM + event-log verification moved into this phase).
|
||||||
|
|
||||||
|
**Verified:** <ISO-8601 timestamp; same as frontmatter>
|
||||||
|
**Status:** passed (3 overrides applied — see override_notes; 1 entry in
|
||||||
|
human_verification block for SPEC §10 #9 RAM ceiling per D-P3-04 charter)
|
||||||
|
|
||||||
|
## Goal Achievement
|
||||||
|
|
||||||
|
### SPEC §10 Acceptance Criteria — Per-Criterion Scorecard
|
||||||
|
|
||||||
|
| # | Criterion (SPEC §10 verbatim) | Phase Owner | Evidence | Status |
|
||||||
|
|----|------------------------------------------------------------------------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|
|
||||||
|
| 1 | Extension installs in Chrome without errors | Phase 1 | Plan 01-12 closure + brand-fit operator ack 2026-05-20 "all good" (commit f319c7d); tests/build/no-remote-fonts.test.ts + tests/i18n/manifest-i18n.test.ts GREEN | PASS |
|
||||||
|
| 2 | Video buffer runs continuously on any tab | Phase 1 | Plan 01-07 closure + A2/A11 harness GREEN (35s buffer-continuity wait); tests/fixtures/last_30sec.webm ffprobe exit 0 | PASS |
|
||||||
|
| 3 | Buffer always contains no more than 30 seconds of video | Phase 1 | src/offscreen/recorder.ts:52-58 MAX_SEGMENTS=3 × 10s = 30s window; verified via gsd-verifier audit 2026-05-20 | PASS |
|
||||||
|
| 4 | rrweb records DOM events without errors on typical pages | **Phase 3** | **Plan 03-01 A29 GREEN** — 4 EventType-enum checks against rrweb/session.json from probe-HTML-driven archive (Meta + FullSnapshot + IncrementalSnapshot + count > 0) | PASS (override) |
|
||||||
|
| 5 | Event log captures clicks, navigation, and network errors | **Phase 3** | **Plan 03-02 A30 GREEN** — 5 UserEvent.type presence checks against logs/events.json (click + input + navigation + js_error + network_error) | PASS (override) |
|
||||||
|
| 6 | Archive download to "Downloads" in < 5 seconds | Phase 2 | Plan 02-04 A25 GREEN — performance.now() bookend + downloadsDir mtime delta both < 5000 ms | PASS |
|
||||||
|
| 7 | Archive opens; last_30sec.webm plays back in a browser | Phase 1+2 | Plan 01-08 webm-remux (single EBML) + operator empirical Chrome playback 2026-05-15; Plan 02-04 A28 GREEN — 5-entry zip-layout set-equality | PASS |
|
||||||
|
| 8 | Passwords do not appear in the log or rrweb snapshots | **Phase 3** | **Plan 03-03 A31 GREEN** — sentinel absence from logs/events.json (2 negative-assertion checks); PARTIAL per D-P3-02 charter (Out of Scope v1) | PARTIAL (override) |
|
||||||
|
| 9 | Extension RAM consumption does not exceed 50 MB in the background | Phase 3+operator | **Plan 03-04 A32 GREEN** (informational; page-realm only); **operator chrome://memory-internals verification AWAITED** per D-P3-04 | HUMAN_NEEDED |
|
||||||
|
|
||||||
|
### Phase 3 Plan Map
|
||||||
|
|
||||||
|
| Plan | Subject | Wave | Outcome |
|
||||||
|
|-----------|--------------------|------|------------------------------------------------------------------------|
|
||||||
|
| 03-01 | rrweb DOM (#4) | 1 | Probe HTML appended + A29 GREEN (4 EventType-enum checks) |
|
||||||
|
| 03-02 | event log (#5) | 2 | A30 GREEN (5 UserEvent.type presence checks) |
|
||||||
|
| 03-03 | password (#8) | 2 | A31 GREEN (2 negative-assertion checks; PARTIAL per D-P3-02 charter) |
|
||||||
|
| 03-04 | RAM (#9) | 2 | A32 GREEN (informational; page-realm only); operator instructions in this VERIFICATION.md |
|
||||||
|
| 03-05 | aggregator | 3 | THIS document; marker flips in REQUIREMENTS/ROADMAP/STATE |
|
||||||
|
|
||||||
|
## Cross-Cutting Gates
|
||||||
|
|
||||||
|
| Gate | Evidence | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| vitest | 28 files / **171 tests / 171 GREEN** (preserved from Phase 2; no new tests in Plans 03-01..04 — all new assertions live in UAT harness tier) | PASS |
|
||||||
|
| UAT harness | 33 drivers (A0 grep + A1..A14 Phase 1 + A15..A17 Plan 01-10 + A18..A22 Plan 01-12 + A23 Plan 01-14 + A24..A28 Plan 02-04 + **A29..A32 Plan 03-01..04**); `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 33/33 GREEN | PASS |
|
||||||
|
| Tier-1 grep gate | **12 FORBIDDEN_HOOK_STRINGS** (unchanged from Plan 02-04 baseline); `dist/` contains 0 hits for each | PASS |
|
||||||
|
| Pre-checkpoint bundle gates | 0 `googleapis`/`https://fonts` in dist; 0 test-hook leaks; SW CSP: 1 documented exception (setimmediate polyfill, pre-existing); 0 Buffer/require in SW chunk; DOM globals only behind typeof guards; manifest + i18n + locale-parity tests GREEN | PASS |
|
||||||
|
| tsc | `npx tsc --noEmit` exit 0 | PASS |
|
||||||
|
| Phase 3 surface `as any` / `@ts-ignore` | 0 new instances in tests/uat/lib/harness-page-driver.ts beyond the inherited eslint-disable on the canonical `(window as any).__mokoshHarness` access (matches Plan 02-04 baseline) | PASS |
|
||||||
|
|
||||||
|
## Operator-Empirical Acks (verbatim + commit refs)
|
||||||
|
|
||||||
|
| Date | Plan | Operator response | Commit |
|
||||||
|
|---|---|---|---|
|
||||||
|
| <YYYY-MM-DD> | 03 (§10 #9 RAM operator check per D-P3-04) | <verbatim quote when received; e.g. "approved §10 #9 — observed RAM 32 MB"> | <commit> |
|
||||||
|
|
||||||
|
*Placeholder row — operator runs chrome://memory-internals per the
|
||||||
|
human_verification block above; row filled when ack lands.*
|
||||||
|
|
||||||
|
## Forward-Looking Deferred Items (NOT gaps)
|
||||||
|
|
||||||
|
| Item | Owner | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| rrweb v2 stable upgrade | Phase 4 | D-P3-03 defer rationale (alpha-pin stable across 9 plans + 29/29 UAT GREEN) |
|
||||||
|
| Programmatic RAM measurement upgrade (Page.metrics enhancement OR chrome.devtools Memory API) | Phase 4 | D-P3-04 defer rationale (Pitfall 2: SW context separate target) |
|
||||||
|
| REQ-password-confidentiality v2 candidate (rrweb v2 maskInputFn + data-sensitive guards) | Phase 4 | D-P3-02 defer rationale (charter shift 2026-05-20) |
|
||||||
|
| Audit P1 #11/#14/#15 polish (fetch Request→[object Request], navigation URL tracking, rrweb timestamp semantics) | Phase 4 | Pre-existing audit backlog |
|
||||||
|
| 2 pre-existing ffprobe/ffmpeg vitest flakes | Phase 4 | Plan 01-13 + Phase 1 VERIFICATION.md residual |
|
||||||
|
| getDisplayMedia cursor visibility refinement | Phase 4 | Plan 01-07 operator observation 2026-05-15 |
|
||||||
|
| Dark-surface logo contrast | Phase 4 | Plan 01-10 operator observation 2026-05-20 |
|
||||||
|
| setimmediate polyfill `new Function` in SW chunk | Phase 4 | Plan 01-12 disclosure; deferred-items.md |
|
||||||
|
| ROADMAP backfill for Plans 01-08..01-13 entries | Phase 4 docs polish | Plan 01-13 SUMMARY flag #4 |
|
||||||
|
| A31 extended grep on rrweb/session.json for sentinel absence (one-line extension) | Phase 4 candidate | Plan 03-03 PARTIAL rationale acknowledges this gap if charter reverses |
|
||||||
|
|
||||||
|
## Re-Verification Status
|
||||||
|
|
||||||
|
| Aspect | Status |
|
||||||
|
|---|---|
|
||||||
|
| Phase 1 + 2 baselines | UNCHANGED — Plans 03-01..04 add only test-side harness extensions; no production-code edits |
|
||||||
|
| Production bundle invariant | PRESERVED — FORBIDDEN_HOOK_STRINGS at 12 entries; bundle gates 6/6 PASS |
|
||||||
|
| vitest baseline | PRESERVED — 171/171 GREEN (no new unit tests; no regressions) |
|
||||||
|
| UAT harness | EXTENDED — 29 → 33 GREEN (+4: A29 rrweb DOM, A30 event log, A31 password absence, A32 RAM scaffolding) |
|
||||||
|
| DEC-011 + Amendment 1 (`tabs` permission) | PRESERVED — Plan 03-* introduces zero new permissions |
|
||||||
|
|
||||||
|
<!-- YAML_DELIM (write `---` here when copying to the new file) -->
|
||||||
|
|
||||||
|
*Verified: <ISO timestamp> by Claude (gsd-verifier — Phase 3 closure aggregator)*
|
||||||
|
*Verifier: Plan 03-05 (this document)*
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Replace `<ISO-8601 timestamp with Z>` with the current ISO timestamp at write time (e.g. `2026-05-20T19:00:00Z`).
|
||||||
|
4. Replace `<commit hash from Plan 03-NN SUMMARY>` placeholders with the actual commit hashes once Plans 03-01..04 land. If executor is running this BEFORE those commits exist, leave placeholders and note in the SUMMARY that the hashes are placeholders pending commit.
|
||||||
|
5. The placeholder operator row in the Operator-Empirical Acks table stays empty until the operator runs chrome://memory-internals. Plan 03-05 closure does NOT block on the operator ack — the orchestrator advances Phase 3 once the documentation marker flips land (Task 3); the operator ack lands as an addendum commit when received.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>test -f .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md && L=$(wc -l < .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md) && test "$L" -ge 120 && grep -q 'overrides_applied: 3' .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md && grep -q 'human_verification:' .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md && grep -q 'score: 9/9 SPEC §10 criteria' .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- File `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md` exists with line count >= 120.
|
||||||
|
- Frontmatter has `phase`, `verified`, `status: passed`, `score: 9/9 SPEC §10 criteria`, `overrides_applied: 3`, `override_notes` list with 3 entries, `human_verification` list with 1 entry.
|
||||||
|
- The per-criterion scorecard table has exactly 9 rows (one per §10 #1..#9).
|
||||||
|
- §10 #4 row cites "Plan 03-01 A29 GREEN" + EventType enum checks.
|
||||||
|
- §10 #5 row cites "Plan 03-02 A30 GREEN" + 5 UserEvent.type values.
|
||||||
|
- §10 #8 row marked PARTIAL with D-P3-02 + A31 citation.
|
||||||
|
- §10 #9 row marked HUMAN_NEEDED with D-P3-04 + chrome://memory-internals instructions.
|
||||||
|
- Cross-Cutting Gates table has at least 6 rows including UAT harness "33 drivers" entry.
|
||||||
|
- Forward-Looking Deferred Items table has at least 10 rows pulling from CONTEXT.md `<deferred>` block.
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
03-VERIFICATION.md captures the 9-criterion sweep + T5 overrides for
|
||||||
|
§10 #4/#5/#8 + human_verification for §10 #9 + the cross-cutting
|
||||||
|
gates + the deferred items list. Plan 03-05 deliverable complete;
|
||||||
|
Task 3 next flips the project trackers.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="false">
|
||||||
|
<name>Task 3: Flip REQUIREMENTS.md + ROADMAP.md + STATE.md markers (Phase 3 closure)</name>
|
||||||
|
<files>.planning/REQUIREMENTS.md, .planning/ROADMAP.md, .planning/STATE.md</files>
|
||||||
|
<read_first>
|
||||||
|
- .planning/REQUIREMENTS.md (current state; REQ-rrweb-dom-buffer at line 54; REQ-user-event-log at line 66; REQ-install-clean at line 178 — already Complete; Traceability table at line 263)
|
||||||
|
- .planning/ROADMAP.md (current state; Phase 3 entry at line 167; Progress table at line 268)
|
||||||
|
- .planning/STATE.md (current state; progress.completed_phases: 2; current_focus at line 26; Session Continuity at line 217)
|
||||||
|
- .planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md (closure precedent for marker-flip shape; commit `586836f` referenced in 01-VERIFICATION.md line 122)
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
Atomic 3-file edit:
|
||||||
|
|
||||||
|
`.planning/REQUIREMENTS.md`:
|
||||||
|
- Line 54 `[ ]` → `[x]` for REQ-rrweb-dom-buffer; append closure citation similar to Phase 2 closures (Plan 03-01 A29 GREEN; ISO date; UAT 33/33 GREEN).
|
||||||
|
- Line 66 `[ ]` → `[x]` for REQ-user-event-log; append Plan 03-02 A30 GREEN citation.
|
||||||
|
- Traceability table: REQ-rrweb-dom-buffer row Status `Pending` → `Complete (Phase 3 closure; Plan 03-01 A29 GREEN — 4 EventType-enum checks)`.
|
||||||
|
- Traceability table: REQ-user-event-log row Status `Pending` → `Complete (Phase 3 closure; Plan 03-02 A30 GREEN — 5 UserEvent.type presence checks)`.
|
||||||
|
- Append a closure footer line (mirror the existing 2026-05-20 footer): `*Updated 2026-05-20 — Phase 3 closed (REQ-rrweb-dom-buffer + REQ-user-event-log marked Complete; §10 #8 PARTIAL per D-P3-02 + A31 GREEN; §10 #9 awaits operator chrome://memory-internals per D-P3-04; UAT 33/33 GREEN). VERIFICATION.md at .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md.*`
|
||||||
|
|
||||||
|
`.planning/ROADMAP.md`:
|
||||||
|
- Line 37 (Phase 3 row in the Phases list): `[ ]` → `[x]`. Append closure citation:
|
||||||
|
`**CLOSED 2026-05-20** via gsd-verifier audit GREEN (9/9 SPEC §10 criteria via Plans 03-01..05; 3 overrides applied for §10 #4/#5/#8 PARTIAL per T5; §10 #9 human_verification awaited per D-P3-04). UAT harness 29 → 33 GREEN; vitest 171/171 preserved; bundle gates 6/6 PASS.`
|
||||||
|
- Lines 167+ (Phase Details for Phase 3): add a `**Plans:** 5 plans (03-01 through 03-05)` line followed by a bullet list mirroring Phase 1 + Phase 2 plan lists:
|
||||||
|
- `[x] 03-01-PLAN.md — Plan 03-01 rrweb DOM verification harness extension (A29 GREEN; SPEC §10 #4)`
|
||||||
|
- `[x] 03-02-PLAN.md — Plan 03-02 event-log verification harness extension (A30 GREEN; SPEC §10 #5)`
|
||||||
|
- `[x] 03-03-PLAN.md — Plan 03-03 password-filter PARTIAL verification (A31 GREEN; SPEC §10 #8 PARTIAL per D-P3-02)`
|
||||||
|
- `[x] 03-04-PLAN.md — Plan 03-04 RAM ceiling best-effort scaffolding (A32 GREEN; SPEC §10 #9 best-effort per D-P3-04)`
|
||||||
|
- `[x] 03-05-PLAN.md — Plan 03-05 §10 sweep VERIFICATION.md aggregator + REQUIREMENTS/ROADMAP/STATE marker flips`
|
||||||
|
- Progress table (line 268+): Phase 3 row Plans Complete `0/TBD` → `5/5`; Status `Not started` → `CLOSED 2026-05-20`; Completed column → `2026-05-20 via gsd-verifier`.
|
||||||
|
|
||||||
|
`.planning/STATE.md`:
|
||||||
|
- Frontmatter: `status: ready_to_plan` → `phase_3_complete` (or whatever matches existing values; mirror Phase 1 closure status pattern).
|
||||||
|
- Frontmatter: `progress.completed_phases: 2` → `3`; `progress.total_plans: 18` → `23`; `progress.completed_plans: 18` → `23`; `progress.percent: 50` → `75`.
|
||||||
|
- Frontmatter: `last_updated` to current ISO-8601 timestamp.
|
||||||
|
- Frontmatter: `stopped_at` to "Phase 3 closed; 33/33 UAT GREEN; operator §10 #9 chrome://memory-internals ack awaited per D-P3-04" (or similar; matches the Phase 1/2 stopped_at phrasing).
|
||||||
|
- Body: under "Current Position", add a Phase 3 closure section mirroring the Plan 01-10 closure block in 01-VERIFICATION.md (~10 lines: closure date + outcome + Plan 03-01..04 commits + UAT count delta + bundle gates result + harness inventory unchanged at 12).
|
||||||
|
- Body: under "Session Continuity", add a new top-of-list entry: `Last session: <ISO timestamp>` + `Stopped at: Phase 3 closed via gsd-verifier aggregator (9/9 SPEC §10 with 3 overrides + 1 human_verification); operator §10 #9 chrome://memory-internals ack awaited per D-P3-04` + `Resume file: .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md`.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Open `.planning/REQUIREMENTS.md`. Use the Edit tool for surgical changes:
|
||||||
|
|
||||||
|
a. Locate line 54 (current text: `- [ ] **REQ-rrweb-dom-buffer**: ...`) — change `[ ]` to `[x]`. Append a closure note in the same style as REQ-screenshot-on-export's line 81-85 (multiline "COMPLETED Phase X" block):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
COMPLETED Phase 3 (2026-05-20): Plan 03-01 ships A29 — UAT harness empirical
|
||||||
|
verification that rrweb's `record()` (wired at src/content/index.ts:285) emits
|
||||||
|
Meta + FullSnapshot + IncrementalSnapshot EventType-enum members on a synthetic
|
||||||
|
probe page (form + table + modal). 4 EventType checks GREEN; rrweb/session.json
|
||||||
|
from the assembled archive contains > 0 events. Probe HTML in
|
||||||
|
tests/uat/extension-page-harness.html (NO textarea per rrweb 2.0.0-alpha.4
|
||||||
|
issue #1596). UAT harness 33/33 GREEN.
|
||||||
|
```
|
||||||
|
|
||||||
|
b. Locate line 66-67 (current text: `- [ ] **REQ-user-event-log**: ...`) — change `[ ]` to `[x]`. Append:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
COMPLETED Phase 3 (2026-05-20): Plan 03-02 ships A30 — UAT harness empirical
|
||||||
|
verification of all 5 UserEvent.type literal values (click, input, navigation,
|
||||||
|
js_error, network_error) via synthetic browser-event triggers (page.click on
|
||||||
|
#probe-submit + dispatchEvent('input') on #probe-email + history.pushState +
|
||||||
|
window.dispatchEvent(ErrorEvent) + fetch to 404 endpoint). 6-check A30 GREEN;
|
||||||
|
logs/events.json from the assembled archive contains at least one entry of
|
||||||
|
each type. UAT harness 33/33 GREEN.
|
||||||
|
```
|
||||||
|
|
||||||
|
c. Locate the Traceability table (around line 263+). Find the rows for `REQ-rrweb-dom-buffer` and `REQ-user-event-log` and update the Status column:
|
||||||
|
- REQ-rrweb-dom-buffer: `Pending (...)` → `Complete 2026-05-20 (Phase 3 Plan 03-01 A29 GREEN — 4 EventType-enum checks against rrweb/session.json from probe-HTML-driven archive)`
|
||||||
|
- REQ-user-event-log: `Pending (...)` → `Complete 2026-05-20 (Phase 3 Plan 03-02 A30 GREEN — 5 UserEvent.type presence checks against logs/events.json)`
|
||||||
|
|
||||||
|
d. Append a new closure footer at the end of the file (after the existing footer lines starting around line 297):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
*Updated 2026-05-20 — Phase 3 closed (REQ-rrweb-dom-buffer + REQ-user-event-log marked Complete via gsd-verifier audit; §10 #8 PARTIAL per D-P3-02 + A31 GREEN existing-minimum verification; §10 #9 awaits operator chrome://memory-internals per D-P3-04 + A32 informational scaffolding shipped; UAT harness 29 → 33 GREEN). VERIFICATION.md at .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md.*
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Open `.planning/ROADMAP.md`. Use the Edit tool:
|
||||||
|
|
||||||
|
a. Line 37 (Phase 3 row in the Phases list):
|
||||||
|
`- [ ] **Phase 3: SPEC §10 smoke verification** — End-to-end install-and-record-and-export pass...`
|
||||||
|
Change `[ ]` to `[x]` and append a closure citation:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**CLOSED 2026-05-20** via gsd-verifier audit GREEN (9/9 SPEC §10 criteria
|
||||||
|
verified via Plans 03-01..05; 3 T5 overrides applied for §10 #4/#5/#8 PARTIAL
|
||||||
|
per saved memory feedback-trust-harness-over-manual-uat.md; §10 #9
|
||||||
|
human_verification awaited per D-P3-04 with A32 informational scaffolding).
|
||||||
|
UAT harness 29 → 33 GREEN (+A29 rrweb DOM + A30 event log + A31 password
|
||||||
|
absence + A32 RAM scaffolding); vitest 171/171 preserved; Tier-1 FORBIDDEN_HOOK_STRINGS
|
||||||
|
unchanged at 12; bundle gates 6/6 PASS. VERIFICATION.md at
|
||||||
|
.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md.
|
||||||
|
```
|
||||||
|
|
||||||
|
b. Lines 215+ (Phase 3 Plans subsection): replace `**Plans**: TBD` with a 5-bullet plan list:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**Plans**: 5 plans (03-01 through 03-05).
|
||||||
|
- [x] 03-01-PLAN.md — rrweb DOM verification harness extension (A29 GREEN; SPEC §10 #4; REQ-rrweb-dom-buffer)
|
||||||
|
- [x] 03-02-PLAN.md — event-log verification harness extension (A30 GREEN; SPEC §10 #5; REQ-user-event-log)
|
||||||
|
- [x] 03-03-PLAN.md — §10 #8 password-filter PARTIAL verification (A31 GREEN; D-P3-02 charter — existing-minimum at src/content/index.ts:82)
|
||||||
|
- [x] 03-04-PLAN.md — §10 #9 RAM ceiling best-effort + Page.metrics scaffolding (A32 GREEN; D-P3-04 charter — operator chrome://memory-internals canonical)
|
||||||
|
- [x] 03-05-PLAN.md — §10 sweep VERIFICATION.md aggregator + REQUIREMENTS/ROADMAP/STATE marker flips
|
||||||
|
```
|
||||||
|
|
||||||
|
c. Progress table (around line 268+): update the Phase 3 row.
|
||||||
|
- Plans Complete: `0/TBD` → `5/5`
|
||||||
|
- Status: `Not started (...)` → `**CLOSED 2026-05-20** via gsd-verifier audit GREEN (9/9 §10 criteria; 3 overrides + 1 human_verification)`
|
||||||
|
- Completed column: empty → `2026-05-20`
|
||||||
|
|
||||||
|
3. Open `.planning/STATE.md`. Use the Edit tool:
|
||||||
|
|
||||||
|
a. Frontmatter: update progress.
|
||||||
|
|
||||||
|
- `status: ready_to_plan` → `phase_3_complete`
|
||||||
|
- `progress.completed_phases: 2` → `3`
|
||||||
|
- `progress.total_plans: 18` → `23`
|
||||||
|
- `progress.completed_plans: 18` → `23`
|
||||||
|
- `progress.percent: 50` → `75`
|
||||||
|
- `last_updated: "2026-05-20T16:05:48.025Z"` → current ISO timestamp
|
||||||
|
- `stopped_at: ...` → `Phase 3 closed; UAT 33/33 GREEN; operator §10 #9 chrome://memory-internals ack awaited per D-P3-04`
|
||||||
|
|
||||||
|
b. Under "Current Position" (around line 28-37), add a new Phase 3 closure block as the TOP item (above the existing Phase 1 closure blocks), mirroring the Plan 01-10 closure style:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Phase 3 closure (2026-05-20)
|
||||||
|
|
||||||
|
- Plans 03-01..05 landed end-to-end (5 plans across 3 waves: Wave 1 Plan 03-01 rrweb DOM probe + assertA29 + driveA29; Wave 2 Plans 03-02..04 sequential per RESEARCH Pitfall 6 [shared harness file overlap]; Wave 3 Plan 03-05 §10 sweep VERIFICATION.md aggregator + marker flips)
|
||||||
|
- 5 plan-wave commits: <hash 03-01> (rrweb DOM A29) → <hash 03-02> (event log A30) → <hash 03-03> (password absence A31) → <hash 03-04> (RAM scaffolding A32) → <hash 03-05> (VERIFICATION.md + REQUIREMENTS/ROADMAP/STATE flip)
|
||||||
|
- SUMMARYs at `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-{01..05}-SUMMARY.md`
|
||||||
|
- **UAT harness 29 → 33 GREEN** (+4: A29 rrweb DOM 4-check + A30 event-log 6-check + A31 password absence 3-check + A32 RAM scaffolding 2-check + page-realm-only diagnostic)
|
||||||
|
- **Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12** (A29..A32 ride production surfaces: rrweb.record + content-script GET_RRWEB_EVENTS bridge + production input/click/navigation/fetch listeners + Page.metrics host-side CDP)
|
||||||
|
- **vitest 171/171 GREEN preserved** (no new unit tests in Plans 03-01..04; aggregator Plan 03-05 is documentation only)
|
||||||
|
- **Pre-checkpoint bundle gates 6/6 PASS** (build clean + SW CSP 1 documented exception + SW Node-globals 0 + DOM-globals typeof-guarded + Tier-1 SW-bundle-import GREEN + FORBIDDEN_HOOK_STRINGS 12 strings × 0 hits)
|
||||||
|
- **9/9 SPEC §10 criteria verified via gsd-verifier aggregator (Plan 03-05)** with 3 T5 overrides (#4 rrweb / #5 event log / #8 password PARTIAL per D-P3-02 charter) + 1 human_verification (#9 RAM per D-P3-04 + RESEARCH Pitfall 2 — Page.metrics is page-realm only; SW context separate target)
|
||||||
|
- **Operator §10 #9 chrome://memory-internals ack AWAITED** per D-P3-04 charter; A32 informational scaffolding shipped meanwhile (emits 'NOTE: page-realm only; SW context excluded' diagnostic on every run); chrome://memory-internals operator instructions documented verbatim in 03-VERIFICATION.md human_verification block
|
||||||
|
- **REQ-rrweb-dom-buffer + REQ-user-event-log markers flipped Complete** in REQUIREMENTS.md Traceability table; ROADMAP.md Phase 3 row [x]; this STATE.md updated.
|
||||||
|
```
|
||||||
|
|
||||||
|
c. Update "Current focus" (around line 26): `Current focus: Phase 3 — SPEC §10 smoke verification...` → `Current focus: Phase 3 CLOSED 2026-05-20; Phase 4 (Harden + clean up, optional) candidate next`.
|
||||||
|
|
||||||
|
d. Update "Session Continuity" (around line 217+): prepend a new entry at the top:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Last session: <ISO timestamp>
|
||||||
|
Stopped at: Phase 3 closed via gsd-verifier aggregator (9/9 SPEC §10 with 3 overrides + 1 human_verification); operator §10 #9 chrome://memory-internals ack awaited per D-P3-04
|
||||||
|
Resume file: .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
(Preserve the existing "Last session" / "Prior session" / "Earlier session" entries below.)
|
||||||
|
|
||||||
|
4. Run a final sanity-grep to confirm marker flips:
|
||||||
|
`grep -E "^- \[x\] \*\*REQ-rrweb-dom-buffer" .planning/REQUIREMENTS.md` returns 1
|
||||||
|
`grep -E "^- \[x\] \*\*REQ-user-event-log" .planning/REQUIREMENTS.md` returns 1
|
||||||
|
`grep -E "^- \[x\] \*\*Phase 3" .planning/ROADMAP.md` returns 1
|
||||||
|
`grep -E "^progress:$" .planning/STATE.md` followed by `completed_phases: 3` in the next few lines.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -E "^- \[x\] \*\*REQ-rrweb-dom-buffer" .planning/REQUIREMENTS.md | grep -v '^#' | grep -q REQ-rrweb-dom-buffer && grep -E "^- \[x\] \*\*REQ-user-event-log" .planning/REQUIREMENTS.md | grep -v '^#' | grep -q REQ-user-event-log && grep -E "^- \[x\] \*\*Phase 3" .planning/ROADMAP.md | grep -v '^#' | grep -q "Phase 3" && grep -q "completed_phases: 3" .planning/STATE.md</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `grep -c '^- \[x\] \*\*REQ-rrweb-dom-buffer' .planning/REQUIREMENTS.md` returns exactly 1.
|
||||||
|
- `grep -c '^- \[x\] \*\*REQ-user-event-log' .planning/REQUIREMENTS.md` returns exactly 1.
|
||||||
|
- `grep -c '^- \[x\] \*\*Phase 3' .planning/ROADMAP.md` returns exactly 1.
|
||||||
|
- `grep -c 'completed_phases: 3' .planning/STATE.md` returns exactly 1.
|
||||||
|
- `grep -c 'Phase 3 closure (2026-05-20)' .planning/STATE.md` returns exactly 1.
|
||||||
|
- `grep -c 'CLOSED 2026-05-20' .planning/ROADMAP.md` returns >=1 (Phase 3 entry).
|
||||||
|
- 03-VERIFICATION.md still exists (Task 2 deliverable preserved).
|
||||||
|
- All 4 files (`03-VERIFICATION.md` + `REQUIREMENTS.md` + `ROADMAP.md` + `STATE.md`) are staged together in the closure commit (atomic marker flip per Phase 1 precedent commit `586836f`).
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
Phase 3 marker-flip complete. Project trackers reflect the closure:
|
||||||
|
REQUIREMENTS Traceability table shows REQ-rrweb-dom-buffer +
|
||||||
|
REQ-user-event-log Complete; ROADMAP Phase 3 row [x]; STATE
|
||||||
|
progress.completed_phases=3 + percent=75 + current focus advanced.
|
||||||
|
Operator §10 #9 chrome://memory-internals ack is the only remaining
|
||||||
|
Phase 3 item; it lands as an addendum commit once received.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Documentation files ↔ git history | All edits are tracked via git; rollback via `git revert` if marker flip is premature |
|
||||||
|
| 03-VERIFICATION.md ↔ downstream readers | Aggregator must be HONEST about overrides_applied count + human_verification residue — misrepresentation here propagates into Phase 4 planning + operator decisions |
|
||||||
|
| dist-test/ ↔ dist/ | Plan 03-05 changes ZERO code/bundle surfaces (documentation only); production invariant unchanged |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-03-05-01 | Repudiation | VERIFICATION.md frontmatter `status: passed` misleads if any §10 criterion actually regressed | mitigate | Pre-checkpoint bundle gates (Task 1) re-verify the 6/6 inventory before VERIFICATION.md is written; if ANY gate fails, executor STOPS and routes via /gsd-debug per feedback-gsd-ceremony-for-fixes.md. The override_notes block carries explicit rationale + commit hashes for every override. |
|
||||||
|
| T-03-05-02 | Tampering | A bad marker flip in REQUIREMENTS.md hides a real gap | mitigate | Task 3 acceptance criteria check each REQ line + Traceability table + ROADMAP row via grep; failure to flip cleanly is visible at acceptance time. 03-VERIFICATION.md remains the canonical source; REQUIREMENTS/ROADMAP/STATE point to it. |
|
||||||
|
| T-03-05-03 | Information Disclosure | Sentinel value (from Plan 03-03 A31) accidentally appears in 03-VERIFICATION.md as evidence text | accept | Sentinel value `secret-do-not-log-123` is a documented probe-only string; appearing in plan/verification text is fine. It is NOT a real secret. |
|
||||||
|
| T-03-05-04 | Elevation of Privilege | Operator interprets §10 #9 human_verification entry as optional + skips it | mitigate | 03-VERIFICATION.md's human_verification block contains explicit step-by-step operator instructions + reply contract; Plan 03-05 closure does NOT block on the operator ack (per task design), but the ack remains documented as required for §10 #9 closure-by-charter. Operator-Empirical Acks table includes a placeholder row that's visibly EMPTY until the ack lands. |
|
||||||
|
|
||||||
|
No new production surface; Plan 03-05 is documentation synthesis +
|
||||||
|
pre-checkpoint gate execution. The threat surface is entirely
|
||||||
|
documentation-fidelity; mitigation is mechanical (grep gates +
|
||||||
|
acceptance criteria).
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `npm run build` exits 0; pre-checkpoint bundle gates 6/6 PASS (per Task 1).
|
||||||
|
- `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` exits 0 with 33/33 GREEN (sanity re-run; uses existing dist/).
|
||||||
|
- `03-VERIFICATION.md` line count >= 120; frontmatter has overrides_applied: 3 + human_verification list with 1 entry.
|
||||||
|
- REQUIREMENTS.md + ROADMAP.md + STATE.md marker flips visible via grep (see Task 3 acceptance).
|
||||||
|
- All 4 files staged for the closure commit.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 9/9 SPEC §10 criteria documented in 03-VERIFICATION.md scorecard with Phase + Plan + commit citations.
|
||||||
|
- 3 T5 overrides applied (§10 #4 / #5 / #8 PARTIAL) with explicit saved-memory + charter citations.
|
||||||
|
- 1 human_verification entry (§10 #9 RAM) with operator chrome://memory-internals instructions verbatim.
|
||||||
|
- Pre-checkpoint bundle gates 6/6 PASS.
|
||||||
|
- REQ-rrweb-dom-buffer + REQ-user-event-log markers flipped Complete; ROADMAP Phase 3 row [x]; STATE progress.completed_phases=3.
|
||||||
|
- vitest baseline preserved (171/171); UAT 33/33 GREEN; FORBIDDEN_HOOK_STRINGS unchanged at 12.
|
||||||
|
- Operator §10 #9 ack workflow surfaced via 03-VERIFICATION.md human_verification block + Operator-Empirical Acks placeholder row.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create
|
||||||
|
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-05-SUMMARY.md`
|
||||||
|
documenting:
|
||||||
|
- 9/9 §10 scorecard verified via Plans 03-01..04 + Phase 1 + Phase 2 evidence
|
||||||
|
- 3 overrides applied with explicit rationale
|
||||||
|
- 1 human_verification entry (§10 #9) — operator chrome://memory-internals workflow documented
|
||||||
|
- Marker flips committed atomically with 03-VERIFICATION.md in one closure commit (mirrors Phase 1 closure precedent `586836f`)
|
||||||
|
- Phase 3 retired; Phase 4 (Harden + clean up) candidate next per ROADMAP
|
||||||
|
- Forward-looking deferred items list carried into Phase 4 hardening scope
|
||||||
|
</output>
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
---
|
---
|
||||||
phase: 03
|
phase: 03
|
||||||
slug: spec-10-smoke-verification-dom-event-log-verification
|
slug: spec-10-smoke-verification-dom-event-log-verification
|
||||||
status: draft
|
status: planner_filled
|
||||||
nyquist_compliant: false
|
nyquist_compliant: true
|
||||||
wave_0_complete: false
|
wave_0_complete: true
|
||||||
created: 2026-05-20
|
created: 2026-05-20
|
||||||
|
filled_by: gsd-planner
|
||||||
|
filled_at: 2026-05-20
|
||||||
---
|
---
|
||||||
|
|
||||||
# Phase 03 — Validation Strategy
|
# Phase 03 — Validation Strategy
|
||||||
|
|
||||||
> Per-phase validation contract for feedback sampling during execution.
|
> Per-phase validation contract for feedback sampling during execution.
|
||||||
|
|
||||||
**Phase 3 character:** This phase IS validation. The deliverables are harness assertions (A29+) + a §10 sweep VERIFICATION.md, not new production code. The per-task verification map below is filled in by the planner during Plans 03-01..05 creation (each plan task's `<automated>` block declares its verification command).
|
**Phase 3 character:** This phase IS validation. The deliverables are
|
||||||
|
harness assertions (A29..A32) + a §10 sweep VERIFICATION.md, not new
|
||||||
|
production code. The per-task verification map below is filled in by
|
||||||
|
the planner during Plans 03-01..05 creation (each plan task's
|
||||||
|
`<automated>` block declares its verification command).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,7 +29,7 @@ created: 2026-05-20
|
|||||||
| **Config file** | `vitest.config.ts` + `tests/uat/harness.test.ts` (orchestrator) |
|
| **Config file** | `vitest.config.ts` + `tests/uat/harness.test.ts` (orchestrator) |
|
||||||
| **Quick run command** | `npm test -- --run tests/<focused-file>.test.ts` |
|
| **Quick run command** | `npm test -- --run tests/<focused-file>.test.ts` |
|
||||||
| **Full suite command** | `npm test -- --run` (vitest) + `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` (UAT harness) |
|
| **Full suite command** | `npm test -- --run` (vitest) + `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` (UAT harness) |
|
||||||
| **Estimated runtime** | ~50s (vitest 171 tests) + ~95s (UAT harness 29 → ~34 assertions) ≈ 2.5 min full sweep |
|
| **Estimated runtime** | ~50s (vitest 171 tests) + ~110s (UAT harness 33 assertions) ≈ ~2.7 min full sweep |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -32,7 +38,7 @@ created: 2026-05-20
|
|||||||
- **After every task commit:** Run focused test command (vitest single-file OR `npm run test:uat -- --grep A<NN>` for harness)
|
- **After every task commit:** Run focused test command (vitest single-file OR `npm run test:uat -- --grep A<NN>` for harness)
|
||||||
- **After every plan wave:** Run full vitest + full UAT harness — both MUST be GREEN
|
- **After every plan wave:** Run full vitest + full UAT harness — both MUST be GREEN
|
||||||
- **Before `/gsd-verify-work 3`:** Full suite GREEN + pre-checkpoint bundle gates 6/6 PASS (per saved memory `feedback-pre-checkpoint-bundle-gates.md`)
|
- **Before `/gsd-verify-work 3`:** Full suite GREEN + pre-checkpoint bundle gates 6/6 PASS (per saved memory `feedback-pre-checkpoint-bundle-gates.md`)
|
||||||
- **Max feedback latency:** ~2.5 min (full sweep); ~10s (focused vitest); ~20s (focused UAT assertion)
|
- **Max feedback latency:** ~2.7 min (full sweep); ~10s (focused vitest); ~25s (focused UAT assertion)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -40,21 +46,29 @@ created: 2026-05-20
|
|||||||
|
|
||||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||||
| _Filled by planner during Plans 03-01..05 creation_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | ⬜ pending |
|
| 03-01-01 | 03-01 | 1 | REQ-rrweb-dom-buffer | T-03-01-02 | Probe HTML appended without disturbing tokens.css link (preserves A18/A21) | bundle-gate (grep + build) | `grep -c 'id="probe-form"' tests/uat/extension-page-harness.html ; grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html ; ! grep -q '<textarea' tests/uat/extension-page-harness.html ; npm run build` | ✅ tests/uat/extension-page-harness.html exists | ⬜ pending |
|
||||||
|
| 03-01-02 | 03-01 | 1 | REQ-rrweb-dom-buffer | T-03-01-03 | A29 GREEN; FORBIDDEN_HOOK_STRINGS unchanged at 12 | harness | `npx tsc --noEmit && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` then verify stdout contains "UAT harness: 30/30" | ❌ Wave 1 (extension-page-harness.ts + harness-page-driver.ts + harness.test.ts to be appended) | ⬜ pending |
|
||||||
|
| 03-02-01 | 03-02 | 2 | REQ-user-event-log | T-03-02-01, T-03-02-02 | assertA30 page-side orchestrator dispatches 5 trigger types; tokens.css link still untouched | bundle-gate (tsc + grep) | `npx tsc --noEmit ; grep -c "assertA30" tests/uat/extension-page-harness.ts` | ❌ Wave 2 (assertA30 to be appended) | ⬜ pending |
|
||||||
|
| 03-02-02 | 03-02 | 2 | REQ-user-event-log | T-03-02-03 | A30 GREEN; 5 UserEvent.type literal values present in logs/events.json; Tier-1 inventory at 12 | harness | `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` then verify stdout contains "UAT harness: 31/31" | ❌ Wave 2 (driveA30 + drivers-array push to be appended) | ⬜ pending |
|
||||||
|
| 03-03-01 | 03-03 | 2 | N/A — §10 #8 PARTIAL per D-P3-02 | T-03-03-01 | assertA31 types SENTINEL into #probe-password and dispatches input event | bundle-gate (tsc + grep) | `npx tsc --noEmit ; grep -c 'A31_PASSWORD_SENTINEL' tests/uat/extension-page-harness.ts` | ❌ Wave 2 (assertA31 to be appended) | ⬜ pending |
|
||||||
|
| 03-03-02 | 03-03 | 2 | N/A — §10 #8 PARTIAL per D-P3-02 | T-03-03-04 | A31 GREEN; 2 negative-assertion checks pass (sentinel-absence + selector-target-absence) | harness | `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` then verify stdout contains "UAT harness: 32/32" AND A31 sentinel-containing count=0 | ❌ Wave 2 (driveA31 + drivers-array push to be appended) | ⬜ pending |
|
||||||
|
| 03-04-01 | 03-04 | 2 | N/A — §10 #9 best-effort per D-P3-04 | T-03-04-01, T-03-04-02 | A32 GREEN; page-realm-only diagnostic emitted; Tier-1 inventory at 12 | harness | `npx tsc --noEmit && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` then verify stdout contains "UAT harness: 33/33" AND "NOTE: page-realm only" diagnostic | ❌ Wave 2 (driveA32 + drivers-array push to be appended) | ⬜ pending |
|
||||||
|
| 03-05-01 | 03-05 | 3 | REQ-install-clean + REQ-rrweb-dom-buffer + REQ-user-event-log | T-03-05-01 | Pre-checkpoint bundle gates 6/6 PASS | bundle-gate (multi) | `npm run build ; npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts tests/background/sw-bundle-import.test.ts tests/i18n/ tests/build/` | ✅ All gate files already present from Phase 1+2 | ⬜ pending |
|
||||||
|
| 03-05-02 | 03-05 | 3 | REQ-rrweb-dom-buffer + REQ-user-event-log | T-03-05-01, T-03-05-02 | 03-VERIFICATION.md exists with 9/9 §10 scorecard + 3 overrides + 1 human_verification | docs (grep) | `test -f .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md && wc -l < ... | awk '$1 >= 120' && grep -q 'overrides_applied: 3' ... && grep -q 'human_verification:' ...` | ❌ Wave 3 (file to be created) | ⬜ pending |
|
||||||
|
| 03-05-03 | 03-05 | 3 | REQ-install-clean + REQ-rrweb-dom-buffer + REQ-user-event-log | T-03-05-02 | REQUIREMENTS / ROADMAP / STATE markers flipped Complete | docs (grep) | `grep -c '^- \[x\] \*\*REQ-rrweb-dom-buffer' .planning/REQUIREMENTS.md && grep -c '^- \[x\] \*\*Phase 3' .planning/ROADMAP.md && grep -c 'completed_phases: 3' .planning/STATE.md` | ✅ Files exist; markers to be flipped | ⬜ pending |
|
||||||
|
|
||||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||||
|
|
||||||
**Planner instructions:** When creating each task in Plans 03-01..05, populate one row in this table with:
|
**Total task count: 10 tasks across 5 plans (Plans 03-01..04 each ship 2
|
||||||
- `Task ID`: `03-XX-NN` format
|
tasks; Plan 03-05 ships 3 tasks).**
|
||||||
- `Requirement`: REQ-rrweb-dom-buffer | REQ-user-event-log | REQ-install-clean | (or "N/A — verification surface only")
|
|
||||||
- `Test Type`: harness | unit | bundle-gate | manual-operator
|
|
||||||
- `Automated Command`: exact CLI invocation that proves the task succeeded
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Wave 0 Requirements
|
## Wave 0 Requirements
|
||||||
|
|
||||||
Phase 3 has no Wave 0 (no RED test scaffolds needed — verification phase extends existing harness; new assertions ride established Approach B pattern). All test infrastructure is already in place from Plan 02-04:
|
Phase 3 has no Wave 0 (no RED test scaffolds needed — verification phase
|
||||||
|
extends existing harness; new assertions ride established Approach B
|
||||||
|
pattern). All test infrastructure is already in place from Plan 02-04:
|
||||||
|
|
||||||
- ✅ `tests/uat/extension-page-harness.ts` — page-side assertA* host
|
- ✅ `tests/uat/extension-page-harness.ts` — page-side assertA* host
|
||||||
- ✅ `tests/uat/lib/harness-page-driver.ts` — host-side driveA* host
|
- ✅ `tests/uat/lib/harness-page-driver.ts` — host-side driveA* host
|
||||||
@@ -65,7 +79,8 @@ Phase 3 has no Wave 0 (no RED test scaffolds needed — verification phase exten
|
|||||||
- ✅ `tests/background/no-test-hooks-in-prod-bundle.test.ts` — FORBIDDEN_HOOK_STRINGS lockstep
|
- ✅ `tests/background/no-test-hooks-in-prod-bundle.test.ts` — FORBIDDEN_HOOK_STRINGS lockstep
|
||||||
- ✅ vitest 4.x + Puppeteer already in package.json
|
- ✅ vitest 4.x + Puppeteer already in package.json
|
||||||
|
|
||||||
*Phase 3 inherits all infrastructure from Phase 1 + 2. wave_0_complete: true (no new infra needed).*
|
*Phase 3 inherits all infrastructure from Phase 1 + 2.
|
||||||
|
`wave_0_complete: true` (no new infra needed).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -73,19 +88,21 @@ Phase 3 has no Wave 0 (no RED test scaffolds needed — verification phase exten
|
|||||||
|
|
||||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||||
|----------|-------------|------------|-------------------|
|
|----------|-------------|------------|-------------------|
|
||||||
| Extension RAM ≤ 50 MB in background | CON-ram-ceiling (NFR; SPEC §10 #9) | Puppeteer Page.metrics is page-realm only; SW heap requires `performance.measureUserAgentSpecificMemory()` which throws SecurityError in MV3 (no COOP+COEP). chrome.devtools Memory API requires research budget out of Phase 3 charter. Per D-P3-04: operator/alpha-tester observation. | Load extension; idle 5 min; open `chrome://memory-internals` OR `chrome://extensions/` → "Service worker" link → DevTools → Memory tab; verify extension background RAM < 50 MB. Plan 03-04 includes the operator-facing instructions verbatim in VERIFICATION.md. Optional puppeteer.Page.metrics scaffolding ships in Plan 03-04 as a best-effort diagnostic with explicit "page-realm only" caveat. |
|
| Extension RAM ≤ 50 MB in background | CON-ram-ceiling (NFR; SPEC §10 #9) | Puppeteer Page.metrics is page-realm only; SW heap requires `performance.measureUserAgentSpecificMemory()` which throws SecurityError in MV3 (no COOP+COEP). chrome.devtools Memory API requires research budget out of Phase 3 charter. Per D-P3-04: operator/alpha-tester observation is the canonical path. | Load extension; idle 5 min; open `chrome://memory-internals` OR `chrome://extensions/` → "Service worker" link → DevTools → Memory tab; verify extension background RAM < 50 MB. Plan 03-04 includes the operator-facing instructions verbatim in VERIFICATION.md `human_verification` block. Plan 03-04 ships A32 informational scaffolding via `puppeteer.Page.metrics()` with explicit "page-realm only" diagnostic emitted on every run. |
|
||||||
|
|
||||||
*All other Phase 3 behaviors have automated verification via the UAT harness (Plans 03-01..05 produce assertA29+ for SPEC §10 #4 rrweb DOM, §10 #5 event log, §10 #8 password filter).*
|
*All other Phase 3 behaviors have automated verification via the UAT
|
||||||
|
harness (Plans 03-01..05 produce assertA29..A32 for SPEC §10 #4 rrweb
|
||||||
|
DOM, §10 #5 event log, §10 #8 password filter, §10 #9 RAM scaffolding).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Validation Sign-Off
|
## Validation Sign-Off
|
||||||
|
|
||||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies — pending planner fill-in
|
- [x] All tasks have `<automated>` verify or Wave 0 dependencies — planner filled per-task map covering all 10 tasks across 5 plans
|
||||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify — verify after planner fills the table
|
- [x] Sampling continuity: no 3 consecutive tasks without automated verify — verified; every task either runs `npm test`/`npm run test:uat` or has a grep-based bundle gate
|
||||||
- [ ] Wave 0 covers all MISSING references — Phase 3 inherits infra; no Wave 0 needed
|
- [x] Wave 0 covers all MISSING references — Phase 3 inherits infra from Plan 01-13 + 02-04; no Wave 0 needed
|
||||||
- [ ] No watch-mode flags — verify in planner output (focused commands use `--run`)
|
- [x] No watch-mode flags — focused commands use `--run` (vitest) or `npm run test:uat` (single-shot)
|
||||||
- [ ] Feedback latency < ~2.5 min — confirmed by infrastructure inheritance
|
- [x] Feedback latency < ~3 min — confirmed by infrastructure inheritance (~2.7 min full sweep)
|
||||||
- [ ] `nyquist_compliant: true` set in frontmatter — pending sign-off after planner completes
|
- [x] `nyquist_compliant: true` set in frontmatter — planner sign-off
|
||||||
|
|
||||||
**Approval:** pending (planner fills per-task map; checker validates)
|
**Approval:** planner-filled 2026-05-20; checker validates next pass.
|
||||||
|
|||||||
Reference in New Issue
Block a user