feat(04-05): A34 host-side + orchestrator — fetch+XHR network_error empirical (ROADMAP SC #2 GREEN)
- Append driveA34 host-side: JSZip-parse logs/events.json + filter network_error entries by '404-fetch-a34' / '404-xhr-a34' target marker; assert >=1 of each + meta.status === 404 - readMetaStatus helper narrows UserEvent.meta.status (typed Record<string,unknown>) to number without an unchecked any cast - 3-site orchestrator wiring in harness.test.ts: import binding, driveA34Wrapped (downloadsDir closure), drivers-array push entry - UAT harness 34 -> 35; skip-mode (SKIP_LONG_UAT=1) 35/35 GREEN - A34 empirical: fetch entry target carries the real URL (https://example.com/404-fetch-a34-<stamp>), NOT '[object Request]' — Plan 04-01 P1 #11 fix validated end-to-end at the SAVE->archive layer; XHR entry confirms the distinct prototype-wrapper path; both meta.status === 404 (ROADMAP SC #2 closed) - vitest baseline 184/184 GREEN preserved (no unit tests this plan) - FORBIDDEN_HOOK_STRINGS unchanged at 12 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,9 @@ import {
|
|||||||
// reframe per debug session-2 verdict; needs Browser + extensionId for
|
// reframe per debug session-2 verdict; needs Browser + extensionId for
|
||||||
// CDP-based SW kill + downloadsDir for host-side JSZip parse).
|
// CDP-based SW kill + downloadsDir for host-side JSZip parse).
|
||||||
driveA33,
|
driveA33,
|
||||||
|
// Plan 04-05 — driveA34 fetch + XHR network_error empirical (ROADMAP SC #2;
|
||||||
|
// needs downloadsDir for host-side JSZip parse of logs/events.json).
|
||||||
|
driveA34,
|
||||||
getManifestVersion,
|
getManifestVersion,
|
||||||
} from './lib/harness-page-driver';
|
} from './lib/harness-page-driver';
|
||||||
import {
|
import {
|
||||||
@@ -363,6 +366,10 @@ async function main(): Promise<number> {
|
|||||||
// AND downloadsDir for host-side JSZip parse of post-restart zip.
|
// AND downloadsDir for host-side JSZip parse of post-restart zip.
|
||||||
const driveA33Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
const driveA33Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||||
(page) => driveA33(page, handles.browser, handles.extensionId, handles.downloadsDir);
|
(page) => driveA33(page, handles.browser, handles.extensionId, handles.downloadsDir);
|
||||||
|
// Plan 04-05 — driveA34 needs downloadsDir for host-side JSZip parse of
|
||||||
|
// logs/events.json (fetch + XHR network_error entry inspection).
|
||||||
|
const driveA34Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||||
|
(page) => driveA34(page, handles.downloadsDir);
|
||||||
|
|
||||||
const drivers: ReadonlyArray<{
|
const drivers: ReadonlyArray<{
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
@@ -514,6 +521,17 @@ async function main(): Promise<number> {
|
|||||||
})
|
})
|
||||||
: driveA33Wrapped,
|
: driveA33Wrapped,
|
||||||
},
|
},
|
||||||
|
// Plan 04-05 A34: fetch + XHR network_error empirical (ROADMAP SC #2).
|
||||||
|
// Verifies both protocol paths in src/content/index.ts setupNetworkLogging
|
||||||
|
// produce events.json entries. Empirically validates Plan 04-01 P1 #11
|
||||||
|
// fetch URL extraction fix at the SAVE->archive layer (A34.4 + A34.5).
|
||||||
|
// A34 owns its SAVE because event-log cleanup runs every 60s
|
||||||
|
// (src/content/index.ts CLEANUP_INTERVAL_MS) and the 2 synthetic
|
||||||
|
// failing requests need a fresh event-log window. Opens a fresh
|
||||||
|
// https://example.com probe tab + injects fetch(404)+XHR(404) via
|
||||||
|
// chrome.scripting.executeScript ISOLATED-world. Runs ~25s (always
|
||||||
|
// RUN — not env-gated; the 5-min wait is A33's, not A34's).
|
||||||
|
{ name: 'A34', drive: driveA34Wrapped },
|
||||||
];
|
];
|
||||||
|
|
||||||
const buffers = { swConsole: handles.swConsole, offConsole: handles.offConsole };
|
const buffers = { swConsole: handles.swConsole, offConsole: handles.offConsole };
|
||||||
|
|||||||
@@ -2710,3 +2710,203 @@ export async function driveA33(
|
|||||||
diagnostics,
|
diagnostics,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Plan 04-05 — driveA34 (fetch + XHR network_error host-side) ───── */
|
||||||
|
|
||||||
|
/** Substring marker for the fetch 404 probe path — same literal the
|
||||||
|
* page-side A34_404_FETCH_PATH constant produces. driveA34 filters
|
||||||
|
* network_error entries whose target contains this. */
|
||||||
|
const A34_FETCH_MARKER = '404-fetch-a34';
|
||||||
|
/** Substring marker for the XHR 404 probe path — same literal the
|
||||||
|
* page-side A34_404_XHR_PATH constant produces. */
|
||||||
|
const A34_XHR_MARKER = '404-xhr-a34';
|
||||||
|
/** Expected HTTP status for both the fetch + XHR 404 probes. */
|
||||||
|
const A34_EXPECTED_STATUS = 404;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a numeric `status` field out of a UserEvent.meta record.
|
||||||
|
*
|
||||||
|
* `UserEvent.meta` is typed `Record<string, unknown>` (src/shared/types.ts),
|
||||||
|
* so the production wrappers' `meta.status` value arrives untyped. This
|
||||||
|
* helper narrows it to a number (or `null` when absent / non-numeric)
|
||||||
|
* without an unchecked `any` cast.
|
||||||
|
*
|
||||||
|
* @param event - The UserEvent whose meta.status to read.
|
||||||
|
* @returns The numeric status, or null when meta.status is missing or
|
||||||
|
* not a number.
|
||||||
|
*/
|
||||||
|
function readMetaStatus(event: UserEvent): number | null {
|
||||||
|
const status = event.meta?.status;
|
||||||
|
return typeof status === 'number' ? status : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A34 (Plan 04-05 — ROADMAP SC #2: fetch + XHR network_error).
|
||||||
|
*
|
||||||
|
* Page-side assertA34 opened a fresh https://example.com probe tab,
|
||||||
|
* injected a `fetch(404)` + 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 (window.fetch + XMLHttpRequest.prototype)
|
||||||
|
* intercept the failing requests, settled, SAVEd, finally-cleanup the
|
||||||
|
* tab. Host-side driveA34 JSZip-parses 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 the literal
|
||||||
|
* '[object Request]' that pre-fix implicit coercion produced)
|
||||||
|
* - >=1 network_error entry whose target contains '404-xhr-a34'
|
||||||
|
* (proves the XHR loadend wrapper fired — the distinct code path
|
||||||
|
* A30 never exercised)
|
||||||
|
* - the fetch entry's meta.status === 404 (end-to-end status pin)
|
||||||
|
* - the XHR entry's meta.status === 404
|
||||||
|
*
|
||||||
|
* Filter-pipeline form (no `continue`) per CLAUDE.md Control Flow §.
|
||||||
|
*
|
||||||
|
* Checks (6 total — 1 page-side + 5 host-side):
|
||||||
|
* - A34.1: SAVE_ARCHIVE ack success (page-side)
|
||||||
|
* - A34.0a: logs/events.json entry exists in zip
|
||||||
|
* - A34.2: >=1 fetch network_error entry (Plan 04-01 P1 #11 end-to-end)
|
||||||
|
* - A34.3: >=1 XHR network_error entry
|
||||||
|
* - A34.4: fetch entry meta.status === 404
|
||||||
|
* - A34.5: XHR entry meta.status === 404
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @param downloadsDir - Absolute path to the per-run downloads directory.
|
||||||
|
* @returns AssertionRecord with the merged checks.
|
||||||
|
*/
|
||||||
|
export async function driveA34(
|
||||||
|
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.assertA34();
|
||||||
|
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: 'A34.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(`A34 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: 'A34.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: 'A34.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 networkErrors = userEvents.filter((e) => e.type === 'network_error');
|
||||||
|
const fetchEntries = networkErrors.filter(
|
||||||
|
(e) => typeof e.target === 'string' && e.target.includes(A34_FETCH_MARKER),
|
||||||
|
);
|
||||||
|
const xhrEntries = networkErrors.filter(
|
||||||
|
(e) => typeof e.target === 'string' && e.target.includes(A34_XHR_MARKER),
|
||||||
|
);
|
||||||
|
mergedDiagnostics.push(`A34 userEvents.length=${userEvents.length}, network_error count=${networkErrors.length}`);
|
||||||
|
mergedDiagnostics.push(
|
||||||
|
`A34 fetch-entry count=${fetchEntries.length}, xhr-entry count=${xhrEntries.length}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchStatus = fetchEntries.length > 0 ? readMetaStatus(fetchEntries[0]) : null;
|
||||||
|
const xhrStatus = xhrEntries.length > 0 ? readMetaStatus(xhrEntries[0]) : null;
|
||||||
|
mergedDiagnostics.push(
|
||||||
|
`A34 fetch-entry[0].target=${fetchEntries[0]?.target ?? '<none>'} meta.status=${fetchStatus ?? '<none>'}`,
|
||||||
|
);
|
||||||
|
mergedDiagnostics.push(
|
||||||
|
`A34 xhr-entry[0].target=${xhrEntries[0]?.target ?? '<none>'} meta.status=${xhrStatus ?? '<none>'}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
mergedChecks.push({
|
||||||
|
name: "A34.2: fetch 404 produced network_error entry containing '404-fetch-a34' (Plan 04-01 P1 #11 end-to-end — target carries the real URL, not '[object Request]')",
|
||||||
|
expected: '>=1 fetch entry',
|
||||||
|
actual: fetchEntries.length,
|
||||||
|
passed: fetchEntries.length >= 1,
|
||||||
|
});
|
||||||
|
mergedChecks.push({
|
||||||
|
name: "A34.3: XHR 404 produced network_error entry containing '404-xhr-a34' (distinct XMLHttpRequest.prototype wrapper path)",
|
||||||
|
expected: '>=1 XHR entry',
|
||||||
|
actual: xhrEntries.length,
|
||||||
|
passed: xhrEntries.length >= 1,
|
||||||
|
});
|
||||||
|
mergedChecks.push({
|
||||||
|
name: `A34.4: fetch network_error entry meta.status === ${A34_EXPECTED_STATUS}`,
|
||||||
|
expected: A34_EXPECTED_STATUS,
|
||||||
|
actual: fetchStatus,
|
||||||
|
passed: fetchStatus === A34_EXPECTED_STATUS,
|
||||||
|
});
|
||||||
|
mergedChecks.push({
|
||||||
|
name: `A34.5: XHR network_error entry meta.status === ${A34_EXPECTED_STATUS}`,
|
||||||
|
expected: A34_EXPECTED_STATUS,
|
||||||
|
actual: xhrStatus,
|
||||||
|
passed: xhrStatus === A34_EXPECTED_STATUS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedPassed = mergedChecks.every((c) => c.passed);
|
||||||
|
return {
|
||||||
|
passed: mergedPassed,
|
||||||
|
name: pageResult.name,
|
||||||
|
checks: mergedChecks,
|
||||||
|
diagnostics: mergedDiagnostics,
|
||||||
|
error: pageResult.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user