Run the layer acceptance under a default-deny network policy with per-layer assertion manifests and strict switch contracts

Every scenario runs through scripts/qa-strict-harness.mjs: each page, frame and worker gets an ordered block list before it runs (paid hosts and routes first, then only the preview, declared fixtures, keyless probes and cached free tile hosts), switches with real input on the picker, and records its assertions per layer and engine. The runner reads the catalog from the live page, fails any layer without an assertion on either engine, can build and serve a keyless checkout itself, and adds rapid, hidden-tab, context-loss, failure-injection, cold-chunk and soak scenarios. The main chunk gets a 1% size budget.
This commit is contained in:
German Khokhlov committed 2026-11-03 20:15:50 +04:00
1 parent 3144169057
commit 1773494116
36 files changed
+6003 -1774

No files matched your search

+736
View File
@@ -0,0 +1,736 @@
// The strict acceptance harness shared by every browser scenario that
// qa-all-layer-switches.mjs runs.
//
// - Network: a default-deny request policy on every page, frame and worker
// of the browser, installed before the target runs (CDP auto-attach with
// waitForDebuggerOnStart). Paid hosts and paid preview routes are blocked,
// then only the preview origin, the scenario's declared fixture URLs and
// the free tile hosts (answered from a disk cache) are allowed. A blocked
// request, a fixture or tile URL answered by the real network, or a socket
// to another origin is a violation and fails the scenario with its URL.
// - Coverage: the runner passes the layer ids (--layers=a,b). The scenario
// records each assertion it made per layer id and engine; finish() fails
// when a declared layer has no assertion on either engine, and writes the
// manifest (--evidence=FILE) the runner cross-checks against the catalog.
// - Engine picks: pickEngine() switches with real input on the picker (a
// pointer click on its label, then keyboard type-ahead), never page.select.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import os from 'node:os';
import {
KEYLESS_PROBES,
probePattern,
strictBlockPatterns,
STRICT_TILE_HOSTS,
} from './qa-map-policy.mjs';
import { createMissLoader, tileCacheFile } from './qa/tileCache.mjs';
export const ENGINES = Object.freeze(['cesium', 'navara']);
const RESERVED_PORTS = ['', '4173', '4185', '4186', '4187', '4188', '4189'];
const RESERVED = [...RESERVED_PORTS, '4194', '4196'];
/** The values of --layers=a,b and --evidence=FILE (null when absent). */
export function strictArguments(argv = process.argv) {
const value = (name) =>
argv.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
const layers = value('layers');
return {
layers: layers ? layers.split(',').filter(Boolean) : null,
evidence: value('evidence') ?? process.env.QA_STRICT_EVIDENCE ?? null,
};
}
/** Refuse anything but an unreserved local preview. */
export function localPreview(href) {
const url = new URL(href);
if (
!['localhost', '127.0.0.1'].includes(url.hostname) ||
RESERVED.includes(url.port)
)
throw Error('Supply an unreserved local keyless preview URL');
return url;
}
/** The URL with no remote IP: fulfilled by a fixture or a cache, or a data URL. */
const live = (response) => Boolean(response.remoteIPAddress);
/**
* Follow every request of every target of `browser` under the strict policy.
* Returns the ledger the policy fills in.
*/
export async function installStrictNetwork(
browser,
{ origin, fixtures = [], routes = [], tileHosts = STRICT_TILE_HOSTS, ledger },
) {
const preview = new URL(origin);
fixtures = [...fixtures, ...routes.map((route) => route.pattern)];
const urlPatterns = strictBlockPatterns(origin, { fixtures, tileHosts });
const answers = routes.map((route) => ({
...route,
test: new URLPattern(route.pattern),
}));
const fixturePatterns = fixtures.map((p) => new URLPattern(p));
const isFixture = (url) => fixturePatterns.some((p) => p.test(url));
const root = await browser.target().createCDPSession();
const connection = root.connection();
const probes = KEYLESS_PROBES.map((probe) => ({
...probe,
test: new URLPattern(probePattern(origin, probe)),
}));
const misses = createMissLoader();
const cacheDir = path.join(os.tmpdir(), 'gev-qa-tiles');
fs.mkdirSync(cacheDir, { recursive: true });
const tiles = ledger.tileCache;
// One CDP Fetch handler per target: the keyless probes and the tile hosts
// (from the disk cache, filled on a miss). Nothing else is paused, so
// workers' own requests are never held.
const intercept = async (session, type) => {
session.on('Fetch.requestPaused', async ({ requestId, request }) => {
const fulfil = (status, headers, body) =>
session.send('Fetch.fulfillRequest', {
requestId,
responseCode: status,
responseHeaders: headers,
...(body == null
? {}
: { body: Buffer.from(body).toString('base64') }),
});
try {
const route = answers.find((r) => r.test.test(request.url));
if (route) {
ledger.routes[route.pattern] =
(ledger.routes[route.pattern] ?? 0) + 1;
const answer = await route.respond(request);
return await fulfil(
answer.status ?? 200,
[
...(answer.contentType
? [{ name: 'Content-Type', value: answer.contentType }]
: []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
answer.body ?? '',
);
}
const probe = probes.find((p) => p.test.test(request.url));
if (probe) {
if (request.method !== probe.method && request.method !== 'HEAD') {
violation('probe-method', request.url, { method: request.method });
return await session.send('Fetch.failRequest', {
requestId,
errorReason: 'BlockedByClient',
});
}
ledger.probes[probe.pattern] =
(ledger.probes[probe.pattern] ?? 0) + 1;
return await fulfil(
probe.status,
[
...(probe.contentType
? [{ name: 'Content-Type', value: probe.contentType }]
: []),
{ name: 'Cache-Control', value: 'no-store' },
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
probe.body ?? null,
);
}
const accept = Object.entries(request.headers ?? {}).find(
([name]) => name.toLowerCase() === 'accept',
)?.[1];
const file =
request.method === 'GET'
? tileCacheFile(request.url, accept, cacheDir, tileHosts)
: null;
if (!file) {
violation('unhandled-intercept', request.url, { target: type });
return await session.send('Fetch.failRequest', {
requestId,
errorReason: 'BlockedByClient',
});
}
let entry;
if (fs.existsSync(`${file}.json`)) {
entry = {
...JSON.parse(fs.readFileSync(`${file}.json`, 'utf8')),
body: fs.readFileSync(file),
};
tiles.hits++;
} else {
entry = await misses.load(request.url, accept);
if (entry.status === 200) {
fs.writeFileSync(file, entry.body);
fs.writeFileSync(
`${file}.json`,
JSON.stringify({ status: 200, contentType: entry.contentType }),
);
tiles.fetched++;
} else tiles.failed++;
const host = new URL(request.url).host;
tiles.fetchedByHost[host] = (tiles.fetchedByHost[host] ?? 0) + 1;
}
await fulfil(
entry.status,
[
...(entry.contentType
? [{ name: 'Content-Type', value: entry.contentType }]
: []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
entry.body,
);
} catch (error) {
if (
/Invalid InterceptionId|closed|detached/i.test(String(error?.message))
)
return void tiles.cancelled++;
tiles.failed++;
await session
.send('Fetch.failRequest', { requestId, errorReason: 'Failed' })
.catch(() => {});
}
});
await session
.send('Fetch.enable', {
patterns: [
...probes.map((probe) => ({
urlPattern: probePattern(origin, probe).replace(/\*$/, '') + '*',
})),
...tileHosts.map((host) => ({ urlPattern: `https://${host}/*` })),
...routes.map((route) => ({ urlPattern: route.pattern })),
],
})
.catch((error) =>
ledger.setupErrors.push(
type + ' Fetch.enable: ' + String(error?.message).slice(0, 120),
),
);
};
const violation = (kind, url, detail = {}) => {
ledger.violations.push({ kind, url: String(url).slice(0, 300), ...detail });
};
const setup = async (session, info) => {
const type = info.type;
const urls = new Map();
session.on('Network.requestWillBeSent', (event) => {
const { url } = event.request;
urls.set(event.requestId, url);
if (!/^https?:/.test(url)) return;
ledger.requests++;
const address = new URL(url);
if (address.origin === preview.origin) {
ledger.preview++;
if (address.pathname.startsWith('/api/')) {
const route = address.pathname.split('/').slice(0, 4).join('/');
ledger.previewApi[route] = (ledger.previewApi[route] ?? 0) + 1;
}
} else {
ledger.external[address.host] =
(ledger.external[address.host] ?? 0) + 1;
}
});
session.on('Network.loadingFailed', (event) => {
if (!event.blockedReason) return;
violation('blocked', urls.get(event.requestId) ?? '?', {
reason: event.blockedReason,
target: type,
resource: event.type,
});
});
session.on('Network.responseReceived', ({ response, type: resource }) => {
if (!/^https?:/.test(response.url)) return;
const address = new URL(response.url);
if (probes.some((p) => p.test.test(response.url)) && live(response))
violation('probe-live', response.url, { target: type });
if (address.origin === preview.origin) return;
if (!live(response)) {
if (isFixture(response.url)) ledger.fixtureResponses++;
return;
}
// A fixture or tile URL answered by the real network, or any other
// external host that slipped past the block (a frame navigation).
violation('live', response.url, {
remote: response.remoteIPAddress,
target: type,
resource,
});
});
session.on('Network.webSocketCreated', ({ url }) => {
const address = new URL(url);
if (address.host !== preview.host)
violation('socket', url, { target: type });
});
session.on('Target.attachedToTarget', (event) =>
attach(event.sessionId, event.targetInfo, event.waitingForDebugger),
);
const steps = [
session.send('Network.enable'),
session.send('Network.setBlockedURLs', { urlPatterns }),
session
.send('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
})
.catch(() => {}),
];
// Workers have no Fetch domain; the page's interception answers their
// requests, and a live answer would still show up as a violation.
if (type === 'page' || type === 'iframe')
steps.push(intercept(session, type));
await Promise.all(steps);
};
const attach = async (sessionId, info, waiting) => {
const session = connection.session(sessionId);
if (!session) return;
try {
await setup(session, info);
} catch (error) {
// A target that closes while it attaches has nothing left to load.
if (!/closed|detached|not found/i.test(String(error?.message)))
ledger.setupErrors.push(String(error?.message ?? error).slice(0, 200));
} finally {
if (waiting)
await session.send('Runtime.runIfWaitingForDebugger').catch(() => {});
}
};
root.on('Target.attachedToTarget', (event) =>
attach(event.sessionId, event.targetInfo, event.waitingForDebugger),
);
await root.send('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
filter: [
{ type: 'page' },
{ type: 'iframe' },
{ type: 'worker' },
{ type: 'shared_worker' },
{ type: 'service_worker' },
{ type: 'tab', exclude: true },
{ type: 'browser', exclude: true },
],
});
return ledger;
}
/** A new, empty network ledger. */
export function networkLedger() {
return {
requests: 0,
preview: 0,
previewApi: {},
external: {},
fixtureResponses: 0,
probes: {},
routes: {},
tileCache: {
hits: 0,
fetched: 0,
failed: 0,
cancelled: 0,
fetchedByHost: {},
},
violations: [],
setupErrors: [],
};
}
/**
* Switch to `kind` with real input on the Globe Engine picker: a pointer
* click on its label (which focuses the select without opening its menu)
* and keyboard type-ahead. Opens the control panel with a pointer click if
* it is collapsed and collapses it again once the switch has settled
* (`settle: false` returns as soon as the picker reports the change;
* `afterInput` runs right after the key, before anything is read back).
*/
export async function pickEngine(
page,
kind,
{ settle = true, afterInput = null } = {},
) {
assert.ok(ENGINES.includes(kind), kind);
const expanded = () =>
page.$eval(
'#control-panel-toggle',
(el) => el.getAttribute('aria-expanded') === 'true',
);
const opened = !(await expanded());
if (opened) {
await page.click('#control-panel-toggle').catch(async (error) => {
const state = await page.evaluate(() => {
const el = document.querySelector('#control-panel-toggle');
const box = el?.getBoundingClientRect();
const style = el && getComputedStyle(el);
const hit =
box &&
document.elementFromPoint(
box.x + box.width / 2,
box.y + box.height / 2,
);
return {
box: box && [box.x, box.y, box.width, box.height],
display: style?.display,
visibility: style?.visibility,
hidden: el?.closest('[hidden]')?.id ?? null,
hit: hit?.id || hit?.className || null,
body: document.body.className,
};
});
throw Error(
`cannot click the control panel toggle (${error.message}): ` +
JSON.stringify(state),
);
});
await page.waitForFunction(
() =>
document
.querySelector('#control-panel-toggle')
?.getAttribute('aria-expanded') === 'true',
);
}
await page.waitForSelector('#globe-engine:not([disabled])', {
visible: true,
timeout: 90000,
});
const from = await page.$eval('#globe-engine', (el) => el.value);
assert.notEqual(from, kind, `the picker already shows ${kind}`);
// Where a pointer can reach the picker: the label's own text (focuses the
// select without opening its menu), else the select itself.
const target = await page.$eval('#globe-engine', (el) => {
const box = el.closest('label').getBoundingClientRect();
const select = el.getBoundingClientRect();
// at: the change event (the person's input reached the page);
// idleAt: the picker enabled again after the switch.
window.__qaPick = { busy: false, changed: false, at: null, idleAt: null };
el.addEventListener(
'change',
() => {
window.__qaPick.changed = true;
window.__qaPick.at = performance.now();
},
{ once: true },
);
window.__qaPickObserver?.disconnect();
window.__qaPickObserver = new MutationObserver(() => {
const disabled = document.querySelector('#globe-engine')?.disabled;
if (disabled) window.__qaPick.busy = true;
else if (window.__qaPick.busy && window.__qaPick.idleAt == null)
window.__qaPick.idleAt = performance.now();
});
window.__qaPickObserver.observe(document.body, {
attributes: true,
attributeFilter: ['disabled'],
subtree: true,
});
const describe = (hit) =>
hit
? `${hit.tagName.toLowerCase()}#${hit.id}.${String(hit.className).slice(0, 60)}`
: 'nothing';
const label = {
x: box.left + Math.max(4, (select.left - box.left) / 2),
y: box.top + box.height / 2,
};
const onLabel = document.elementFromPoint(label.x, label.y);
if (
onLabel === el.closest('label') ||
(onLabel?.closest('label') === el.closest('label') && onLabel !== el)
)
return { ...label, via: 'label' };
const middle = {
x: select.left + select.width / 2,
y: select.top + select.height / 2,
};
const onSelect = document.elementFromPoint(middle.x, middle.y);
if (onSelect === el)
return { ...middle, via: 'select', labelCoveredBy: describe(onLabel) };
return { covered: describe(onLabel) + ' / ' + describe(onSelect) };
});
assert.ok(!target.covered, 'the picker is covered by ' + target.covered);
await page.mouse.click(target.x, target.y);
assert.equal(
await page.evaluate(() => document.activeElement?.id),
'globe-engine',
'the click focuses the picker',
);
await page.keyboard.type(kind[0]);
// Whatever the person does next, right after the key (no round trip to
// the page in between).
if (afterInput) await afterInput();
await page.waitForFunction(() => window.__qaPick?.changed, {
timeout: 5000,
polling: 50,
});
// Headless Chrome opens no menu for a click on the select: the key
// events above reached the page, so there is nothing to dismiss.
const how = {
opened,
clicked: target.via,
...(target.labelCoveredBy ? { labelCoveredBy: target.labelCoveredBy } : {}),
};
if (!settle) return how;
await page.waitForFunction(
() =>
(window.__qaPick.busy &&
!document.querySelector('#globe-engine')?.disabled &&
document.querySelector('#loading-screen.hidden') &&
!document.querySelector('.engine-switch-frame')) ||
document.querySelector('.engine-switch-retry'),
{ timeout: 120000, polling: 50 },
);
await page.evaluate(() => window.__qaPickObserver?.disconnect());
Object.assign(
how,
await page.evaluate(() => ({
at: window.__qaPick.at,
idleAt: window.__qaPick.idleAt,
})),
);
if (opened && (await expanded().catch(() => false))) {
await page.click('#control-panel-toggle');
await page.waitForFunction(
() =>
document
.querySelector('#control-panel-toggle')
?.getAttribute('aria-expanded') !== 'true',
);
}
return how;
}
/** Wait until `kind` runs, its picker is idle and its view is restored. */
export async function waitForEngine(page, kind, { timeout = 120000 } = {}) {
await page.waitForFunction(
(expected) =>
window.__godsEyeView?.engine?.kind === expected &&
document.querySelector('#globe-engine')?.value === expected &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout },
kind,
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
}
/**
* The strict context of one scenario run.
*
* @param {object} options
* @param {string} options.origin The preview origin.
* @param {string[]} options.layers The layer ids this scenario covers (the
* runner's --layers wins when given; both must agree).
* @param {string[]} [options.fixtures] URLPattern strings for external
* requests the scenario answers itself.
* @param {{pattern: string, respond: (request: object) => ({status?: number,
* contentType?: string, body?: string|Buffer}|Promise<object>)}[]} [options.routes]
* External URLs the harness answers for the scenario (the pattern is both
* a URLPattern and a CDP Fetch glob, e.g. `https://i.ytimg.com/*`).
* @param {readonly string[]} [options.tileHosts] Hosts the disk cache
* answers (default STRICT_TILE_HOSTS).
* @param {string} [options.name] Scenario name for the manifest.
*/
export function createStrict({
origin,
layers,
fixtures = [],
routes = [],
tileHosts = STRICT_TILE_HOSTS,
name = path.basename(process.argv[1] ?? 'scenario', '.mjs'),
}) {
const args = strictArguments();
if (args.layers)
assert.deepEqual(
[...args.layers].sort(),
[...layers].sort(),
`${name} covers ${layers.join(',')}, the runner passed ${args.layers.join(',')}`,
);
const declared = args.layers ?? layers;
const ledger = networkLedger();
const assertions = [];
const picks = [];
const timings = [];
const extra = {};
let finished = false;
const record = (ids, engine, claim, detail) => {
for (const id of [ids].flat()) {
assert.ok(declared.includes(id), `${id} is not declared by ${name}`);
assert.ok(ENGINES.includes(engine), `unknown engine ${engine}`);
assertions.push({
layer: id,
engine,
claim,
...(detail === undefined ? {} : { detail }),
});
}
};
const engineOf = (page) =>
page.evaluate(() => window.__godsEyeView?.engine?.kind ?? null);
const strict = {
name,
layers: declared,
ledger,
extra,
timings,
/** Launch Chrome with the policy on every target. */
async launch(options = {}) {
const browser = await puppeteer.launch({ headless: true, ...options });
await installStrictNetwork(browser, {
origin,
fixtures,
routes,
tileHosts,
ledger,
});
return browser;
},
/** Launch, run `body(browser)`, close, then finish(). */
async run(options, body) {
const browser = await strict.launch(options);
let failed = null;
try {
await body(browser);
} catch (error) {
failed = error;
} finally {
await browser.close().catch(() => {});
}
await strict.finish({ failed });
if (failed) throw failed;
},
/** Record an assertion that already passed. */
record,
/** Record a passed assertion on the engine the page runs now. */
async passed(page, ids, claim, detail) {
const engine = await engineOf(page);
record(ids, engine, claim, detail);
},
/** Assert `value` and record it on the engine the page runs now. */
async check(page, ids, claim, value, detail) {
assert.ok(
value,
claim + (detail === undefined ? '' : ' ' + JSON.stringify(detail)),
);
await strict.passed(page, ids, claim, detail);
},
/** Switch with real input and log the pick. */
async pick(page, kind, options) {
const result = await pickEngine(page, kind, options);
picks.push({ to: kind, via: 'pointer+keyboard', ...result });
return result;
},
/**
* Switch through the select element without input, only where no
* person can reach the picker (`reason` says why); logged as such.
*/
async selectEngine(page, kind, reason) {
assert.ok(reason, 'say why the picker cannot be used');
await page.select('#globe-engine', kind);
picks.push({ to: kind, via: 'page.select', reason });
},
/**
* Switch with real input in the same document and wait until `kind`
* runs (the picker counterpart of qa-switch-globe.mjs).
*/
async switchTo(page, kind) {
const origin = await page.evaluate(() => performance.timeOrigin);
await strict.pick(page, kind);
await waitForEngine(page, kind);
assert.equal(
await page.evaluate(() => performance.timeOrigin),
origin,
'same document after switching globes',
);
},
/** Write the manifest; fail on violations or missing coverage. */
async finish({ failed = null } = {}) {
if (finished) return;
finished = true;
const coverage = Object.fromEntries(
declared.map((id) => [
id,
Object.fromEntries(
ENGINES.map((engine) => [
engine,
assertions.filter((a) => a.layer === id && a.engine === engine)
.length,
]),
),
]),
);
const missing = Object.entries(coverage).flatMap(([id, byEngine]) =>
ENGINES.filter((engine) => !byEngine[engine]).map(
(engine) => `${id} on ${engine}`,
),
);
const network = {
requests: ledger.requests,
preview: ledger.preview,
previewApi: ledger.previewApi,
external: ledger.external,
fixtureResponses: ledger.fixtureResponses,
probes: ledger.probes,
routes: ledger.routes,
tileCache: ledger.tileCache,
violations: ledger.violations,
setupErrors: ledger.setupErrors,
fixtures,
tileHosts: [...tileHosts],
};
const manifest = {
scenario: name,
argv: process.argv.slice(2),
status: failed ? 'fail' : 'pass',
error: failed ? String(failed?.stack ?? failed).slice(0, 2000) : null,
layers: declared,
coverage,
missing,
picks,
assertions,
network,
timings,
...extra,
};
if (args.evidence) {
fs.mkdirSync(path.dirname(args.evidence), { recursive: true });
fs.writeFileSync(
args.evidence,
JSON.stringify(manifest, null, 2) + '\n',
);
}
console.log(
'STRICT',
JSON.stringify({
scenario: name,
assertions: assertions.length,
coverage,
picks: picks.length,
requests: ledger.requests,
violations: ledger.violations.length,
}),
);
for (const v of ledger.violations.slice(0, 20))
console.log('NETWORK VIOLATION', JSON.stringify(v));
if (failed) return;
assert.equal(
ledger.violations.length,
0,
'network policy violations: ' +
JSON.stringify(ledger.violations.slice(0, 5)),
);
assert.deepEqual(missing, [], 'layers without an assertion');
assert.ok(
picks.some((pick) => pick.via === 'pointer+keyboard'),
'switch at least once with real input on the picker',
);
},
};
return strict;
}