feat(03-01): Task 2 — assertA29 + driveA29 + orchestrator wiring (A29 30/30 GREEN)

Page-side (tests/uat/extension-page-harness.ts):
- assertA29 dispatches probe-page DOM mutation (input value + modal
  toggle), settles 500ms for rrweb IncrementalSnapshot to enqueue,
  setupFreshRecording, 11s segment-settle, SAVE_ARCHIVE; pushes
  A29.1 SAVE ack check. Module-local constants:
  A29_SAVE_ARCHIVE_TIMEOUT_MS=15s, A29_SEGMENT_SETTLE_MS=11s,
  A29_MUTATION_SETTLE_MS=500ms.
- declare global interface + window.__mokoshHarness object literal
  extended with assertA29 (single-method-per-assertion contract).
- statusEl + console banner updated A28 → A29 + cite Plan 03-01.

Host-side (tests/uat/lib/harness-page-driver.ts):
- Add `import { EventType } from '@rrweb/types';`.
- driveA29 — 3-phase orchestration mirroring driveA26:
  Phase 1 page.evaluate harness.assertA29(); Phase 2 findLatestZip;
  Phase 3 JSZip.loadAsync rrweb/session.json + EventType grep.
  Appends A29.0a (rrweb/session.json present) + A29.2..A29.5
  (events.length>0 + Meta + FullSnapshot + IncrementalSnapshot).

Orchestrator (tests/uat/harness.test.ts):
- driveA29 imported after driveA28.
- driveA29Wrapped const captures handles.downloadsDir.
- drivers array push A29 entry with banner citing Plan 03-01 + Pitfall 1.
- Architecture banner string updated A28 → A29.

Empirical verification (HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat):
- UAT harness: 30/30 GREEN (29 prior + A29 NEW).
- A29 events.length=4; event types observed: 2, 3, 4 (FullSnapshot,
  IncrementalSnapshot, Meta — all three required types present).
- Pitfall 1 mitigation empirically verified — the pre-SAVE DOM
  mutation produced the IncrementalSnapshot.
- vitest 171/171 GREEN preserved (full suite).
- Tier-1 FORBIDDEN_HOOK_STRINGS unit gate 13/13 GREEN (12 strings × 0
  hits each) — A29 rides production rrweb wiring + GET_RRWEB_EVENTS
  bridge + sendMessageWithTimeout helper; NO new __MOKOSH_UAT__
  symbols.
- npx tsc --noEmit exit 0.
This commit is contained in:
2026-05-20 19:17:47 +02:00
parent c02914df86
commit cc13f319a1
3 changed files with 273 additions and 3 deletions

View File

