feat(02-04): harness A24 — empirical Blob URL download verification (D-P2-01 closes P0-6)

Wire A24 into the Plan 01-13 Approach B UAT harness as the binding empirical
gate for D-P2-01. A24 verifies end-to-end that SAVE_ARCHIVE → chrome.downloads.
download receives a `blob:` URL prefix (NOT `data:application/zip;base64,`),
closing audit P0-6 functionally. The Plan 02-02 unit tests pin the wire-format
at the SW↔offscreen boundary; A24 pins it at the chrome.downloads platform
boundary through a real Chrome instance.

Strategy: chrome.downloads.onCreated listener captures the URL cross-realm.
The plan's <action> block proposed a chrome.downloads.download monkey-patch
installed in the harness page realm — but that intercepts only same-realm
calls, missing the SW's call. The canonical cross-realm capture pattern is
chrome.downloads.onCreated (fires for any download initiated by any extension
realm, with the full DownloadItem including .url). Documented as a deviation
from the plan's pseudo-code in SUMMARY.md (Rule 1 — bug fix vs the pseudo-code
strategy; same A24 contract verified, correct mechanism).

Files modified:
- tests/uat/extension-page-harness.ts (+~150 lines): assertA24 + A24_* constants
- tests/uat/lib/harness-page-driver.ts (+~30 lines): driveA24 page.evaluate wrapper
- tests/uat/harness.test.ts (+~10 lines): import driveA24, append to drivers list

Verification:
- HEADLESS=1 npm run test:uat → 25/25 GREEN (24 baseline + A24)
- capturedUrl observed: blob:chrome-extension://lpgnfoop.../...
- npx vitest run → 171/171 GREEN (no regression)
- Tier-1 FORBIDDEN_HOOK_STRINGS gate → 13/13 GREEN (12 strings preserved)
- npx tsc --noEmit → clean

Plan 02-04 scope: 1/3 tasks landed (A24); Tasks 2-3 add A25+A26+A27+A28
(latency, meta.json shape, multi-tab strict, REQ-archive-layout strict).
This commit is contained in:
2026-05-20 16:41:36 +02:00
parent 3821e5c402
commit 4ae73250fa
3 changed files with 219 additions and 4 deletions

View File

@@ -1178,6 +1178,35 @@ export async function driveA23(page: Page): Promise<AssertionRecord> {
}) as AssertionRecord;
}
/* ─── Plan 02-04 Task 1 — driveA24 (Blob URL empirical D-P2-01) ─────── */
/**
* Drive A24 (Plan 02-04 D-P2-01 empirical Blob URL verification).
* Standard page.evaluate wrapper — page side installs the
* chrome.downloads.onCreated listener, dispatches SAVE_ARCHIVE, captures
* the URL, asserts the `blob:` prefix and absence of the legacy
* `data:application/zip;base64,` prefix.
*
* Chains AFTER driveA23 in the orchestrator. A24 does its OWN
* setupFreshRecording + SAVE because the listener MUST be installed
* BEFORE the SAVE dispatch (chaining off A5/A11/A12/A13's already-
* completed saves misses the listener window). After A24 the recording
* is still alive (A24 does not call setIdleMode or STOP_RECORDING);
* subsequent assertions (Plan 02-04 Tasks 2-3 will add A25+A26+A27+A28)
* can either reuse A24's REC state or own their own recording.
*
* @param page - The harness page from `launchHarnessBrowser`.
* @returns Structured AssertionRecord with 4 checks (A24.1..A24.4).
*/
export async function driveA24(page: Page): Promise<AssertionRecord> {
return 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.assertA24();
return r;
}) as AssertionRecord;
}
// Note (Wave 3D): the AssertionWithBytes interface is retained at the
// top of this file as a public export — but Wave 3D's drivers no
// longer use it (the host side now does all bytes-handling internally