Files
gods-eye-view/scripts/qa-layer-token-twochar.mjs

338 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* End-to-end share/reload proof for the first two-character token. The QA
* layer and an exhausted-digit reservation scenario are injected into
* dev-server responses in this isolated browser only. Neither belongs in a
* production bundle or ledger.
*
* Run against a local Vite server: node scripts/qa-layer-token-twochar.mjs
*/
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import puppeteer from 'puppeteer';
import {
LAYER_STATE_TOKEN_RESERVATIONS,
nextLayerStateToken,
parseLayerStateTokenReservations,
validateLayerStateAllocations,
} from '../src/data/layerState.js';
const base = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173');
if (!['127.0.0.1', 'localhost'].includes(base.hostname))
throw new Error('The QA fixture requires a loopback-only dev server');
const origin = base.origin;
const fixtureId = 'qa-twochar-fixture';
const injections = { codec: 0, catalog: 0 };
const errors = [];
const reservations = JSON.parse(
await readFile(
new URL('../src/data/layerStateTokenReservations.json', import.meta.url),
'utf8',
),
);
assert.equal(
reservations.some(([, token]) => token === '00'),
false,
'00 must remain unallocated in the production ledger for this QA fixture',
);
const productionTokens = new Set(reservations.map(([, token]) => token));
const priorDigitRows = [...'0123456789']
.filter((digit) => !productionTokens.has(digit))
.map((digit) => [`qa-prior-digit-${digit}`, digit]);
const priorState = parseLayerStateTokenReservations([
...reservations,
...priorDigitRows,
]);
assert.equal(nextLayerStateToken(priorState), '00');
assert.equal(
validateLayerStateAllocations(
LAYER_STATE_TOKEN_RESERVATIONS,
parseLayerStateTokenReservations([
...reservations,
...priorDigitRows,
[fixtureId, '00'],
]),
),
true,
);
console.log(
'PASS: isolated fixture exhausts prior digits before allocating 00',
);
for (const source of [
'../src/data/layerState.js',
'../src/app/constructCatalog.js',
]) {
assert.equal(
(await readFile(new URL(source, import.meta.url), 'utf8')).includes(
fixtureId,
),
false,
`${fixtureId} must not exist in production source`,
);
}
function replaceOnce(source, from, to, label) {
const first = source.indexOf(from);
if (first < 0 || source.indexOf(from, first + from.length) >= 0)
throw new Error(`QA fixture anchor changed: ${label}`);
return source.replace(from, to);
}
function injectCodec(source) {
const fixtureRows = JSON.stringify([...priorDigitRows, [fixtureId, '00']]);
let result = replaceOnce(
source,
'parseLayerStateTokenReservations(reservationRows)',
`parseLayerStateTokenReservations([...reservationRows, ...${fixtureRows}])`,
'reservation',
);
result = replaceOnce(
result,
']);\n\nexport const REGISTERED_LAYER_IDS',
` Object.freeze({ id: '${fixtureId}', token: '00', disposition: 'enabled-only' }),\n]);\n\nexport const REGISTERED_LAYER_IDS`,
'registry',
);
injections.codec += 1;
return result;
}
function injectCatalog(source) {
const result = replaceOnce(
source,
' createBhoteKoshiEventLayer(),',
` {
id: '${fixtureId}',
name: 'Synthetic two-character token',
icon: '◆',
source: 'QA-only fixture',
async init() { return true; },
async enable() { return true; },
async update() { return true; },
async disable() { return true; },
async destroy() { return true; },
getStats() { return { count: 0 }; },
},
createBhoteKoshiEventLayer(),`,
'catalog',
);
injections.catalog += 1;
return result;
}
const browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-dev-shm-usage',
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: ['--use-gl=angle', '--use-angle=swiftshader']),
],
});
async function newPage() {
const context = await browser.createBrowserContext();
const page = await context.newPage();
page.on('pageerror', (error) => errors.push(error.message));
await page.setViewport({ width: 1280, height: 800 });
await page.setCacheEnabled(false);
await page.evaluateOnNewDocument(() => {
window.__qaRestoreEvents = [];
window.addEventListener('gev:initial-share-restore-settled', (event) => {
window.__qaRestoreEvents.push(event.detail);
});
});
await page.setRequestInterception(true);
page.on('request', async (request) => {
try {
const url = new URL(request.url());
if (url.origin !== origin) return await request.abort();
const transform =
url.pathname === '/src/data/layerState.js'
? injectCodec
: url.pathname === '/src/app/constructCatalog.js'
? injectCatalog
: null;
if (!transform) return await request.continue();
const response = await fetch(request.url());
assert.equal(response.status, 200, `dev module ${url.pathname}`);
await request.respond({
status: 200,
contentType: 'application/javascript',
body: transform(await response.text()),
});
} catch (error) {
errors.push(String(error?.stack || error));
await request.abort().catch(() => {});
}
});
return { context, page };
}
async function load(page, url) {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForFunction(
(id) => window.__godsEyeView?.dataManager?.layers?.has(id),
{ timeout: 60_000 },
fixtureId,
);
}
async function loadedState(page) {
return page.evaluate((id) => {
const manager = window.__godsEyeView.dataManager;
return {
enabled: manager.isEnabled(id),
flights: manager.isEnabled('flights'),
localStorage: localStorage.getItem('gev:layer-state:v2'),
restoreEvents: window.__qaRestoreEvents,
hash: location.hash,
};
}, fixtureId);
}
async function waitForRestore(page) {
await page.waitForFunction(() => window.__qaRestoreEvents.length > 0, {
timeout: 60_000,
});
return loadedState(page);
}
function withLayers(link, layers) {
const url = new URL(link);
const params = new URLSearchParams(url.hash.slice(1));
params.set('l', layers);
url.hash = params.toString();
return url.href;
}
const contexts = [];
try {
const sender = await newPage();
contexts.push(sender.context);
await load(sender.page, `${origin}/?welcome=0`);
const toggled = await sender.page.evaluate(
(id) =>
window.__godsEyeView.dataManager.setEnabled(id, true, { origin: 'user' }),
fixtureId,
);
assert.equal(toggled, true, 'synthetic layer enables through manager');
await sender.page.waitForFunction(
() => new URLSearchParams(location.hash.slice(1)).get('l') === '00',
{ timeout: 15_000 },
);
const sharedUrl = sender.page.url();
const authored = new URLSearchParams(new URL(sharedUrl).hash.slice(1));
assert.equal(authored.get('v'), '2');
assert.equal(authored.get('l'), '00');
assert.ok(authored.has('lat') && authored.has('lon'));
console.log('PASS: real user-origin enable encoded 00 in a full share URL');
const recipient = await newPage();
contexts.push(recipient.context);
await load(recipient.page, sharedUrl);
const restored = await waitForRestore(recipient.page);
assert.equal(restored.enabled, true);
assert.equal(restored.localStorage, null);
assert.equal(restored.restoreEvents.at(-1)?.status, 'settled');
assert.ok(
restored.restoreEvents
.at(-1)
?.layers?.some(
(layer) =>
layer.layerId === fixtureId &&
layer.targetEnabled === true &&
layer.succeeded === true,
),
'share coordinator restored the synthetic layer',
);
console.log(
'PASS: cold recipient reload restored the 00 layer without writing local preferences',
);
await recipient.page.reload({
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await recipient.page.waitForFunction(
(id) => window.__godsEyeView?.dataManager?.layers?.has(id),
{ timeout: 60_000 },
fixtureId,
);
const reloaded = await waitForRestore(recipient.page);
assert.equal(reloaded.enabled, true);
assert.equal(reloaded.localStorage, null);
assert.equal(new URLSearchParams(reloaded.hash.slice(1)).get('l'), '00');
console.log('PASS: a full recipient reload restores the same 00 layer state');
const disabled = await recipient.page.evaluate(
(id) =>
window.__godsEyeView.dataManager.setEnabled(id, false, {
origin: 'user',
}),
fixtureId,
);
assert.equal(disabled, true);
await recipient.page.waitForFunction(
() => new URLSearchParams(location.hash.slice(1)).get('l') === '',
{ timeout: 15_000 },
);
assert.equal((await loadedState(recipient.page)).enabled, false);
console.log('PASS: explicit OFF removes 00 from the authored share URL');
const legacy = await newPage();
contexts.push(legacy.context);
await load(legacy.page, withLayers(sharedUrl, 'f'));
const legacyState = await waitForRestore(legacy.page);
assert.equal(legacyState.flights, true);
assert.equal(legacyState.enabled, false);
assert.equal(legacyState.restoreEvents.at(-1)?.status, 'settled');
console.log('PASS: a legacy one-character token still restores');
const mixed = await newPage();
contexts.push(mixed.context);
await load(mixed.page, withLayers(sharedUrl, 'f.00'));
const mixedState = await waitForRestore(mixed.page);
assert.equal(mixedState.flights, true);
assert.equal(mixedState.enabled, true);
console.log('PASS: mixed legacy and two-character tokens restore together');
const malformed = await newPage();
contexts.push(malformed.context);
await load(malformed.page, withLayers(sharedUrl, 'f..00'));
const malformedState = await waitForRestore(malformed.page);
assert.equal(malformedState.flights, false);
assert.equal(malformedState.enabled, false);
assert.equal(malformedState.localStorage, null);
console.log(
'PASS: malformed layer list is rejected without partial restoration',
);
const repeated = await newPage();
contexts.push(repeated.context);
const repeatedUrl = new URL(withLayers(sharedUrl, 'f'));
const repeatedParams = new URLSearchParams(repeatedUrl.hash.slice(1));
repeatedParams.append('l', '00');
repeatedUrl.hash = repeatedParams.toString();
await load(repeated.page, repeatedUrl.href);
const repeatedState = await waitForRestore(repeated.page);
assert.equal(repeatedState.flights, false);
assert.equal(repeatedState.enabled, false);
assert.equal(repeatedState.localStorage, null);
console.log('PASS: repeated layer fields reject the entire share payload');
assert.ok(injections.codec >= contexts.length);
assert.ok(injections.catalog >= contexts.length);
assert.deepEqual(errors, []);
console.log(
'PASS: QA-only codec and catalog injections reached every isolated session',
);
console.log(
'Synthetic end-to-end evidence only; production 00 allocation remains pending.',
);
console.log('RESULT: 10 passed, 0 failed, 0 skipped');
} finally {
await Promise.all(contexts.map((context) => context.close().catch(() => {})));
await browser.close();
}