feat(04-05): A34 page-side — cs-injection-world fetch + XHR 404 injection
- Append assertA34 after assertA31 — cs-injection-world skeleton (verbatim from assertA30/A31; ROADMAP SC #2 empirical) - chrome.scripting.executeScript ISOLATED injects TWO 404 triggers into the content-script realm: fetch(404) + XMLHttpRequest(404) - fetch trigger validates Plan 04-01 P1 #11 (Request-narrow URL extraction) end-to-end in a real Chrome page context - XHR trigger covers the distinct XMLHttpRequest.prototype wrapper path that A30 did not exercise - Date.now() uniqueness stamp on both probe URLs (T-04-05-02) - assertA34 registered in Window interface + __mokoshHarness literal - Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12 (rides production window.fetch + XMLHttpRequest.prototype + chrome.scripting/tabs) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4020,6 +4020,239 @@ async function assertA31(): Promise<AssertionResult> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ─── Plan 04-05 — A34 (fetch + XHR network_error empirical; ROADMAP SC #2) ─
|
||||
*
|
||||
* A34 — ROADMAP SC #2 empirical: "A page that issues a failing `fetch`
|
||||
* (response code >= 400) produces a `network_error` entry in
|
||||
* `events.json`; a failing `XMLHttpRequest` does too."
|
||||
*
|
||||
* A34 EXTENDS Plan 03-02's A30 (which exercised the fetch path
|
||||
* once via a 404-fetch from a probe tab) with:
|
||||
*
|
||||
* 1. An empirical end-to-end test that the Plan 04-01 P1 #11
|
||||
* fetch URL extraction fix (`args[0] instanceof Request ?
|
||||
* args[0].url : String(args[0])` at src/content/index.ts:194
|
||||
* + :214) works in a REAL Chrome page context — the fetch
|
||||
* network_error entry's `target` field must carry the
|
||||
* actual URL, NOT the literal '[object Request]' that the
|
||||
* pre-fix implicit coercion produced. A30 only proved a
|
||||
* network_error entry EXISTS; A34.4 pins the URL value.
|
||||
*
|
||||
* 2. A complementary XMLHttpRequest 404 path that A30 does NOT
|
||||
* cover. XHR uses a distinct code path in
|
||||
* src/content/index.ts (the XMLHttpRequest.prototype.open
|
||||
* + .send wrappers at lines ~225-258, with the loadend
|
||||
* listener emitting network_error when xhr.status >= 400)
|
||||
* and merits its own empirical gate.
|
||||
*
|
||||
* cs-injection-world pattern (Plan 03-02 / Plan 04-03 A29 precedent —
|
||||
* verbatim skeleton from assertA30 / assertA31 with A34 substitutions):
|
||||
* - chrome.tabs.create({url: 'https://example.com/'}) probe tab.
|
||||
* - Wait 1.5s for content-script attach.
|
||||
* - Wait 11s for first segment rotation.
|
||||
* - chrome.scripting.executeScript world:'ISOLATED' injects TWO
|
||||
* triggers into the content-script realm so BOTH production
|
||||
* wrappers (window.fetch + XMLHttpRequest.prototype) intercept the
|
||||
* failing requests:
|
||||
* - fetch('https://example.com/404-fetch-a34-<stamp>').catch(noop)
|
||||
* - new XMLHttpRequest(); open('GET', '/404-xhr-a34-<stamp>'); send()
|
||||
* The `-<stamp>` (Date.now()) suffix is a uniqueness guard against
|
||||
* any future intermediate-caching behavior change (T-04-05-02).
|
||||
* The fetch(404).catch(noop) is REQUIRED — without the catch the
|
||||
* network rejection would surface as a separate js_error UserEvent
|
||||
* which A34 does not care about.
|
||||
* - Wait ~1s for both responses to land + both content-script
|
||||
* wrappers to enqueue their network_error UserEvents (the XHR
|
||||
* loadend listener is async; the fetch .then/.catch chain is async).
|
||||
* - SAVE_ARCHIVE.
|
||||
* - Host-side driveA34 JSZip-parses logs/events.json; filters for
|
||||
* network_error entries whose target contains '404-fetch-a34' /
|
||||
* '404-xhr-a34'; asserts >=1 of each + meta.status === 404.
|
||||
*
|
||||
* FORBIDDEN_HOOK_STRINGS impact: NONE. A34 rides production wrappers
|
||||
* (window.fetch + XMLHttpRequest.prototype at src/content/index.ts) +
|
||||
* chrome.tabs.* (`tabs` perm) + chrome.scripting.executeScript
|
||||
* (`scripting` perm) + existing helpers. Tier-1 inventory stays at 12.
|
||||
*/
|
||||
|
||||
/** SAVE_ARCHIVE dispatch timeout for A34 — matches A24/A25/A27/A29/A30/A31. */
|
||||
const A34_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
|
||||
const A34_SEGMENT_SETTLE_MS = 11_000;
|
||||
/** Settle after the fetch+XHR triggers so BOTH async network-error
|
||||
* wrappers in src/content/index.ts complete: the fetch .then/.catch
|
||||
* chain AND the XHR loadend listener both enqueue their UserEvent. */
|
||||
const A34_NETWORK_SETTLE_MS = 1_000;
|
||||
/** Wait after chrome.tabs.create for the tab navigation to complete so
|
||||
* the content script attaches + production wrappers are installed
|
||||
* (mirrors A30_TAB_NAVIGATION_WAIT_MS = 1.5s). */
|
||||
const A34_TAB_NAVIGATION_WAIT_MS = 1_500;
|
||||
/** Probe tab URL — example.com is RFC 2606 reserved + serves stable
|
||||
* HTML under headless Chrome (Plan 02-04 A27 / Plan 03-02 A30 parity). */
|
||||
const A34_PROBE_TAB_URL = 'https://example.com/';
|
||||
/** 404 fetch probe path — same origin as the probe tab so the fetch is
|
||||
* a same-origin GET (no CORS preflight noise). example.com serves a
|
||||
* 404 for arbitrary unknown paths. driveA34 filters network_error
|
||||
* entries whose target contains this literal. */
|
||||
const A34_404_FETCH_PATH = '/404-fetch-a34';
|
||||
/** 404 XHR probe path — distinct from the fetch path so driveA34 can
|
||||
* tell the two protocol entries apart. Same same-origin rationale. */
|
||||
const A34_404_XHR_PATH = '/404-xhr-a34';
|
||||
|
||||
/**
|
||||
* A34 — fetch + XHR network_error empirical (ROADMAP SC #2).
|
||||
*
|
||||
* Creates a fresh `https://example.com` probe tab (where the content
|
||||
* script attaches normally per Plan 03-02 cs-injection-world insight),
|
||||
* injects TWO failing-request triggers — a `fetch(404)` and an
|
||||
* `XMLHttpRequest` GET against a 404 path — into the content-script's
|
||||
* ISOLATED world via chrome.scripting.executeScript so BOTH production
|
||||
* network wrappers in src/content/index.ts intercept them, settles a
|
||||
* segment, SAVEs while the probe tab is active, finally-cleanup the
|
||||
* tab. Host-side driveA34 inspects logs/events.json and asserts:
|
||||
* - >=1 network_error entry whose target contains '404-fetch-a34'
|
||||
* (proves the fetch wrapper fired AND — per Plan 04-01 P1 #11 —
|
||||
* the target carries the real URL, not '[object Request]')
|
||||
* - >=1 network_error entry whose target contains '404-xhr-a34'
|
||||
* (proves the XHR loadend wrapper fired)
|
||||
* - the fetch entry's meta.status === 404
|
||||
* - the XHR entry's meta.status === 404
|
||||
*
|
||||
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
|
||||
* driveA34 appends the fetch/XHR presence + status checks.
|
||||
*/
|
||||
async function assertA34(): Promise<AssertionResult> {
|
||||
const result: AssertionResult = {
|
||||
passed: false,
|
||||
name: 'A34 — fetch + XHR network_error empirical (ROADMAP SC #2)',
|
||||
checks: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
|
||||
let probeTabId: number | undefined;
|
||||
|
||||
try {
|
||||
diag(result, 'Step 1: setupFreshRecording (A34 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: chrome.tabs.create(${A34_PROBE_TAB_URL}, active:true) — content script ISOLATED world is alive on https://, not on chrome-extension:// (Plan 03-02 lesson)`);
|
||||
const probeTab = await chrome.tabs.create({ url: A34_PROBE_TAB_URL, active: true });
|
||||
probeTabId = probeTab.id;
|
||||
diag(result, `Step 2 result: probeTab.id=${probeTabId}, probeTab.url=${probeTab.url ?? '<pending>'}`);
|
||||
if (probeTabId === undefined) {
|
||||
throw new Error('chrome.tabs.create returned undefined tab.id');
|
||||
}
|
||||
|
||||
diag(result, `Step 3: wait ${A34_TAB_NAVIGATION_WAIT_MS}ms for navigation + content script attach`);
|
||||
await new Promise((r) => setTimeout(r, A34_TAB_NAVIGATION_WAIT_MS));
|
||||
|
||||
diag(result, `Step 4: settle ${A34_SEGMENT_SETTLE_MS}ms for first segment rotation`);
|
||||
await new Promise((r) => setTimeout(r, A34_SEGMENT_SETTLE_MS));
|
||||
|
||||
diag(result, 'Step 5: chrome.scripting.executeScript — inject fetch(404) + XHR(404) triggers in ISOLATED world (production window.fetch + XMLHttpRequest.prototype wrappers at src/content/index.ts both intercept)');
|
||||
const injectionResults = await chrome.scripting.executeScript({
|
||||
target: { tabId: probeTabId },
|
||||
world: 'ISOLATED',
|
||||
func: async (fetchPath: string, xhrPath: string): Promise<{
|
||||
stamp: number;
|
||||
fetchUrl: string;
|
||||
xhrUrl: string;
|
||||
fetchSettled: boolean;
|
||||
xhrLoadended: boolean;
|
||||
}> => {
|
||||
// Uniqueness guard against any future intermediate-caching
|
||||
// behavior change (T-04-05-02). The 404 paths do not exist
|
||||
// today so the response is always fresh, but the stamp keeps
|
||||
// A34 robust if example.com's caching semantics ever change.
|
||||
const stamp = Date.now();
|
||||
const fetchUrl = `https://example.com${fetchPath}-${stamp}`;
|
||||
const xhrUrl = `https://example.com${xhrPath}-${stamp}`;
|
||||
|
||||
// Trigger 1 — failing fetch. The content script patches
|
||||
// window.fetch at src/content/index.ts:183 in its ISOLATED
|
||||
// world; this fetch runs in the SAME ISOLATED world so it
|
||||
// routes through the wrapper. response.ok===false →
|
||||
// addUserEvent({type:'network_error'}) at line 187. The
|
||||
// .catch(noop) is REQUIRED — without it a network-layer
|
||||
// rejection would surface as a separate js_error UserEvent
|
||||
// (window 'unhandledrejection' listener) which A34 ignores.
|
||||
let fetchSettled = false;
|
||||
try {
|
||||
await fetch(fetchUrl);
|
||||
fetchSettled = true;
|
||||
} catch {
|
||||
// Expected for a non-2xx / network failure; the production
|
||||
// wrapper's .catch branch (src/content/index.ts:206) has
|
||||
// already enqueued the network_error UserEvent by here.
|
||||
fetchSettled = true;
|
||||
}
|
||||
|
||||
// Trigger 2 — failing XHR. The content script wraps
|
||||
// XMLHttpRequest.prototype.open + .send at
|
||||
// src/content/index.ts:232/240; the loadend listener emits a
|
||||
// network_error UserEvent when xhr.status >= 400. xhr.send()
|
||||
// is fire-and-forget, so we await the loadend event here so
|
||||
// the production wrapper has enqueued its UserEvent before
|
||||
// this injected function returns.
|
||||
const xhrLoadended = await new Promise<boolean>((resolve) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', xhrUrl);
|
||||
xhr.addEventListener('loadend', () => resolve(true));
|
||||
xhr.addEventListener('error', () => resolve(true));
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
return { stamp, fetchUrl, xhrUrl, fetchSettled, xhrLoadended };
|
||||
},
|
||||
args: [
|
||||
A34_404_FETCH_PATH,
|
||||
A34_404_XHR_PATH,
|
||||
],
|
||||
});
|
||||
const injectionSummary = injectionResults[0]?.result ?? null;
|
||||
diag(result, `Step 5 result: ${JSON.stringify(injectionSummary)}`);
|
||||
|
||||
diag(result, `Step 6: settle ${A34_NETWORK_SETTLE_MS}ms so BOTH async wrappers (fetch .then/.catch + XHR loadend) finish enqueuing their network_error UserEvent`);
|
||||
await new Promise((r) => setTimeout(r, A34_NETWORK_SETTLE_MS));
|
||||
|
||||
diag(result, 'Step 7: dispatch SAVE_ARCHIVE (probe tab is the active tab; SW will harvest from there)');
|
||||
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
|
||||
{ type: 'SAVE_ARCHIVE' },
|
||||
A34_SAVE_ARCHIVE_TIMEOUT_MS,
|
||||
'SAVE_ARCHIVE (A34)',
|
||||
);
|
||||
diag(result, `Step 7 result: ${JSON.stringify(ack)}`);
|
||||
|
||||
result.checks.push({
|
||||
name: 'A34.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}`);
|
||||
} finally {
|
||||
// T-02-04-04 mitigation parity (Plan 03-02 / 03-03 precedent):
|
||||
// cleanup probe tab with silent-ignore on already-closed.
|
||||
if (probeTabId !== undefined) {
|
||||
try {
|
||||
await chrome.tabs.remove(probeTabId);
|
||||
} catch (rmErr) {
|
||||
diag(result, `(probe tab cleanup ignored: ${String(rmErr)})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read `chrome.runtime.getManifest().version`. Used by the host-side
|
||||
* orchestrator at startup to capture the expected version for A13's
|
||||
@@ -4077,6 +4310,8 @@ declare global {
|
||||
assertA30: () => Promise<AssertionResult>;
|
||||
// Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02)
|
||||
assertA31: () => Promise<AssertionResult>;
|
||||
// Plan 04-05 — fetch + XHR network_error empirical (ROADMAP SC #2)
|
||||
assertA34: () => Promise<AssertionResult>;
|
||||
getManifestVersion: () => Promise<string>;
|
||||
};
|
||||
}
|
||||
@@ -4114,14 +4349,15 @@ window.__mokoshHarness = {
|
||||
assertA29,
|
||||
assertA30,
|
||||
assertA31,
|
||||
assertA34,
|
||||
getManifestVersion,
|
||||
};
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
if (statusEl !== null) {
|
||||
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A31, getManifestVersion} available.';
|
||||
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A31, assertA34, 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 + Plan 03-02: A30 + Plan 03-03: A31 + 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 + Plan 03-03: A31 + Plan 04-05: A34 + getManifestVersion)');
|
||||
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user