feat(03-02): Task 1 — assertA30 page-side orchestrator (5 event triggers + SAVE)

- Add assertA30 dispatching 5 synthetic browser events on the harness page:
  click (#probe-submit), input (#probe-email),
  navigation (history.pushState #a30-probe), js_error (window.dispatchEvent
  ErrorEvent), network_error (fetch https://example.com/<404-path>).
- Module-local timing/url constants: A30_SAVE_ARCHIVE_TIMEOUT_MS=15s,
  A30_SEGMENT_SETTLE_MS=11s, A30_TRIGGER_SETTLE_MS=500ms,
  A30_404_PROBE_URL (RFC 2606 reserved example.com).
- Wire assertA30 into declare global Window.__mokoshHarness interface +
  window.__mokoshHarness object literal (preserves assertA29 from Plan 03-01).
- Update statusEl banner A29 -> A30 and closing console.log to append
  "Plan 03-02: A30".
- A30 rides production listeners at src/content/index.ts:60-237 + existing
  setupFreshRecording / sendMessageWithTimeout helpers — Tier-1
  FORBIDDEN_HOOK_STRINGS inventory unchanged at 12.
This commit is contained in:
2026-05-20 19:25:03 +02:00
parent 72bbb8044b
commit b5181012a8

View File

@@ -3418,6 +3418,137 @@ async function assertA29(): Promise<AssertionResult> {
return result; return result;
} }
/* ─── 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;
}
/** /**
* Read `chrome.runtime.getManifest().version`. Used by the host-side * Read `chrome.runtime.getManifest().version`. Used by the host-side
* orchestrator at startup to capture the expected version for A13's * orchestrator at startup to capture the expected version for A13's
@@ -3471,6 +3602,8 @@ declare global {
assertA28: () => Promise<AssertionResult>; assertA28: () => Promise<AssertionResult>;
// Plan 03-01 — rrweb DOM verification (SPEC §10 #4) // Plan 03-01 — rrweb DOM verification (SPEC §10 #4)
assertA29: () => Promise<AssertionResult>; assertA29: () => Promise<AssertionResult>;
// Plan 03-02 — event-log verification (SPEC §10 #5)
assertA30: () => Promise<AssertionResult>;
getManifestVersion: () => Promise<string>; getManifestVersion: () => Promise<string>;
}; };
} }
@@ -3506,14 +3639,15 @@ window.__mokoshHarness = {
assertA27, assertA27,
assertA28, assertA28,
assertA29, assertA29,
assertA30,
getManifestVersion, getManifestVersion,
}; };
const statusEl = document.getElementById('status'); const statusEl = document.getElementById('status');
if (statusEl !== null) { if (statusEl !== null) {
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A29, getManifestVersion} available.'; statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A30, 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 + Plan 03-01: A29 + 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 + Plan 03-02: A30 + getManifestVersion)');
export {}; export {};