Exercise every catalog layer on both planets and verify live switch continuity

This commit is contained in:
German Khokhlov committed 2026-11-03 05:22:02 +04:00
1 parent 05b646daa3
commit 62296a833c
6 files changed
+224 -15

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
import { spawnSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import assert from 'node:assert/strict';
import { APPLICATION_LAYER_DIRECTORY } from '../src/app/layers/directory.js';
const base = new URL(process.argv[2]);
if (
!['127.0.0.1', 'localhost'].includes(base.hostname) ||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
base.port,
)
)
throw Error('Supply an unreserved keyless local preview URL');
const output = process.argv[3]
? resolve(process.argv[3])
: mkdtempSync(join(tmpdir(), 'gev-layer-switch-'));
mkdirSync(output, { recursive: true });
const runs = [
['engine-switch', ['earthquakes']],
['director-engine-switch', []],
['layer-handoff', ['flights']],
['layer-handoff', ['military'], '--military'],
[
'ground-layer-switch',
['ais-live-vessels', 'air-quality', 'gnss-integrity', 'fire-perimeters'],
],
['contact-switch', ['military-awareness', 'military-installations']],
['local-adsb-engine-switch', ['local-adsb']],
['ghostmaps-switch', ['s2-ghostmaps']],
[
'weather-engine-switch',
[
'weather-radar',
'weather-satellite',
'weather-lightning',
'weather-cyclones',
],
],
['wind-engine-switch', ['wind']],
['infrastructure-engine-switch', ['local-dams', 'local-datacenters']],
['submarine-cables-engine-switch', ['telegeography-submarine-cables']],
['directions-engine-switch', ['directions']],
['nepal-locator-switch', ['bhote-koshi-locator']],
['nepal-event-switch', ['bhote-koshi-2026']],
['traffic-engine-switch', ['traffic']],
['traffic-engine-switch', ['traffic'], '--simulation'],
['street-level-engine-switch', ['street-level']],
['street-level-engine-switch', ['street-level'], '--panoramax'],
['radio-engine-switch', ['radio']],
['w2-imagery', ['recent-imagery']],
['fire-camera-switch', ['alpr-cameras']],
['fire-camera-switch', ['local-firms'], '--fires'],
['urban-engine-switch', ['bikeshare']],
['urban-engine-switch', ['transit'], '--transit'],
['cctv-engine-switch', ['cctv']],
['space-engine-switch', ['satellites']],
['space-engine-switch', ['rocket-launches'], '--missions'],
];
const ids = [...new Set(runs.flatMap(([, ids]) => ids))].sort();
assert.deepEqual(
ids,
APPLICATION_LAYER_DIRECTORY.map((l) => l.id).sort(),
'Every catalog layer must have a browser scenario',
);
const report = [];
for (const [name, layers, flag] of runs) {
const label = name + (flag ?? '');
const args =
name === 'w2-imagery'
? [
'--url',
base.origin + '/?engine=navara',
'--report',
join(output, 'imagery.json'),
'--switch',
'--preview',
]
: [base.origin, ...(flag ? [flag] : [])];
const start = Date.now();
const run = spawnSync(
process.execPath,
[fileURLToPath(new URL('./qa-' + name + '.mjs', import.meta.url)), ...args],
{
encoding: 'utf8',
timeout: 240000,
maxBuffer: 8388608,
env: { ...process.env, GOOGLE_MAPS_API_KEY: '', CESIUM_ION_TOKEN: '' },
},
);
const ok = run.status === 0;
writeFileSync(
join(output, label + '.log'),
(run.stdout || '') + (run.stderr || '') + (run.error?.message || ''),
);
report.push({
label,
layers,
ok,
seconds: Math.round((Date.now() - start) / 1000),
});
writeFileSync(
join(output, 'summary.json'),
JSON.stringify({ ids, report }, null, 2) + '\n',
);
console.log(ok ? 'PASS' : 'FAIL', label, report.at(-1).seconds);
}
console.log(
ids.length +
' layers, ' +
report.filter((row) => row.ok).length +
'/' +
runs.length +
' browser scenarios. ' +
output,
);
if (report.some((row) => !row.ok)) process.exitCode = 1;
+29 -1
View File
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
import { blockPaidQaMaps } from './qa-map-policy.mjs';
import { installTileCache } from './qa/tileCache.mjs';
@@ -29,7 +30,14 @@ try {
window.fetch = (url, options) =>
String(url).startsWith('https://earthquake.usgs.gov/earthquakes/feed/')
? Promise.resolve(
Response.json({ type: 'FeatureCollection', features: [] }),
Response.json({
type: 'FeatureCollection',
features: [{
type: 'Feature', id: 'qa-boston',
geometry: { type: 'Point', coordinates: [-71.0605, 42.3554, 8] },
properties: { mag: 4, time: Date.now(), place: 'QA Boston' },
}],
}),
)
: fetch(url, options);
});
@@ -53,6 +61,24 @@ try {
});
assert.equal(await page.$eval('#globe-engine', (el) => el.value), kind);
}
async function verifyQuakeSurface(kind) {
const meanRed = async (enabled) => {
await page.evaluate(async (enabled) => {
const a = window.__godsEyeView;
await a.dataManager.setEnabled('earthquakes', enabled, { origin: 'user' });
a.engine.render.request('qa-quake-surface');
}, enabled);
if (enabled) await page.waitForFunction(() =>
window.__godsEyeView.dataManager.layers.get('earthquakes').module.getAnalystRecords().length === 1);
await new Promise(resolve => setTimeout(resolve, 1800));
const bytes = await page.screenshot({ clip: { x: 675, y: 403, width: 20, height: 20 } });
const { channels } = await sharp(bytes).stats();
return channels[0].mean - channels[1].mean;
};
const off = await meanRed(false), on = await meanRed(true);
assert.ok(on > off + 8, kind + ' must draw the shallow red earthquake footprint: ' + off + ' -> ' + on);
console.log('PASS', kind, 'earthquake footprint red minus green:', off, '->', on);
}
const stored = () => page.evaluate(() => localStorage.getItem('gev.engine'));
async function switchTo(kind) {
if (
@@ -151,6 +177,7 @@ try {
}
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
await ready('cesium');
await verifyQuakeSurface('cesium');
const sharpen = () =>
page.evaluate(
() => window.__godsEyeView.styleManager.getControlState().sharpen,
@@ -162,6 +189,7 @@ try {
});
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 49 });
await switchTo('navara');
await verifyQuakeSurface('navara');
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 80 });
assert.ok(
await page.evaluate(
+7 -2
View File
@@ -5,6 +5,11 @@ import { blockPaidQaMaps } from './qa-map-policy.mjs';
import { switchGlobe } from './qa-switch-globe.mjs';
const url = new URL(process.argv[2]);
const soak = process.argv.includes('--soak');
const requested = process.argv.find((arg) => arg.startsWith('--swaps='));
const swaps = requested ? Number(requested.slice(8)) : soak ? 50 : 2;
if (requested && (!soak || !Number.isInteger(swaps) || swaps % 2 || swaps < 4 || swaps > 1000))
throw Error('Use --soak --swaps=N with an even count from 4 to 1000');
if (
!['localhost', '127.0.0.1'].includes(url.hostname) ||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
@@ -78,7 +83,7 @@ try {
window.qaModels = new Map([...dataManager.layers].map(([id, entry]) => [id, entry.module]));
window.qaEngines = new Map([[engine.kind, engine]]);
});
const swaps = soak ? 50 : 2, measurements = [], memory = [];
const measurements = [], memory = [];
let contextListeners;
for (let i = 1; i <= swaps; i++) {
const kind = i % 2 ? 'cesium' : 'navara';
@@ -96,7 +101,7 @@ try {
window.qaModels.size === 33 && window.qaQuakeReads >= 1 &&
dataManager.getEnabledLayerIds().has('earthquakes');
}));
if ([2, 10, 50].includes(i)) {
if ([2, 10, 50, 100, 250, swaps].includes(i)) {
const { result } = await client.send('Runtime.evaluate', {
includeCommandLineAPI: true,
returnByValue: true,
+6 -1
View File
@@ -2,6 +2,7 @@ import {switchGlobe} from './qa-switch-globe.mjs';
import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
import { installTileCache } from './qa/tileCache.mjs';
import { blockPaidQaMaps } from './qa-map-policy.mjs';
import { isExpectedPreviewSetupError, redact } from './qa-browserEvidence.mjs';
@@ -51,6 +52,7 @@ try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await blockPaidQaMaps(page);
await installTileCache(page);
page.on('pageerror', (e) => report.errors.push(redact(e.message)));
page.on('console', (message) => {
if (message.type() !== 'error') return;
@@ -64,7 +66,10 @@ try {
((/\b503\b/.test(text) &&
String(message.location()?.url).includes('HLS_L30')) ||
(failLandsat && /Failed to obtain image tile/.test(text))));
(expected ? report.expectedErrors : report.errors).push(text);
const source = message.location()?.url;
(expected ? report.expectedErrors : report.errors).push(
source ? text + ' (' + new URL(source, url).hostname + ')' : text,
);
});
const session = await page.createCDPSession();
await session.send('Fetch.enable', {
+37 -11
View File
@@ -266,14 +266,41 @@ try {
await manager.setEnabled('weather-cyclones', true, {origin:'user'});
window.qaStorm = manager.layers.get('weather-cyclones').module;
},ids);
const click = async (lon,lat) => {
const at = await p.evaluate(position => {
const {engine} = window.__godsEyeView;
const xy = engine.projector.worldToWindow(position,{});
const rect = engine.host.canvas.getBoundingClientRect();
return [rect.left+xy.x,rect.top+xy.y];
},fromDegrees(lon,lat));
await p.mouse.click(...at);
const click = async (lon, lat, insideCone) => {
const hit = await p.waitForFunction(
({ lon, lat, base, insideCone }) => {
const { engine } = window.__godsEyeView;
const longitude = (lon * Math.PI) / 180,
latitude = (lat * Math.PI) / 180;
const height =
engine.surface.terrainHeight({ longitude, latitude, height: 0 }) ?? 0;
const xy = engine.projector.worldToWindow(
{
x: base.x + height * Math.cos(latitude) * Math.cos(longitude),
y: base.y + height * Math.cos(latitude) * Math.sin(longitude),
z: base.z + height * Math.sin(latitude),
},
{},
);
if (!xy) return false;
const canvas = engine.host.canvas,
rect = canvas.getBoundingClientRect();
const x = rect.left + xy.x,
y = rect.top + xy.y;
if (document.elementFromPoint(x, y) !== canvas) return false;
const id = engine.picker.pick(xy)?.id;
if (
String(id?.id ?? id).startsWith('cyclone:al012026:cone:') !==
insideCone
)
return false;
return [x, y];
},
{ timeout: 30000 },
{ lon, lat, base: fromDegrees(lon, lat), insideCone },
);
await p.mouse.click(...(await hit.jsonValue()));
await hit.dispose();
};
const before = cycloneReads;
for (const kind of ['navara','cesium','navara']) {
@@ -285,10 +312,9 @@ try {
window.qaStorm.getDiagnostics().selectedId === 'al012026');
const d = await p.evaluate(()=>window.qaStorm.getDiagnostics());
assert.deepEqual([d.storms,d.tracks,d.cones,d.forecastPoints],[1,1,1,1],kind);
await new Promise(r => setTimeout(r,1200));
await click(-79.43,43.59);
await click(-79.43,43.59,false);
await p.waitForFunction(()=>window.qaStorm.getDiagnostics().selectedId === null);
await click(-79.30,43.643);
await click(-79.30,43.643,true);
await p.waitForFunction(()=>window.qaStorm.getDiagnostics().selectedId === 'al012026');
assert.equal(cycloneReads,before);
console.log('PASS',kind,'pick advisory cone and retain selected storm');
+26
View File
@@ -245,6 +245,32 @@ test('the catalog per engine: Cesium as before, Navara only what it can show', a
assert.equal(subset.get('military'), undefined);
});
test('both production registries attach to the same 33 session models', async (t) => {
const { registerNavaraLayerRenderers } = await import('./navaraMode.js');
const lifetime = new AbortController();
t.after(() => lifetime.abort());
const options = {
layerSession: { lifetime },
sources: createStandaloneLayerSources(),
signal: lifetime.signal,
surface: fixtureSurface(lifetime.signal),
};
const cesium = createApplicationCatalog(options);
await registerNavaraLayerRenderers();
const navara = createApplicationCatalog({ ...options, engineKind: 'navara' });
assert.equal(cesium.layers.length, 33);
assert.deepEqual(
navara.layers.map(({ id }) => id),
cesium.layers.map(({ id }) => id),
);
assert.deepEqual(navara.metadata, cesium.metadata);
for (const layer of cesium.layers) {
assert.equal(navara.get(layer.id), layer, layer.id);
assert.equal(typeof layer.attachRenderer, 'function', layer.id);
assert.equal(typeof layer.detachRenderer, 'function', layer.id);
}
});
test('a GNSS integrity source can set the layer evidence window', (t) => {
const lifetime = new AbortController();
t.after(() => lifetime.abort());