Plan 01-13 Task 6 (Wave 3C). Wires the final three Wave-3 assertions
before A11+A12+A13 (Wave 3D — 35s segments / ffprobe / zip shape):
- A8 (Bug A canonical regression rewind) — invokes
chrome.notifications.create from the harness page with the SAME options
the production SW onStartup handler uses (iconUrl resolved via
chrome.runtime.getURL('icons/icon128.png')). Exercises Chrome's
imageUtil icon validation — the exact code path Bug A regressed on
(a881bf0). 4 checks: non-empty assignedId, id-honoring, getAll delta=1,
prefix set-membership. The SW handler invocation itself remains
covered by tests/background/onstartup-notification.test.ts (unit
tier); A8 covers the end-to-end imageUtil-acceptance gate (e2e tier).
Per T-1-13-06 threat-model row: unit + e2e are intentional defense in
depth covering both halves of the Bug A contract.
- A9 (icon file sizes meet imageUtil floors) — fetches icons/icon{16,48,
128}.png via chrome.runtime.getURL and asserts blob.size against the
200/500/1024-byte silent-rejection floors per assets-spec.md. Cheap
pre-check for the Bug A class: a future icon swap that drops below
the floor would silently break the notification flow; A9 catches it
BEFORE the SW even tries to create.
- A10 (manifest shape contract) — chrome.runtime.getManifest() asserts:
permissions includes 'notifications' (without it,
chrome.notifications.create is unreachable), icons['16/48/128']
defined + non-empty, action.default_icon['16/48/128'] same. 7 checks
total. Catches manifest-edit regressions that would silently break A8.
Bug A canonical RED-on-regression demo cycle
============================================
Regression trigger: head -c 50 /tmp/icon128.png.backup > icons/icon128.png
(truncates the 2615-byte PNG to 50 bytes — preserves PNG magic so
manifest loads, but Chrome's imageUtil silent-rejects the create).
RED — A8 standalone driver with truncated icon128.png (50 bytes):
A8 — BUG A canonical: chrome.notifications.create accepts startup-icon (imageUtil contract): FAIL
Top-level error: notifications.create rejected: Unable to download all specified images.
Diagnostics:
- Step 1: snapshot notif count + ids BEFORE create
- Step 1 result: 0 active; ids=[]
- Step 2: chrome.notifications.create(id='mokosh-startup-1779124969677', iconUrl='chrome-extension://<ext-id>/icons/icon128.png')
- THREW: notifications.create rejected: Unable to download all specified images.
GREEN — A8 standalone driver after restoring icon128.png (2615 bytes):
A8 — BUG A canonical: chrome.notifications.create accepts startup-icon (imageUtil contract): PASS
Checks:
[PASS] A8.1: create callback resolves with non-empty assignedId (imageUtil acceptance)
expected: "non-empty string"
actual: "mokosh-startup-1779124999809"
[PASS] A8.2: assignedId matches input id (chrome.notifications honors caller-supplied id)
expected: "mokosh-startup-1779124999809"
actual: "mokosh-startup-1779124999809"
[PASS] A8.3: notification count delta === 1 (exactly one new startup notification)
expected: 1
actual: 1
[PASS] A8.4: at least one notification id startsWith 'mokosh-startup-' (set membership)
expected: true
actual: true
The RED→GREEN cycle proves the harness empirically catches Bug A
regression class (imageUtil silent rejection on undersized iconUrl PNG).
The "Unable to download all specified images." rejection is Chrome's
internal error surface for the same imageUtil validation that Bug A
originally regressed on (fix at a881bf0). Note: under the full
orchestrator order, the same truncation surfaces FIRST at A7 (recovery
notification, which shares NOTIFICATION_ICON_PATH) — orchestrator
bail-on-first-failure means A8 isn't reached in the full run. The
isolated A8 demo above (via an ephemeral local driver script, NOT
committed) confirmed A8 catches the same regression independently.
Baseline preserved
==================
- vitest: 93/93 GREEN (SKIP_BUILD=1 to dodge the pre-existing
~5s-default test timeout in no-test-hooks-in-prod-bundle.test.ts;
with a fresh dist/ in place all 9 hook-string sub-tests PASS).
- tsc: clean (no diagnostics).
- npm run build: exit 0; production bundle unchanged
(no SW/offscreen src edits — only tests/ + dist-test/).
- npm run test:uat: 11/14 GREEN (A0+A1+A2+A3+A4+A5+A6+A7+A8+A9+A10);
bails at A11 (Wave 3D wires that).
Files touched
=============
- tests/uat/extension-page-harness.ts: +assertA8 +assertA9 +assertA10
with 4 + 3 + 7 checks respectively; +createNotificationPromise +
getActiveNotificationIds + STARTUP_NOTIF_PREFIX + A8_GETALL_SETTLE_MS
+ A9_ICON_SPEC helpers. window.__mokoshHarness extends 7 → 10 methods.
- tests/uat/lib/harness-page-driver.ts: replaces driveA8/driveA9/driveA10
NYI stubs with page.evaluate wrappers.
- tests/uat/harness.test.ts: updates Wave-3C-current comment block to
reflect A8+A9+A10 wired (expected diagnostic 11/14, bail at A11).
Approach rationale (per plan resolved-questions §A8)
====================================================
The plan resolved A8's "no SW-side handler-capture hook" challenge with
an explicit SIMPLER WORKAROUND: invoke chrome.notifications.create
DIRECTLY from the harness page with the same production options. This
sidesteps the MV3-SW-dynamic-import block (01-11-SUMMARY) while still
exercising Chrome's imageUtil validation — the exact code path Bug A
broke. Approach considered but rejected per the plan: a SW-side
static eager-import test hook + a __mokoshTriggerStartup message
handler would have required adding a new production code path (even
gated by __MOKOSH_UAT__) and a new FORBIDDEN_HOOK_STRINGS entry. The
page-direct approach adds ZERO production surface and ZERO new
forbidden strings — strictly better.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
413 lines
18 KiB
TypeScript
413 lines
18 KiB
TypeScript
// tests/uat/lib/harness-page-driver.ts — Plan 01-13 Wave 2.
|
|
//
|
|
// Driver wrappers — one per assertion (A1..A13). Each wraps a single
|
|
// `page.evaluate(() => window.__mokoshHarness.assertXX())` call,
|
|
// returning the structured AssertionRecord (or the extended shape with
|
|
// `bytesBase64` for A5/A12/A13 which return host-side-required payloads
|
|
// like the downloaded zip bytes or the recorded webm bytes).
|
|
//
|
|
// Centralizing the page.evaluate call here means adding or renaming an
|
|
// assertion requires a two-file edit:
|
|
// 1. extension-page-harness.ts — page-side impl + window.__mokoshHarness wire
|
|
// 2. this file — host-side driver wrapper
|
|
// instead of touching every test-file that calls the assertion.
|
|
//
|
|
// Wave 2 ONLY wires `driveA6` (the proven assertion from the c647f61
|
|
// prototype). The 12 Wave-3 assertions are stubbed as `throw new
|
|
// Error('NOT YET IMPLEMENTED — Wave 3<X> wires this')` so the
|
|
// orchestrator's `for (const drive of drivers)` loop fails cleanly on
|
|
// the first unimplemented one (bail-on-first-failure semantics in
|
|
// `harness.test.ts` lands in Wave 3A).
|
|
//
|
|
// Wave 3A wires driveA1/A2/A3/A4 (page-side surface in
|
|
// `extension-page-harness.ts` from the same wave).
|
|
// Wave 3B wires driveA5 (page-side ack + HOST-side fs polling for the
|
|
// dropped `session_report_*.zip` in `handles.downloadsDir`) + driveA7
|
|
// (standard page.evaluate wrapper). The driveA5 signature requires a
|
|
// second `downloadsDir` argument; the orchestrator at `harness.test.ts`
|
|
// threads `handles.downloadsDir` through.
|
|
//
|
|
// References:
|
|
// - puppeteer Page.evaluate:
|
|
// https://pptr.dev/api/puppeteer.page.evaluate
|
|
// - Node fs.readdirSync / statSync:
|
|
// https://nodejs.org/api/fs.html
|
|
|
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
import { resolve as resolvePath } from 'node:path';
|
|
|
|
import type { Page } from 'puppeteer';
|
|
|
|
import type { AssertionRecord, CheckRecord } from './assertions';
|
|
|
|
/**
|
|
* Extended assertion-record shape for A5/A12/A13 which return
|
|
* host-side-required binary payloads:
|
|
* - A5 (SAVE_ARCHIVE): `bytesBase64` is the downloaded zip bytes
|
|
* (read by host-side from `handles.downloadsDir`); page side only
|
|
* returns the trigger ack.
|
|
* - A12 (ffprobe): `bytesBase64` is the recorded webm bytes —
|
|
* extracted from the zip by the host so ffprobe (host-side binary)
|
|
* can analyze it.
|
|
* - A13 (zip shape): `bytesBase64` is the zip bytes; `expectedVersion`
|
|
* is the manifest version the harness was built against.
|
|
*
|
|
* All Wave-3 assertions; not used in Wave 2.
|
|
*/
|
|
export interface AssertionWithBytes {
|
|
readonly passed: boolean;
|
|
readonly name: string;
|
|
readonly checks: ReadonlyArray<CheckRecord>;
|
|
readonly diagnostics: ReadonlyArray<string>;
|
|
readonly error?: string;
|
|
readonly bytesBase64?: string;
|
|
readonly expectedVersion?: string;
|
|
}
|
|
|
|
/** Marker error message for unimplemented Wave-3 drivers — orchestrator
|
|
* matches on this prefix to format the diagnostic distinctly from a
|
|
* genuine assertion failure. */
|
|
const WAVE3_STUB_PREFIX = 'NOT YET IMPLEMENTED';
|
|
|
|
/**
|
|
* Drive the A6 (Bug B canonical) assertion. The proven, prototype-
|
|
* inherited driver. Page side does all orchestration (ensureOffscreen +
|
|
* start + wait + dispatch + assert); host side just triggers + reads
|
|
* the result.
|
|
*
|
|
* @param page - The harness page (from `launchHarnessBrowser`).
|
|
* @returns Structured AssertionRecord with 5 checks (SETUP + A6.1..A6.4).
|
|
*/
|
|
export async function driveA6(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.assertA6();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/* ─── Wave 3A — WIRED ─────────────────────────────────────────────── */
|
|
|
|
/**
|
|
* Drive A1 (SW bootstrap state). Asserts the post-load idle-mode state:
|
|
* badge='', popup='', isRecording=false. MUST run BEFORE A2 in any
|
|
* orchestrated sequence — A2 manually sets badge='REC' which invalidates
|
|
* the A1 contract until the SW is reset.
|
|
*
|
|
* @param page - The harness page from `launchHarnessBrowser`.
|
|
* @returns Structured AssertionRecord with 3 checks (badge + popup + isRecording).
|
|
*/
|
|
export async function driveA1(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.assertA1();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/**
|
|
* Drive A2 (toolbar onClicked → REC). Uses the direct-offscreen workaround
|
|
* for the missing `tabs` manifest permission (per 01-11-SUMMARY). Leaves
|
|
* the offscreen recording active — A3 + A4 chain off A2's REC state.
|
|
*
|
|
* @param page - The harness page from `launchHarnessBrowser`.
|
|
* @returns Structured AssertionRecord with 2 checks (badge + popup).
|
|
*/
|
|
export async function driveA2(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.assertA2();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/**
|
|
* Drive A3 (displaySurface === 'monitor'). Assumes A2 left recording
|
|
* active. Queries the offscreen `get-display-surface` bridge op.
|
|
*
|
|
* @param page - The harness page from `launchHarnessBrowser`.
|
|
* @returns Structured AssertionRecord with 1 check (displaySurface).
|
|
*/
|
|
export async function driveA3(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.assertA3();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/**
|
|
* Drive A4 (popup pinned + single offscreen during recording). Assumes
|
|
* A2 left recording active. Verifies getPopup unchanged + hasDocument
|
|
* true (no duplicate offscreen spawned).
|
|
*
|
|
* @param page - The harness page from `launchHarnessBrowser`.
|
|
* @returns Structured AssertionRecord with 2 checks (popup + hasDocument).
|
|
*/
|
|
export async function driveA4(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.assertA4();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/* ─── Wave 3B — WIRED ─────────────────────────────────────────────── */
|
|
|
|
/** Maximum wait for the SAVE_ARCHIVE zip to appear in `downloadsDir`. */
|
|
const A5_DOWNLOAD_POLL_TIMEOUT_MS = 15_000;
|
|
/** Polling cadence while waiting for the zip. */
|
|
const A5_DOWNLOAD_POLL_INTERVAL_MS = 200;
|
|
/** Filename suffix for the dropped archive. Production code in
|
|
* `src/background/index.ts:downloadArchive` requests
|
|
* `session_report_<date>_<time>.zip`, BUT under CDP-routed downloads
|
|
* (`Browser.setDownloadBehavior`) Chrome ignores the
|
|
* `chrome.downloads.download` `filename` parameter for `data:` URLs and
|
|
* defaults to `download.zip` (or `download (N).zip` on collision). The
|
|
* contract A5 verifies is "a zip file lands in downloadsDir within
|
|
* timeout" — the exact filename is not load-bearing for Wave 3B.
|
|
* Wave 3D's A13 (zip structure) verifies the zip content. */
|
|
const A5_ZIP_NAME_SUFFIX = '.zip';
|
|
/** Minimum acceptable zip size — the production
|
|
* `downloadArchive` always writes at least a JSZip header + screenshot
|
|
* PNG (typically several KB even with an empty video buffer).
|
|
* 1KB is the floor specified in the plan's success criteria for A5. */
|
|
const A5_MIN_ZIP_SIZE_BYTES = 1024;
|
|
|
|
/**
|
|
* Drive A5 (SAVE_ARCHIVE download). Three-phase orchestration:
|
|
*
|
|
* 1. Page side: send SAVE_ARCHIVE via the harness `assertA5` helper.
|
|
* Returns AssertionRecord with check `A5.1: SW handler returns
|
|
* success=true`. Throws are caught + returned as a failure record
|
|
* with `error` set.
|
|
*
|
|
* 2. Host side: poll `downloadsDir` for `session_report_*.zip` for up
|
|
* to `A5_DOWNLOAD_POLL_TIMEOUT_MS`. If found, read bytes for the
|
|
* size check; the bytes are NOT returned to the orchestrator (no
|
|
* consumer in Wave 3B — A13 will read them out of the zip-shape
|
|
* driver in Wave 3D).
|
|
*
|
|
* 3. Host side: assert `zipSize >= A5_MIN_ZIP_SIZE_BYTES`. Merge the
|
|
* host-side check onto the page-side AssertionRecord; recompute
|
|
* `passed` as the conjunction of all checks.
|
|
*
|
|
* The split between page-side (SW dispatch ack) and host-side
|
|
* (file-system verification) is dictated by the page isolate's lack of
|
|
* filesystem access — `handles.downloadsDir` is a Node-side `mkdtempSync`
|
|
* configured via CDP `Browser.setDownloadBehavior` and only readable
|
|
* from the Node process.
|
|
*
|
|
* @param page - The harness page from `launchHarnessBrowser`.
|
|
* @param downloadsDir - Absolute path to the per-run downloads directory
|
|
* (from `handles.downloadsDir`).
|
|
* @returns AssertionRecord with merged page + host checks.
|
|
*/
|
|
export async function driveA5(
|
|
page: Page,
|
|
downloadsDir: string,
|
|
): Promise<AssertionRecord> {
|
|
// Snapshot existing zip files BEFORE dispatching SAVE_ARCHIVE so the
|
|
// post-dispatch poll only considers NEW files. Single-browser orchestrator
|
|
// pattern means there should never be a pre-existing zip on a fresh
|
|
// run, but a re-used `downloadsDir` (`HARNESS_DOWNLOADS_DIR` env override)
|
|
// can legitimately have prior runs' files.
|
|
const preExisting = new Set(readdirSync(downloadsDir).filter(isZipFilename));
|
|
|
|
// Phase 1: page-side dispatch.
|
|
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.assertA5();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
|
|
// Phase 2: host-side poll for the dropped zip.
|
|
let zipFilename: string | null = null;
|
|
let zipBytes: Buffer | null = null;
|
|
const pollStart = Date.now();
|
|
while (Date.now() - pollStart < A5_DOWNLOAD_POLL_TIMEOUT_MS) {
|
|
const candidates = readdirSync(downloadsDir).filter(
|
|
(name) => isZipFilename(name) && !preExisting.has(name),
|
|
);
|
|
if (candidates.length > 0) {
|
|
// Take the most-recently-modified to be deterministic if multiple appear.
|
|
const sorted = candidates
|
|
.map((name) => ({
|
|
name,
|
|
mtime: statSync(resolvePath(downloadsDir, name)).mtimeMs,
|
|
}))
|
|
.sort((a, b) => b.mtime - a.mtime);
|
|
zipFilename = sorted[0].name;
|
|
const zipPath = resolvePath(downloadsDir, zipFilename);
|
|
// Wait a beat: the file may still be writing. Re-check size stable
|
|
// by reading twice; we take the second read as the canonical bytes.
|
|
const sizeFirst = statSync(zipPath).size;
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
const sizeSecond = statSync(zipPath).size;
|
|
if (sizeFirst === sizeSecond) {
|
|
zipBytes = readFileSync(zipPath);
|
|
break;
|
|
}
|
|
}
|
|
await new Promise((r) => setTimeout(r, A5_DOWNLOAD_POLL_INTERVAL_MS));
|
|
}
|
|
|
|
// Phase 3: merge checks. Page-side checks are immutable
|
|
// (ReadonlyArray); copy into a mutable buffer + append host-side.
|
|
const mergedChecks: CheckRecord[] = pageResult.checks.slice();
|
|
const mergedDiagnostics: string[] = pageResult.diagnostics.slice();
|
|
|
|
const zipPresent = zipFilename !== null;
|
|
const zipSize = zipBytes !== null ? zipBytes.length : 0;
|
|
mergedChecks.push({
|
|
name: `A5.2: a *.zip file appears in downloadsDir within ${A5_DOWNLOAD_POLL_TIMEOUT_MS}ms (production name: 'session_report_*.zip'; CDP fallback: 'download*.zip')`,
|
|
expected: true,
|
|
actual: zipPresent,
|
|
passed: zipPresent,
|
|
});
|
|
mergedChecks.push({
|
|
name: `A5.3: zip file size >= ${A5_MIN_ZIP_SIZE_BYTES} bytes`,
|
|
expected: A5_MIN_ZIP_SIZE_BYTES,
|
|
actual: zipSize,
|
|
passed: zipSize >= A5_MIN_ZIP_SIZE_BYTES,
|
|
});
|
|
mergedDiagnostics.push(
|
|
`host-side: zipFilename=${zipFilename ?? '<missing>'}, zipSize=${zipSize} bytes, downloadsDir=${downloadsDir}`,
|
|
);
|
|
|
|
const mergedPassed = mergedChecks.every((c) => c.passed);
|
|
return {
|
|
passed: mergedPassed,
|
|
name: pageResult.name,
|
|
checks: mergedChecks,
|
|
diagnostics: mergedDiagnostics,
|
|
error: pageResult.error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Filename predicate — matches any completed `.zip` file. Mid-write
|
|
* `.crdownload` files are auto-excluded by the suffix anchor. The
|
|
* permissive prefix matches both the production filename
|
|
* `session_report_<ts>.zip` and the CDP-fallback `download.zip` (see
|
|
* `A5_ZIP_NAME_SUFFIX` comment for why the latter happens under
|
|
* `Browser.setDownloadBehavior`).
|
|
*
|
|
* @param name - Filename (basename, not full path).
|
|
* @returns True iff `name` is a completed zip.
|
|
*/
|
|
function isZipFilename(name: string): boolean {
|
|
return name.endsWith(A5_ZIP_NAME_SUFFIX);
|
|
}
|
|
|
|
/**
|
|
* Drive A7 (genuine error → ERR + recovery notification). Standard
|
|
* page.evaluate wrapper — all orchestration (setupFreshRecording +
|
|
* notification snapshot + RECORDING_ERROR dispatch + post-state read)
|
|
* happens page-side. Host side just triggers + reads the result.
|
|
*
|
|
* @param page - The harness page from `launchHarnessBrowser`.
|
|
* @returns Structured AssertionRecord with 4 checks (A7.1..A7.4).
|
|
*/
|
|
export async function driveA7(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.assertA7();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/* ─── Wave 3C — WIRED ─────────────────────────────────────────────── */
|
|
|
|
/**
|
|
* Drive A8 (Bug A canonical regression rewind — onStartup notification
|
|
* creates). Standard page.evaluate wrapper — all orchestration
|
|
* (chrome.notifications.create dispatch + getAll snapshot + delta +
|
|
* set-membership check) happens page-side. The page calls
|
|
* chrome.notifications.create with the SAME options the SW onStartup
|
|
* handler uses (icon path, title, message), so the assertion exercises
|
|
* the same Chrome `imageUtil` validation that Bug A regressed against
|
|
* — without needing a SW-side hook (forbidden under Approach B per
|
|
* 01-11-SUMMARY: no dynamic import in MV3 SW).
|
|
*
|
|
* @param page - The harness page from `launchHarnessBrowser`.
|
|
* @returns Structured AssertionRecord with 4 checks (A8.1..A8.4).
|
|
*/
|
|
export async function driveA8(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.assertA8();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/**
|
|
* Drive A9 (icon file sizes meet `imageUtil` floors). Standard
|
|
* page.evaluate wrapper — the page fetches each icon via
|
|
* chrome.runtime.getURL + reads blob.size and verifies against the
|
|
* 200/500/1024-byte floors per assets-spec.md / Plan 01-13 Task 6.
|
|
*
|
|
* @param page - The harness page from `launchHarnessBrowser`.
|
|
* @returns Structured AssertionRecord with 3 checks (one per icon size).
|
|
*/
|
|
export async function driveA9(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.assertA9();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/**
|
|
* Drive A10 (manifest shape contract). Standard page.evaluate wrapper —
|
|
* the page reads chrome.runtime.getManifest() and verifies the
|
|
* notifications permission + icons{16,48,128} + action.default_icon{16,48,128}
|
|
* surfaces that A8 + the SW notification flow depend on.
|
|
*
|
|
* @param page - The harness page from `launchHarnessBrowser`.
|
|
* @returns Structured AssertionRecord with 7 checks (1 permissions + 3 icons + 3 default_icon).
|
|
*/
|
|
export async function driveA10(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.assertA10();
|
|
return r;
|
|
}) as AssertionRecord;
|
|
}
|
|
|
|
/* ─── Wave 3D — NOT YET IMPLEMENTED ──────────────────────────────── */
|
|
|
|
/**
|
|
* Drive A11 (35s → ≥3 segments). Wave 3D wires.
|
|
* @throws Always — replace stub when Wave 3D lands.
|
|
*/
|
|
export async function driveA11(_page: Page): Promise<AssertionRecord> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3D wires driveA11`);
|
|
}
|
|
|
|
/**
|
|
* Drive A12 (ffprobe — host-side returns webm bytes). Wave 3D wires.
|
|
* @throws Always — replace stub when Wave 3D lands.
|
|
*/
|
|
export async function driveA12(_page: Page): Promise<AssertionWithBytes> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3D wires driveA12`);
|
|
}
|
|
|
|
/**
|
|
* Drive A13 (zip structure + meta.json). Wave 3D wires.
|
|
* @throws Always — replace stub when Wave 3D lands.
|
|
*/
|
|
export async function driveA13(_page: Page): Promise<AssertionWithBytes> {
|
|
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3D wires driveA13`);
|
|
}
|