@@ -3315,6 +3315,109 @@ async function assertA28(): Promise<AssertionResult> {
};
}
/* ─── Plan 03-01 Task 2 — A29 (rrweb DOM verification; SPEC §10 #4) ─
*
* A29 — REQ-rrweb-dom-buffer empirical: rrweb's record() (already
* wired at src/content/index.ts:285) emits Meta + FullSnapshot
* + at least one IncrementalSnapshot when the harness page
* contains the probe HTML (form + table + modal) AND the driver
* injects a DOM mutation before SAVE (RESEARCH Pitfall 1:
* static probe HTML emits Meta + FullSnapshot but not
* IncrementalSnapshot without mutation).
*
* Page side: dispatch the probe-page DOM mutations (input value +
* modal toggle), settle, setupFreshRecording, settle one segment,
* dispatch SAVE_ARCHIVE, push A29.1 ack check. The host-side driveA29
* does the EventType-enum-shape grep against rrweb/session.json from
* the assembled zip (matches A26's chained-assertion pattern; JSZip
* + @rrweb/types are host-only deps).
*
* FORBIDDEN_HOOK_STRINGS impact: NONE. A29 rides production rrweb
* wiring (record() at src/content/index.ts:285 + GET_RRWEB_EVENTS
* round-trip at src/background/index.ts → src/content/index.ts:318)
* + the existing setupFreshRecording / sendMessageWithTimeout
* helpers. Tier-1 inventory stays at 12 entries.
*/
/** SAVE_ARCHIVE dispatch timeout for A29 — matches A24/A25/A27. */
const A29_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
const A29_SEGMENT_SETTLE_MS = 11_000;
/** Settle window between DOM mutation and SAVE so rrweb's
* IncrementalSnapshot lands in the in-memory buffer before
* GET_RRWEB_EVENTS fires. */
const A29_MUTATION_SETTLE_MS = 500;
/**
* A29 — rrweb DOM event recording empirical (SPEC §10 #4).
*
* Page-side dispatches the probe-page mutation (input.value + modal
* toggle), settles, runs setupFreshRecording + segment-settle, then
* dispatches SAVE_ARCHIVE. Only the SAVE ack lives in the
* page-side checks; the EventType-enum-shape grep is host-side
* because @rrweb/types is host-only.
*
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
* driveA29 appends A29.0a/A29.2..A29.5 EventType checks.
*/
async function assertA29(): Promise<AssertionResult> {
const result: AssertionResult = {
passed: false,
name: 'A29 — rrweb DOM events recorded without errors (SPEC §10 #4 / REQ-rrweb-dom-buffer)',
checks: [],
diagnostics: [],
};
try {
diag(result, 'Step 1: dispatch probe-page DOM mutation (input value + modal toggle)');
const textInput = document.querySelector<HTMLInputElement>('#probe-text');
if (textInput !== null) {
textInput.value = 'probe';
textInput.dispatchEvent(new Event('input', { bubbles: true }));
}
const modalTrigger = document.querySelector<HTMLButtonElement>('#probe-modal-trigger');
if (modalTrigger !== null) {
modalTrigger.click();
}
diag(result, `Step 1 OK — mutations dispatched (#probe-text=${textInput !== null}, #probe-modal-trigger=${modalTrigger !== null})`);
diag(result, `Step 2: settle ${A29_MUTATION_SETTLE_MS}ms for rrweb IncrementalSnapshot to enqueue`);
await new Promise((r) => setTimeout(r, A29_MUTATION_SETTLE_MS));
diag(result, 'Step 3: setupFreshRecording');
const setupResp = await setupFreshRecording();
if (!setupResp.ok) {
throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
}
diag(result, 'Step 3 OK — REC state established');
diag(result, `Step 4: settle ${A29_SEGMENT_SETTLE_MS}ms for first segment rotation`);
await new Promise((r) => setTimeout(r, A29_SEGMENT_SETTLE_MS));
diag(result, 'Step 5: dispatch SAVE_ARCHIVE');
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
{ type: 'SAVE_ARCHIVE' },
A29_SAVE_ARCHIVE_TIMEOUT_MS,
'SAVE_ARCHIVE (A29)',
);
diag(result, `Step 5 result: ${JSON.stringify(ack)}`);
result.checks.push({
name: 'A29.1: SAVE_ARCHIVE ack received with success=true',
expected: true,
actual: ack.success,
passed: ack.success === true,
});
result.passed = result.checks.every((c) => c.passed);
} catch (err) {
result.error = err instanceof Error ? err.message : String(err);
diag(result, `THREW: ${result.error}`);
}
return result;
}
/**
* Read `chrome.runtime.getManifest().version`. Used by the host-side
* orchestrator at startup to capture the expected version for A13's
@@ -3366,6 +3469,8 @@ declare global {
assertA26: () => Promise<AssertionResult>;
assertA27: () => Promise<A27Result>;
assertA28: () => Promise<AssertionResult>;
// Plan 03-01 — rrweb DOM verification (SPEC §10 #4)
assertA29: () => Promise<AssertionResult>;
getManifestVersion: () => Promise<string>;
};
}
@@ -3400,14 +3505,15 @@ window.__mokoshHarness = {
assertA26,
assertA27,
assertA28,
assertA29,
getManifestVersion,
};
const statusEl = document.getElementById('status');
if (statusEl !== null) {
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A28, getManifestVersion} available.';
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A29, getManifestVersion} available.';
}
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + getManifestVersion)');
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + getManifestVersion)');
export {};