Wave structure: - W1 (parallel): 04-01 (Audit P1 polish #11/#14/#15 TDD) + 04-02 (build/CSP hygiene: setimmediate polyfill + dead-code + generate-icons.cjs) - W2: 04-03 (A29 cs-injection-world rewrite; closes flake) - W3: 04-04 (A33 SW state persistence; spike-first + CDP worker.close()) - W4: 04-05 (A34 fetch+XHR network_error; ROADMAP SC #2 + validates Plan 04-01 P1 #11 end-to-end) - W5: 04-06 (dark-logo currentColor + cursor verification + 01-07-SUMMARY back-patch; operator empirical) - W6: 04-07 (04-VERIFICATION.md aggregator + ROADMAP backfill + v1 close prep) Honors locked decisions D-P4-01..05 (full Phase 4 + all 3 P1 polish + both visual items + alpha-independent + ROADMAP backfill). Implements RESEARCH Q1 (setimmediate option a), Q2 (spike-first SW persistence), Q3 (A29 cs-injection-world), Finding 4 (cursor already shipped — verification only). UI-SPEC dark-logo currentColor strategy with inline-SVG injection landed per UI-SPEC §"Implementation amendment". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21 KiB
phase, slug, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, user_setup, must_haves
| phase | slug | plan | type | wave | depends_on | files_modified | autonomous | requirements | tags | user_setup | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04 | harden-clean-up-optional | 03 | execute | 2 |
|
|
true |
|
|
The fix per RESEARCH Q3:
- Open a fresh
https://example.com/probe tab viachrome.tabs.create(canonical RFC 2606 reserved domain; matches A30/A31). - Wait the canonical 1.5s for content-script attach (mirrors A27/A30/A31's
A2X_TAB_NAVIGATION_WAIT_MS). - Wait the canonical 11s for the first MediaRecorder segment to rotate (mirrors A30's
SEGMENT_SETTLE_MS). - Use
chrome.scripting.executeScript({world: 'ISOLATED', func: ...})to inject a synthetic DOM mutation that carries a UNIQUE SENTINEL STRING ('a29-mutation-sentinel') into a fresh<div>appendChild'd todocument.body. - Wait 500ms for rrweb's MutationObserver to enqueue the IncrementalSnapshot.
- Dispatch SAVE_ARCHIVE while the probe tab is active (its content script is the source of
rrweb/session.json). - Host-side driveA29 JSZip-parses the resulting archive, filters rrweb events for
EventType.IncrementalSnapshot+IncrementalSource.Mutation, descends into the mutation payload'sadds[*].node.textContentfield, and asserts ≥ 1 event contains the sentinel — PROVING the captured mutation came from OUR injection, not from leftover iana.org tabs.
Purpose: Flake stabilization. Plan 03-02 + 03-03 SUMMARYs both document A29 as the canonical "tests-pass-for-wrong-reason" smell — the EventType-loose-grep currently in place trivially passes because iana.org's home page has plenty of mutations in its own normal rendering, and the previous A27/A28 probe tab leaks rrweb events into the buffer that A29's findLatestZip picks up. The fix is mechanical (verbatim port of an established pattern) but the diagnostic value is large (the test now genuinely verifies SPEC §10 #4 / REQ-rrweb-dom-buffer at the rrweb wiring at src/content/index.ts:284-309).
Output: 1 file rewrite in tests/uat/extension-page-harness.ts (the existing assertA29 body, lines 3363-3419) + 1 file rewrite in tests/uat/lib/harness-page-driver.ts (the existing driveA29 body, lines 1884-2001). No new harness assertion number is added; A29 count stays 33; the rewrite is in-place.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md @.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md @.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md @.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-SUMMARY.md @.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-SUMMARY.mdSource files — the canonical assertA30 + driveA30 to copy from + the current A29 to rewrite
@tests/uat/extension-page-harness.ts @tests/uat/lib/harness-page-driver.ts
Prior plan SUMMARYs documenting the A29 flake
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md
From tests/uat/extension-page-harness.ts:3363-3419 (current assertA29 — TO BE REWRITTEN):
- Function body uses a harness-page approach (no chrome.tabs.create; relies on the existing harness page's probe HTML structure).
- The harness-page approach is rejected per Plan 03-02 SUMMARY's "Pitfall 2" —
<all_urls>content-script match-pattern EXCLUDES chrome-extension:// scheme; the harness page never gets a content script attached, so rrweb never wires up, so any mutations captured come from elsewhere.
From tests/uat/extension-page-harness.ts:3517-3636 (assertA30 — the CANONICAL cs-injection-world skeleton to copy):
- 7-step pattern: setupFreshRecording → chrome.tabs.create probe → wait nav → wait segment → chrome.scripting.executeScript ISOLATED → wait settle → SAVE_ARCHIVE
- Uses constants block:
const A30_PROBE_TAB_URL = 'https://example.com/';+ similar. - Uses
try/finallyto ensure probe-tab cleanup viachrome.tabs.remove(silent-ignore via T-02-04-04).
From tests/uat/lib/harness-page-driver.ts:1884-2001 (current driveA29 — TO BE REWRITTEN):
- Currently performs a JSZip parse + EventType-loose-grep that counts
e.type === EventType.Meta+EventType.FullSnapshot+EventType.IncrementalSnapshotoccurrences ≥ 1. - The loose-grep is RESEARCH Pitfall 1 ("Trusting Plan 03-02's SUMMARY 'A29 events.length=4' diagnostic as proof of correctness").
- After Plan 04-03 rewrite: still does JSZip parse but adds a strict-sentinel filter step PER RESEARCH Code Example Pattern 3.
From tests/uat/lib/harness-page-driver.ts:2039-2148 (driveA30 — the CANONICAL host-side filter pattern):
- Imports
EventType(and nowIncrementalSource) from@rrweb/types. - Calls
JSZip.loadAsync(readFileSync(zipPath))+zip.file('rrweb/session.json')?.async('text'). - Filter-pipeline form for events extraction (no for/continue per CLAUDE.md).
From RESEARCH Q3 Code Example Pattern 3 (host-side strict-sentinel filter — the NEW code path for driveA29):
import { EventType, IncrementalSource } from '@rrweb/types';
const A29_MUTATION_SENTINEL = 'a29-mutation-sentinel';
const mutationEvents = events.filter((e) =>
e.type === EventType.IncrementalSnapshot &&
e.data?.source === IncrementalSource.Mutation,
);
const sentinelEvents = mutationEvents.filter((e) => {
const adds = e.data?.adds ?? [];
return adds.some((a: any) =>
typeof a?.node?.textContent === 'string' &&
a.node.textContent.includes(A29_MUTATION_SENTINEL),
);
});
Constants to define at the top of assertA29 (RESEARCH Q3 Code Example Pattern 3):
const A29_PROBE_TAB_URL = 'https://example.com/'; // RFC 2606 reserved; matches A30/A31
const A29_TAB_NAVIGATION_WAIT_MS = 1_500; // mirrors A27/A30/A31 canonical
const A29_SEGMENT_SETTLE_MS = 11_000; // first segment rotation (matches A30)
const A29_MUTATION_SETTLE_MS = 500; // rrweb MutationObserver enqueue
const A29_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
const A29_MUTATION_SENTINEL = 'a29-mutation-sentinel';
const A29_PROBE_DIV_ID = 'a29-probe-mutation';
Injection function body (inside chrome.scripting.executeScript func: arg):
func: (sentinel: string, divId: string) => {
const div = document.createElement('div');
div.id = divId;
div.textContent = sentinel;
document.body.appendChild(div);
},
args: [A29_MUTATION_SENTINEL, A29_PROBE_DIV_ID],
Note on @rrweb/types IncrementalSource enum (verified for rrweb 2.0.0-alpha.4 per RESEARCH Assumption A5):
IncrementalSource.Mutation = 0— the mutation-source enum value- Already imported at tests/uat/lib/harness-page-driver.ts:2039+ for driveA30 (or grep to confirm; if not imported, ADD the import alongside the existing
EventTypeimport).
Filter-pipeline form: no `continue`. TypeScript-strict. Inline cleanup comments cite RESEARCH Q3 + Plan 03-02 SUMMARY at top of function.
Verify: `npx tsc --noEmit` exits 0. Build the test bundle: `npm run build:test` exits 0 (the bundled `dist-test/` is what the harness loads; assertA29 lives in extension-page-harness.ts which is bundled by the test config).
npx tsc --noEmit 2>&1 | grep -c 'error TS'; npm run build:test 2>&1 | tail -5
- `npx tsc --noEmit` exits 0.
- `npm run build:test` exits 0.
- `grep -c 'A29_MUTATION_SENTINEL' tests/uat/extension-page-harness.ts` returns ≥ 2 (constant definition + injection args).
- `grep -c 'a29-mutation-sentinel' tests/uat/extension-page-harness.ts` returns ≥ 1 (constant value literal).
- `grep -nE "world: 'ISOLATED'" tests/uat/extension-page-harness.ts` returns ≥ 3 lines (assertA29 new entry + existing assertA30 + assertA31 — all 3 must continue to use ISOLATED world; ANY MAIN-world drift is a regression per RESEARCH Pitfall 5).
- `grep -nE 'chrome\\.tabs\\.create.*A29_PROBE_TAB_URL' tests/uat/extension-page-harness.ts` returns ≥ 1.
- Existing assertA30 + assertA31 untouched: `git diff tests/uat/extension-page-harness.ts | grep -c '^+.*assertA30\\|^+.*assertA31'` returns 0 (only assertA29 changed).
assertA29 rewritten in-place using cs-injection-world pattern + sentinel-bearing DOM mutation injector. Commit: `feat(04-03): A29 page-side rewrite — cs-injection-world + sentinel`.
Task 2: Rewrite driveA29 with strict-sentinel rrweb event filter (host-side)
tests/uat/lib/harness-page-driver.ts
tests/uat/lib/harness-page-driver.ts:1884-2001 (current driveA29), tests/uat/lib/harness-page-driver.ts:2039-2148 (driveA30 — canonical host-side filter pattern), .planning/phases/04-harden-clean-up-optional/04-RESEARCH.md Q3 Code Example Pattern 3
1. Read tests/uat/lib/harness-page-driver.ts in the line range 1850-2200 once (covers existing driveA29 + driveA30 import block + driveA31). DO NOT re-read.
2. Verify the imports at the top of the file include both `EventType` and `IncrementalSource` from `@rrweb/types`:
- If only `EventType` is imported, ADD `IncrementalSource` to the import binding list.
- Mirror the existing `import { EventType } from '@rrweb/types';` shape.
3. Locate the existing `export async function driveA29(...)` at line ~1884. Replace its body to:
- Preserve the existing Phase 1 page-side stub call: `const pageResult = await page.evaluate(() => harness.assertA29());`
- Preserve the existing zipPath = findLatestZip(downloadsDir); + null-check fallback.
- Replace the existing loose-EventType-grep block with the strict-sentinel filter per RESEARCH Code Example Pattern 3 (use the exact code snippet from `` above).
- Push a single check `A29.2: rrweb captured the injected mutation containing 'a29-mutation-sentinel' (closes iana.org-leftover-flake)` with `passed: sentinelEvents.length >= 1`.
- PRESERVE any existing A29.0a or A29.3+ checks (Meta/FullSnapshot presence) if they're valuable as defense-in-depth; the strict-sentinel A29.2 is THE primary check that closes the flake.
- Filter-pipeline form (no for/continue).
- Aggregate: `mergedPassed = mergedChecks.every(c => c.passed);` and return.
4. Verify: `npx tsc --noEmit` exits 0.
Run the full UAT harness as the empirical gate: `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` (uses the bundle from Plan 04-02 Task 2 dist build; harness has its own build pipeline via build:test). Expect:
- A29 GREEN (specifically: A29.1 SAVE ack + A29.2 sentinel found).
- All other assertions A0-A28, A30-A32 GREEN (no regression to A30's sentinel-based check; no regression to A31's password-filter check).
- Total 33/33 GREEN.
Run the 5-consecutive-runs stress test to confirm flake fixed:
```bash
for i in {1..5}; do
HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat 2>&1 | grep -E '^(A29|TOTAL)' | tail -2;
done
```
Expect: A29 PASS in all 5 runs (was ~3/5 historical baseline per Plan 03-03 SUMMARY).
npx tsc --noEmit && HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat 2>&1 | tail -10 | tee /tmp/04-03-uat-1.log; grep -c '33/33' /tmp/04-03-uat-1.log
- `npx tsc --noEmit` exits 0.
- UAT harness 33/33 GREEN in one run; A29 specifically PASS.
- `grep -c 'IncrementalSource' tests/uat/lib/harness-page-driver.ts` returns ≥ 2 (import + filter usage).
- `grep -c 'a29-mutation-sentinel' tests/uat/lib/harness-page-driver.ts` returns ≥ 1 (constant value or import from a shared symbol; verifiable post-edit).
- `grep -cE "A29\\.2:.*sentinel" tests/uat/lib/harness-page-driver.ts` returns ≥ 1 (the new strict-sentinel check entry).
- 5 consecutive UAT runs all GREEN for A29 (manual stress; record outcomes in SUMMARY).
- No regression to A30 + A31 (they have their own sentinel-based checks; verify their PASS state unchanged).
driveA29 strict-sentinel filter landed; UAT harness 33/33 GREEN; flake stress 5/5 PASS. Commit: `feat(04-03): A29 host-side strict-sentinel filter + 5/5 PASS stress test`.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| harness host (Node tsx) → Puppeteer page (extension realm) | host-side driveA29 only reads the saved zip; the actual mutation injection happens in the page realm via chrome.scripting.executeScript (sender.id matches chrome.runtime.id automatically — ISOLATED world; no untrusted input crosses boundary) |
| chrome.scripting.executeScript ISOLATED world → page DOM | injected function runs in the content-script ISOLATED world (per Chrome match-pattern docs); rrweb's MutationObserver lives in the same ISOLATED world, so the injected mutation IS captured |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-04-03-01 | Repudiation | A29 currently passes for the wrong reason (iana.org-leftover events from A27/A28 probe tabs); a real rrweb regression at src/content/index.ts:284 would be masked by the loose-EventType-grep | mitigate | Strict-sentinel filter (IncrementalSource.Mutation + adds[*].node.textContent includes injected sentinel string); test fails fast if the captured mutation doesn't carry OUR sentinel |
| T-04-03-02 | Spoofing | a future plan could accidentally inject a mutation via MAIN world instead of ISOLATED world, silently masking the test (mutation visible in DOM but rrweb's observer in ISOLATED doesn't see it) | mitigate | Explicit world: 'ISOLATED' written in code + acceptance criterion grep gate requires ≥ 3 occurrences across the 3 cs-injection-world callers (A29 + A30 + A31) |
| T-04-03-03 | DoS (test) | the 5-min idle path (Plan 04-04 A33) could leave a leftover probe tab from A29 if cleanup misfires, polluting the next run's iana.org-leftover baseline | accept | The harness uses mkdtemp downloadsDir per run + each assertA* uses try/finally chrome.tabs.remove; cross-assertion leftover risk is bounded by Puppeteer browser.close() between full runs |
| </threat_model> |
<success_criteria>
- assertA29 rewritten in-place using cs-injection-world skeleton + sentinel-bearing mutation injection (Task 1).
- driveA29 rewritten with strict-sentinel rrweb event filter (Task 2).
- UAT harness 33/33 GREEN preserved.
- A29 flake stabilized — 5/5 PASS across consecutive runs (was ~2/3 historical).
- Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12 (production chrome.tabs.create + chrome.scripting.executeScript exclusively — no new
__MOKOSH_UAT__-gated symbols). - vitest baseline preserved (Plan 04-02 baseline; ≥ 181 GREEN). </success_criteria>