diff --git a/tests/uat/harness.test.ts b/tests/uat/harness.test.ts index c020ed8..fa7274e 100644 --- a/tests/uat/harness.test.ts +++ b/tests/uat/harness.test.ts @@ -103,6 +103,8 @@ import { driveA30, // Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02) driveA31, + // Plan 03-04 — RAM scaffolding best-effort (SPEC §10 #9 per D-P3-04) + driveA32, getManifestVersion, } from './lib/harness-page-driver'; import { @@ -271,7 +273,7 @@ async function assertA0_GrepGate(): Promise<{ */ async function main(): Promise { process.stdout.write('\nMokosh Plan 01-13 + 01-14 + 02-04 — UAT harness orchestrator\n'); - process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30, A31)\n'); + process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30, A31, A32)\n'); process.stdout.write('='.repeat(72) + '\n'); // A0 pre-flight (no Chrome launch needed; runs against built dist/). @@ -475,6 +477,13 @@ async function main(): Promise { // mean the filter actually fired rather than the trivial "no // events at all" tautology). { name: 'A31', drive: driveA31Wrapped }, + // Plan 03-04 A32: RAM scaffolding (SPEC §10 #9 best-effort per D-P3-04). + // NOTE — Page.metrics is page-realm only; SW context is a separate + // Puppeteer target (RESEARCH Pitfall 2). A32 is informational + // scaffolding; the binding §10 #9 gate lives in Plan 03-05 + // VERIFICATION.md `human_verification` block. No wrapped const + // needed — driveA32 takes only `page`. + { name: 'A32', drive: driveA32 }, ]; const buffers = { swConsole: handles.swConsole, offConsole: handles.offConsole }; diff --git a/tests/uat/lib/harness-page-driver.ts b/tests/uat/lib/harness-page-driver.ts index 223c1eb..2f208bf 100644 --- a/tests/uat/lib/harness-page-driver.ts +++ b/tests/uat/lib/harness-page-driver.ts @@ -2314,3 +2314,93 @@ export async function driveA31( error: pageResult.error, }; } + +/* ─── Plan 03-04 — driveA32 (RAM scaffolding best-effort) ──────────── */ + +/** RAM ceiling per SPEC §10 #9 + CON-ram-ceiling. */ +const A32_RAM_CEILING_BYTES = 50 * 1024 * 1024; +/** Bytes-per-MB factor for diagnostic copy. */ +const A32_BYTES_PER_MB = 1024 * 1024; + +/** + * Drive A32 (Plan 03-04 — SPEC §10 #9 RAM best-effort per D-P3-04). + * + * Reads puppeteer.Page.metrics() against the harness page and asserts + * JSHeapUsedSize is below the 50 MB ceiling. This is informational + * scaffolding ONLY: + * + * - RESEARCH Pitfall 2: Page.metrics is page-realm only. The MV3 + * service worker is a separate Puppeteer target with its own V8 + * isolate; page.metrics() does not aggregate across workers/iframes. + * - The page-realm value reported here is NOT the operator-facing + * "extension background RAM" measurement that SPEC §10 #9 requires. + * - The binding §10 #9 gate lives in Plan 03-05 VERIFICATION.md + * `human_verification` block (operator runs chrome://memory-internals + * OR chrome://extensions service-worker memory display). + * + * Why ship this anyway (per RESEARCH Open Question 3): + * - Low cost (~30 lines; single API call; no new bundle surface). + * - Exercises the Page.metrics API end-to-end so Phase 4 (programmatic + * RAM measurement upgrade) inherits a working scaffold. + * - Provides a sanity floor — if the harness page-realm heap ever + * blows past 50 MB, something has gone catastrophically wrong in + * the test infrastructure itself (not necessarily a §10 #9 regression + * in production). + * + * The diagnostic line about page-realm scope MUST be emitted regardless + * of pass/fail per Pitfall 2. + * + * @param page - The harness page from `launchHarnessBrowser`. + * @returns AssertionRecord with 2 checks (heap returned + heap < 50 MB) + * + explicit page-realm-only diagnostic. + */ +export async function driveA32(page: Page): Promise { + const checks: CheckRecord[] = []; + const diagnostics: string[] = []; + + // Pitfall 2 gate: emit the page-realm caveat BEFORE any other diagnostic + // so it leads in the structured output (the operator sees it first). + diagnostics.push( + 'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.', + ); + + let metricsErr: string | null = null; + let jsHeapBytes = -1; + let jsHeapTotal = -1; + try { + const metrics = await page.metrics(); + jsHeapBytes = metrics.JSHeapUsedSize ?? -1; + jsHeapTotal = metrics.JSHeapTotalSize ?? -1; + } catch (err) { + metricsErr = err instanceof Error ? err.message : String(err); + } + + const jsHeapMB = jsHeapBytes >= 0 ? jsHeapBytes / A32_BYTES_PER_MB : -1; + diagnostics.push(`A32 JSHeapUsedSize=${jsHeapBytes} bytes (${jsHeapMB.toFixed(2)} MB)`); + diagnostics.push(`A32 JSHeapTotalSize=${jsHeapTotal} bytes`); + if (metricsErr !== null) { + diagnostics.push(`A32 Page.metrics threw: ${metricsErr}`); + } + + checks.push({ + name: 'A32.1: Page.metrics returned a JSHeapUsedSize value >= 0', + expected: '>= 0', + actual: jsHeapBytes, + passed: jsHeapBytes >= 0, + }); + checks.push({ + name: `A32.2: Page-realm JS heap < ${A32_RAM_CEILING_BYTES / A32_BYTES_PER_MB} MB (NOTE: scaffolding only; SW context excluded per D-P3-04)`, + expected: `< ${A32_RAM_CEILING_BYTES / A32_BYTES_PER_MB} MB`, + actual: jsHeapMB >= 0 ? `${jsHeapMB.toFixed(2)} MB` : 'unavailable', + passed: jsHeapBytes >= 0 && jsHeapBytes < A32_RAM_CEILING_BYTES, + }); + + const passed = checks.every((c) => c.passed); + return { + passed, + name: 'A32 — RAM scaffolding (best-effort; page-realm only per D-P3-04 / SPEC §10 #9)', + checks, + diagnostics, + error: metricsErr ?? undefined, + }; +}