Merge branch 'german/refactor-e8-cold-start' into german/navara-refactor

This commit is contained in:
German Khokhlov committed 2026-11-10 17:47:30 +04:00
commit 417890b862
12 files changed
+2020

No files matched your search

+119
View File
@@ -286,6 +286,125 @@ const soakRuns = quick
const longRun = quick ? '--swaps=100' : '--swaps=500';
// label, script, layers, extra arguments, timeout in seconds.
const runs = [
[
'switch-equals-cold-start--aircraft',
'switch-equals-cold-start',
['flights', 'military'],
['--group=aircraft'],
],
[
'switch-equals-cold-start--local',
'switch-equals-cold-start',
['local-adsb', 'local-dams', 'local-datacenters'],
['--group=local'],
],
[
'switch-equals-cold-start--ground',
'switch-equals-cold-start',
['ais-live-vessels', 'air-quality', 'gnss-integrity', 'fire-perimeters'],
['--group=ground'],
],
[
'switch-equals-cold-start--contacts',
'switch-equals-cold-start',
['military-awareness', 'military-installations'],
['--group=contacts'],
],
[
'switch-equals-cold-start--open',
'switch-equals-cold-start',
['drone-airspace', 'osm-places', 'sea-marks', 'ski-pistes'],
['--group=open'],
],
[
'switch-equals-cold-start--weather',
'switch-equals-cold-start',
[
'weather-radar',
'weather-satellite',
'weather-lightning',
'weather-cyclones',
],
['--group=weather'],
],
[
'switch-equals-cold-start--wind',
'switch-equals-cold-start',
['wind'],
['--group=wind'],
],
[
'switch-equals-cold-start--ghostmaps',
'switch-equals-cold-start',
['s2-ghostmaps'],
['--group=ghostmaps'],
],
[
'switch-equals-cold-start--wildlife',
'switch-equals-cold-start',
['wildlife-sightings'],
['--group=wildlife'],
],
[
'switch-equals-cold-start--urban',
'switch-equals-cold-start',
['bikeshare', 'transit', 'earthquakes'],
['--group=urban'],
],
[
'switch-equals-cold-start--cameras',
'switch-equals-cold-start',
['cctv', 'alpr-cameras'],
['--group=cameras'],
],
[
'switch-equals-cold-start--radio',
'switch-equals-cold-start',
['radio'],
['--group=radio'],
],
[
'switch-equals-cold-start--space',
'switch-equals-cold-start',
['satellites', 'rocket-launches'],
['--group=space'],
],
[
'switch-equals-cold-start--traffic',
'switch-equals-cold-start',
['traffic'],
['--group=traffic'],
],
[
'switch-equals-cold-start--street',
'switch-equals-cold-start',
['street-level'],
['--group=street'],
],
[
'switch-equals-cold-start--imagery',
'switch-equals-cold-start',
['recent-imagery'],
['--group=imagery'],
],
[
'switch-equals-cold-start--nepal',
'switch-equals-cold-start',
['bhote-koshi-locator', 'bhote-koshi-2026'],
['--group=nepal'],
],
[
'switch-equals-cold-start--cables',
'switch-equals-cold-start',
['telegeography-submarine-cables', 'directions'],
['--group=cables'],
],
[
'switch-equals-cold-start--fires',
'switch-equals-cold-start',
['local-firms'],
['--group=fires'],
],
['engine-switch', 'engine-switch', ['earthquakes']],
['director-engine-switch', 'director-engine-switch', []],
['layer-handoff', 'layer-handoff', ['flights']],
+363
View File
@@ -0,0 +1,363 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
import { whenSettled } from './qa/ready.mjs';
import { drawLedger } from './qa/ledger.mjs';
import {
COLD_START_GROUPS,
coldStartUrl,
compareColdStart,
probeSnapshot,
sessionExclusion,
} from './qa/coldStart.mjs';
import { coldStartFixtures } from './qa/coldStartFixtures.mjs';
import { stations } from './fixtures/layers/radio.mjs';
// Each registered group compares both directions, with a fresh storage context
// for every cold page. The URL writer and restore path supply the reference.
// node scripts/qa-switch-equals-cold-start.mjs http://127.0.0.1:4309
// --group=ground --evidence=tmp_switch_cold_ground.json
const base = localPreview(process.argv[2]);
const group = process.argv.find((arg) => arg.startsWith('--group='))?.slice(8);
assert.ok(
COLD_START_GROUPS[group],
`Supply --group=${Object.keys(COLD_START_GROUPS).join('|')}`,
);
const { layers, view } = COLD_START_GROUPS[group];
const fixture = await coldStartFixtures(base.origin, group);
const allow = JSON.parse(
readFileSync(new URL('./qa-ledger-allow.json', import.meta.url), 'utf8'),
);
const strict = createStrict({
origin: base.origin,
layers,
routes: fixture.routes,
fixtures: fixture.fixtures,
name: `switch-equals-cold-start--${group}`,
time: new Date(fixture.epoch).toISOString(),
});
const evidence = (strict.extra.coldStart = {
group,
layers,
samples: [],
differences: [],
skipped: [],
});
async function settle(page, cold = false) {
await whenSettled(page, { timeoutMs: 30000 });
await page.waitForFunction(
(layers, cold) => {
const { dataManager, engine } = window.__godsEyeView;
engine.render.request('qa-cold-start-readiness');
return layers.every((id) => {
const entry = dataManager.layers.get(id),
module = entry?.module;
if (!entry) return false;
if (cold && module.sessionState?.includes('enabled'))
return !entry.enabled;
if (
!entry.enabled ||
!entry.initialized ||
entry.sceneDetached ||
entry.refreshing
)
return false;
const stats = module.getStats?.() ?? {};
if (stats.loading || stats.refreshing) return false;
// These layers can be enabled with no search, route or selected subject.
if (
[
'directions',
'military-awareness',
'osm-places',
'sea-marks',
'ski-pistes',
].includes(id)
)
return true;
return stats.count > 0;
});
},
{ timeout: 30000, polling: 100 },
layers,
cold,
);
await whenSettled(page, { frames: 5, timeoutMs: 30000 });
}
async function prepare(page, owners) {
for (const id of layers) {
const before = await drawLedger(page);
assert.equal(
await page.evaluate(async (id) => {
const manager = window.__godsEyeView.dataManager;
return manager.setEnabled(id, true, { origin: 'user' });
}, id),
true,
`${id} enabled`,
);
for (const row of await drawLedger(page)) {
if (!before.some((old) => old.family === row.family && old.id === row.id))
owners[`${row.family}:${row.id}`] = id;
}
}
await page.evaluate(
async (group, stationId) => {
const manager = window.__godsEyeView.dataManager;
const module = (id) => manager.layers.get(id).module;
if (group === 'open')
module('osm-places').setResults(
[
{
id: 'node/1',
name: 'Fixture Clinic',
lat: 38.9,
lon: -77.03,
tags: {},
},
{
id: 'node/2',
name: 'Fixture Hospital',
lat: 38.905,
lon: -77.02,
tags: {},
},
],
{ kind: 'clinic', label: 'Clinics', area: 'Washington' },
);
if (group === 'street')
manager.setLayerParams(
'street-level',
{ kartaview: true, mapillary: false },
{ origin: 'user' },
);
if (group === 'wind')
await manager.setLayerParams(
'wind',
{ paused: true, overlay: 'temperature', units: 'm/s' },
{ origin: 'user' },
);
if (group === 'radio') {
module('radio').selectStation(stationId, {
autoplay: true,
focus: false,
origin: 'user',
});
await manager.setLayerParams(
'radio',
{ volume: 0.36 },
{ origin: 'user' },
);
}
if (group === 'imagery')
module('recent-imagery').setBox({
west: -97.77,
east: -97.71,
south: 30.25,
north: 30.29,
});
if (group === 'cables') {
module('directions').placeEndpoint('a', { lat: 55.32, lon: 10.78 });
module('directions').placeEndpoint('b', { lat: 55.33, lon: 10.82 });
}
},
group,
stations[10].id,
);
await settle(page);
if (group === 'cables')
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers
.get('directions')
.module.getStats().count > 0,
{ timeout: 30000 },
);
if (group === 'wind')
await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('wind')
.module.setParams({ inspect: true }),
);
if (group === 'imagery') {
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers
.get('recent-imagery')
.module.getSnapshot()
.candidates.some(
(row) =>
row.product === 'S30' && row.thumbnail?.status === 'present',
),
{ timeout: 30000 },
);
await page.evaluate(() => {
const module =
window.__godsEyeView.dataManager.layers.get('recent-imagery').module;
const row = module
.getSnapshot()
.candidates.find(
(row) => row.product === 'S30' && row.thumbnail?.status === 'present',
);
module.setAssignment('a', row.key);
module.setMode('image');
});
}
await settle(page);
// Asynchronous feed completion can create registries after enable resolves.
for (const row of await drawLedger(page)) {
const owner = layers.find((id) => row.id.startsWith(id));
if (owner) owners[`${row.family}:${row.id}`] ??= owner;
}
}
await strict.run({}, async (browser) => {
for (const [from, target] of [
['cesium', 'navara'],
['navara', 'cesium'],
]) {
const direction = `${from} -> ${target}`;
const sample = { direction, owners: {} };
evidence.samples.push(sample);
let context, page;
const close = async () => {
const unexpected = await page
?.evaluate(() => window.__coldStartUnexpectedApi ?? [])
.catch(() => []);
for (const path of unexpected ?? [])
evidence.differences.push({
layer: '[page]',
direction,
family: 'fixture',
id: path,
field: 'unstubbed API',
switchValue: 'blocked',
coldStartValue: 'fixture required',
});
await page?.evaluate(() => window.__qaFinishMonitors?.()).catch(() => {});
await context?.close();
context = page = null;
};
const open = async (url) => {
context = await browser.createBrowserContext();
const opened = await openPage(context, { shaderErrors: true });
page = opened.page;
sample.errors ??= [];
sample.errors.push(opened.errors);
await page.evaluateOnNewDocument(() => {
if (localStorage.length || sessionStorage.length)
throw new Error('Cold-start storage is not empty');
});
await fixture.install(page);
await page.goto(url, { waitUntil: 'domcontentloaded' });
return opened;
};
try {
const url = new URL(base.origin);
url.search = `?engine=${from}&welcome=0&look=cinematic`;
const [lat, lon, alt] = view;
url.hash = `v=2&lat=${lat}&lon=${group === 'street' ? fixture.streetLongitude : lon}&alt=${alt}&heading=0&pitch=-90&roll=0&map=osm&l=`;
await open(url.href);
await waitForEngine(page, from, { timeout: 60000 });
await prepare(page, sample.owners);
sample.before = await probeSnapshot(page, layers);
await strict.switchTo(page, target);
await settle(page);
sample.switched = await probeSnapshot(page, layers);
sample.url = await page.evaluate(() => {
window.__godsEyeView.styleManager.shareLinkManager.flush();
return location.href;
});
const retention = compareColdStart(sample.before, sample.switched, {
direction,
owners: sample.owners,
sessionState: false,
});
sample.retention = retention.differences.filter((row) =>
sessionExclusion(row, sample.before),
);
evidence.differences.push(
...sample.retention.map((row) => ({
...row,
family: `handoff:${row.family}`,
})),
);
await close();
sample.coldUrl = coldStartUrl(sample.url, target);
await open(sample.coldUrl);
await waitForEngine(page, target, { timeout: 60000 });
await settle(page, true);
sample.cold = await probeSnapshot(page, layers);
sample.comparison = compareColdStart(sample.switched, sample.cold, {
direction,
owners: sample.owners,
allow,
});
evidence.differences.push(...sample.comparison.differences);
evidence.skipped.push(...sample.comparison.skipped);
for (const id of layers) {
if (
![...sample.retention, ...sample.comparison.differences].some(
(row) => row.layer === id || row.layer === '[page]',
)
)
strict.record(
id,
target,
'engine switch equals a fresh share-link restore; declared session fields survive the switch',
);
}
for (const error of sample.errors.flat())
evidence.differences.push({
layer: '[page]',
direction,
family: 'error',
id: '',
field: 'pageerror',
switchValue: error,
coldStartValue: null,
});
} catch (error) {
sample.error = String(error.stack ?? error);
sample.failureSnapshot = await probeSnapshot(page, layers).catch(
() => null,
);
evidence.differences.push({
layer: layers.join(', '),
direction,
family: 'readiness',
id: '',
field: 'error',
switchValue: error.message,
coldStartValue: '<not captured>',
});
} finally {
await close();
}
}
console.log(
`switch-equals-cold-start ${group}: ${evidence.differences.length} differences, ${evidence.skipped.length} declared session differences`,
);
if (evidence.differences.length)
console.table(
evidence.differences.map((row) => ({
...row,
switchValue: JSON.stringify(row.switchValue),
coldStartValue: JSON.stringify(row.coldStartValue),
})),
);
assert.equal(
evidence.differences.length,
0,
'See coldStart.differences in the evidence file',
);
console.log(
`PASS switch-equals-cold-start ${group}: 2 directions, ${layers.length} layers, 0 differences`,
);
});
+293
View File
@@ -0,0 +1,293 @@
import { createQaProbe, createSettledState } from '../../src/app/qaProbe.js';
import { compareLedgers, drawLedger } from './ledger.mjs';
// Each group needs four page starts and two picker switches within 300 seconds.
export const COLD_START_GROUPS = {
aircraft: {
layers: ['flights', 'military'],
view: [40.2539, -125.5779, 21130],
},
local: {
layers: ['local-adsb', 'local-dams', 'local-datacenters'],
view: [41.534, -90.507, 20000],
},
ground: {
layers: [
'ais-live-vessels',
'air-quality',
'gnss-integrity',
'fire-perimeters',
],
view: [43.65, -79.38, 46000],
},
contacts: {
layers: ['military-awareness', 'military-installations'],
view: [43.65, -79.38, 46000],
},
open: {
layers: ['drone-airspace', 'osm-places', 'sea-marks', 'ski-pistes'],
view: [38.9, -77.03, 30000],
},
weather: {
layers: [
'weather-radar',
'weather-satellite',
'weather-lightning',
'weather-cyclones',
],
view: [43.65, -79.38, 46000],
},
wind: { layers: ['wind'], view: [43.65, -79.38, 2000000] },
ghostmaps: { layers: ['s2-ghostmaps'], view: [43.65, -79.38, 46000] },
wildlife: { layers: ['wildlife-sightings'], view: [48.4, -123.1, 200000] },
urban: {
layers: ['bikeshare', 'transit', 'earthquakes'],
view: [42.3554, -71.0605, 6000],
},
cameras: { layers: ['cctv', 'alpr-cameras'], view: [30.262, -97.7431, 5500] },
radio: { layers: ['radio'], view: [30.27, -97.74, 12000] },
space: { layers: ['satellites', 'rocket-launches'], view: [5, 5, 18000000] },
traffic: { layers: ['traffic'], view: [30.267, -97.744, 1800] },
street: { layers: ['street-level'], view: [38.5831, -121.504, 850] },
imagery: { layers: ['recent-imagery'], view: [30.27, -97.74, 12000] },
nepal: {
layers: ['bhote-koshi-locator', 'bhote-koshi-2026'],
view: [28.3326, 85.4848, 18000],
},
cables: {
layers: ['telegeography-submarine-cables', 'directions'],
view: [55.3241, 10.7995, 25000],
},
fires: { layers: ['local-firms'], view: [37.77, -122.42, 100000] },
};
/** Change only the engine query of the address written by the application. */
export function coldStartUrl(href, engine) {
const url = new URL(href);
url.searchParams.set('engine', engine);
return url.href;
}
// Share addresses round the camera to 0.0001 degrees and whole metres/degrees.
// World positions must still match within a metre; projected positions allow
// two CSS pixels for that camera quantization. Counts and identities are exact.
export function numericTolerance(field) {
if (/(?:^|\.)screen\.[xy]$/.test(field)) return 2;
if (/(?:^|\.)position\.[xyz]$/.test(field)) return 1;
return 1e-6;
}
/** Compare JSON without losing absent fields, nulls or identities in arrays. */
export function compareProbe(before, after, { skip = [], field = '' } = {}) {
const differences = [];
const visit = (left, right, path) => {
if (skip.includes(path)) return;
if (Object.is(left, right)) return;
if (typeof left === 'number' && typeof right === 'number') {
const tolerance = /(?:count|capacity|length|id)$/.test(path)
? 0
: numericTolerance(path);
if (
Number.isFinite(left) &&
Number.isFinite(right) &&
Math.abs(left - right) <= tolerance
)
return;
} else if (
left &&
right &&
typeof left === 'object' &&
typeof right === 'object' &&
Array.isArray(left) === Array.isArray(right)
) {
if (Array.isArray(left)) {
if (left.length !== right.length)
visit(left.length, right.length, `${path}.length`);
const identified = [...left, ...right].every(
(row) => row && typeof row === 'object' && row.id !== undefined,
);
if (identified) {
const a = new Map(left.map((row) => [String(row.id), row]));
const b = new Map(right.map((row) => [String(row.id), row]));
if (a.size !== left.length || b.size !== right.length)
differences.push({
field: path,
switchValue: left,
coldStartValue: right,
error: 'duplicate ids',
});
for (const id of new Set([...a.keys(), ...b.keys()]))
visit(a.get(id), b.get(id), `${path}[${JSON.stringify(id)}]`);
} else
for (let i = 0; i < Math.max(left.length, right.length); i++)
visit(left[i], right[i], `${path}[${i}]`);
} else
for (const key of new Set([
...Object.keys(left),
...Object.keys(right),
]))
visit(left[key], right[key], path ? `${path}.${key}` : key);
return;
}
differences.push({
field: path,
switchValue: left === undefined ? '<absent>' : left,
coldStartValue: right === undefined ? '<absent>' : right,
});
};
visit(before, after, field);
return differences;
}
/** The production build drops its dev probe; inject the same implementation. */
export async function probeSnapshot(page, layers) {
const probe = await page.evaluate(`(() => {
const plain = (p) => p ? { x: p.x, y: p.y, z: p.z } : null;
const createSettledState = ${createSettledState};
const probe = (${createQaProbe})(window.__godsEyeView);
const layers = ${JSON.stringify(layers)};
try {
return {
layers: Object.fromEntries(layers.map((id) => {
const module = window.__godsEyeView.dataManager.layers.get(id).module;
const session = id === 'radio'
? { selected: module.getUIState().selected, audioState: module.getUIState().audioState }
: id === 'wind' ? { reading: module.getRowControls().summary.reading }
: id === 'osm-places' ? { results: module.getAnalystRecords(1000000) }
: id === 'directions' ? { route: module.getRowControls().list } : {};
return [id, {
enabled: window.__godsEyeView.dataManager.layers.get(id).enabled,
objects: probe.objects(id, 1000000), selected: probe.selected(id),
session, sessionState: module.sessionState ?? [],
}];
})),
following: probe.following(), movers: probe.movers(),
};
} finally { probe.destroy(); }
})()`);
return { ...probe, ledger: await drawLedger(page) };
}
/** Keep raw samples and declared exclusions beside the complete difference list. */
export function compareColdStart(
switched,
cold,
{ direction, owners = {}, allow = [], sessionState = true },
) {
const differences = [],
skipped = [];
const add = (layer, family, id, rows) =>
differences.push(
...rows.map((row) => ({ layer, direction, family, id, ...row })),
);
const layerOf = (family, id) => owners[`${family}:${id}`] ?? '[page]';
const ledger = compareLedgers(switched.ledger, cold.ledger, allow);
for (const [side, rows] of [
['switch', ledger.onlyBefore],
['cold', ledger.onlyAfter],
])
for (const { family, id } of rows)
add(layerOf(family, id), family, id, [
{
field: 'present',
switchValue: side === 'switch',
coldStartValue: side === 'cold',
},
]);
for (const { family, id, before, after } of ledger.shown)
add(layerOf(family, id), family, id, [
{ field: 'shown', switchValue: before, coldStartValue: after },
]);
for (const row of switched.ledger) {
const next = cold.ledger.find(
(other) => other.family === row.family && other.id === row.id,
);
if (next)
add(
layerOf(row.family, row.id),
row.family,
row.id,
compareProbe(
{ kind: row.kind, count: row.count },
{ kind: next.kind, count: next.count },
),
);
}
for (const layer of new Set([
...Object.keys(switched.layers),
...Object.keys(cold.layers),
])) {
const before = switched.layers[layer],
after = cold.layers[layer];
for (const row of compareProbe(before, after)) {
const id = /^objects\[("(?:[^"\\]|\\.)*")\]/.exec(row.field)?.[1];
add(layer, 'probe', id ? JSON.parse(id) : layer, [row]);
}
}
add(
'[page]',
'probe',
'following',
compareProbe(switched.following, cold.following, { field: 'following' }),
);
// Keep all mover registry fields, including capacity, beyond the draw ledger.
const a = new Map(switched.movers.map((row) => [String(row.id), row]));
const b = new Map(cold.movers.map((row) => [String(row.id), row]));
for (const id of new Set([...a.keys(), ...b.keys()]))
add(
layerOf('movers', id),
'probe-movers',
id,
compareProbe(a.get(id), b.get(id)),
);
return {
differences: differences.filter((row) => {
const field = sessionState && sessionExclusion(row, switched);
if (!field) return true;
skipped.push({ ...row, sessionState: field });
return false;
}),
skipped,
ledger,
};
}
/** Only declared fields and their drawn derivatives can differ from a link. */
export function sessionExclusion(row, snapshot) {
const declared = snapshot.layers[row.layer]?.sessionState ?? [];
for (const field of declared) {
if (
row.family === 'probe' &&
(row.field === `session.${field}` ||
row.field.startsWith(`session.${field}.`) ||
row.field.startsWith(`session.${field}[`))
)
return field;
// Session-only layers are absent from the link. Their drawing must still
// survive the switch; the scenario checks these same rows against S first.
if (
field === 'enabled' &&
(row.family !== 'probe' ||
/^(enabled|objects|selected)(\.|\[|$)/.test(row.field))
)
return field;
if (
row.layer === 'radio' &&
field === 'selected' &&
['radio-selected', 'radio-bracket'].includes(row.id)
)
return field;
if (
row.layer === 'directions' &&
field === 'route' &&
[
'directions-route',
'directions-steps',
'directions-markers',
'directions-marker-letters',
].includes(row.id)
)
return field;
}
return null;
}
+258
View File
@@ -0,0 +1,258 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
import { APPLICATION_LAYER_DIRECTORY } from '../../src/app/layers/directory.js';
import {
COLD_START_GROUPS,
coldStartUrl,
compareProbe,
compareColdStart,
sessionExclusion,
probeSnapshot,
} from './coldStart.mjs';
const snapshot = () => ({
layers: {
radio: {
enabled: true,
objects: [],
selected: null,
session: { selected: null, audioState: 'idle' },
sessionState: ['selected', 'audioState'],
},
},
ledger: [
{
family: 'movers',
id: 'radio-selected',
shown: true,
count: 1,
kind: 'point',
},
],
movers: [{ id: 'radio-selected', count: 1, kind: 'point', capacity: 1 }],
following: null,
});
const options = {
direction: 'cesium -> navara',
owners: { 'movers:radio-selected': 'radio' },
};
test('groups cover every strict catalog layer once and are registered with flags', () => {
const ids = Object.values(COLD_START_GROUPS).flatMap((group) => group.layers);
assert.deepEqual(
ids.sort(),
APPLICATION_LAYER_DIRECTORY.map(({ id }) => id).sort(),
);
assert.ok(
Object.values(COLD_START_GROUPS).every(({ layers }) => layers.length <= 4),
);
const runner = readFileSync(
new URL('../qa-all-layer-switches.mjs', import.meta.url),
'utf8',
);
const registered = JSON.parse(
JSON.stringify(
vm.runInNewContext(runner.match(/const runs = (\[[\s\S]*?\n\]);/)[1], {
soakRuns: [],
longRun: '',
}),
),
);
for (const [group, { layers }] of Object.entries(COLD_START_GROUPS)) {
assert.deepEqual(
registered.find(
([label]) => label === `switch-equals-cold-start--${group}`,
),
[
`switch-equals-cold-start--${group}`,
'switch-equals-cold-start',
layers,
[`--group=${group}`],
],
);
}
});
test('cold URL preserves the app-written hash, path and other query settings', () => {
const from =
'http://127.0.0.1:4309/share/ABC?engine=cesium&lang=de#v=2&l=f&lo=f.t.71be26';
const result = new URL(coldStartUrl(from, 'navara'));
assert.equal(result.pathname, '/share/ABC');
assert.equal(result.hash, new URL(from).hash);
assert.equal(result.searchParams.get('engine'), 'navara');
assert.equal(result.searchParams.get('lang'), 'de');
});
test('probe comparison tolerates camera quantization but rejects lost objects and count changes', () => {
const a = {
objects: [
{
id: 'a',
position: { x: 6378000, y: 0, z: 0 },
screen: { x: 500, y: 400 },
},
],
};
const b = structuredClone(a);
b.objects[0].position.x += 0.5;
b.objects[0].screen.x += 1.5;
assert.deepEqual(compareProbe(a, b), []);
b.objects[0].position.x += 1;
assert.equal(compareProbe(a, b)[0].field, 'objects["a"].position.x');
assert.ok(
compareProbe(a, { objects: [] }).some(
({ field }) => field === 'objects["a"]',
),
);
assert.equal(compareProbe({ count: 1 }, { count: 1.0000001 }).length, 1);
assert.equal(compareProbe({ selected: null }, {}).length, 1);
assert.equal(
compareProbe({ position: { x: NaN } }, { position: { x: 0 } }).length,
1,
);
});
test('probe arrays compare identities without hiding duplicate ids or reordered paths', () => {
assert.deepEqual(
compareProbe([{ id: 'b' }, { id: 'a' }], [{ id: 'a' }, { id: 'b' }]),
[],
);
assert.ok(
compareProbe([{ id: 'a' }, { id: 'a' }], [{ id: 'a' }, { id: 'a' }]).some(
(row) => row.error === 'duplicate ids',
),
);
assert.equal(compareProbe([1, 2], [2, 1]).length, 2);
assert.equal(
compareProbe(
{ selected: { id: 'a' }, objects: [] },
{ selected: null, objects: [1] },
{ skip: ['selected'] },
).length,
2,
);
});
test('comparison returns every ledger family and field difference, with attribution', () => {
const a = snapshot(),
b = snapshot();
b.ledger[0].count = 2;
b.ledger[0].shown = false;
b.ledger.push({ family: 'lines', id: 'extra', shown: true });
b.layers.radio.objects = [{ id: 'new' }];
b.following = { label: 'lost', native: null };
const result = compareColdStart(a, b, { ...options, sessionState: false });
assert.ok(
result.differences.some(
(row) => row.field === 'count' && row.layer === 'radio',
),
);
assert.ok(result.differences.some((row) => row.field === 'shown'));
assert.ok(
result.differences.some(
(row) => row.family === 'lines' && row.id === 'extra',
),
);
assert.ok(result.differences.some((row) => row.id === 'following'));
assert.ok(
result.differences.some(
(row) => row.field === 'objects["new"]' && row.id === 'new',
),
);
assert.equal(result.differences.length, 6);
});
test('declared session fields do not hide other probe fields or other layers', () => {
const a = snapshot(),
b = snapshot();
a.layers.radio.session.selected = { id: 'station' };
a.layers.radio.session.audioState = 'playing';
b.movers[0].count = b.ledger[0].count = 0;
b.layers.radio.enabled = false;
const result = compareColdStart(a, b, options);
assert.equal(result.skipped.length, 4);
assert.deepEqual(
result.differences.map((row) => row.field),
['enabled'],
);
b.layers.radio.sessionState.push('objects');
assert.ok(
compareColdStart(a, b, options).differences.some((row) =>
row.field.startsWith('sessionState'),
),
);
assert.equal(
sessionExclusion(
{ layer: 'wind', family: 'movers', id: 'radio-selected', field: 'count' },
a,
),
null,
);
});
test('a lost session field still fails the source-to-switch retention check', () => {
const a = snapshot(),
b = snapshot();
a.layers.radio.session.selected = { id: 'station' };
b.movers[0].count = b.ledger[0].count = 0;
const retention = compareColdStart(a, b, {
...options,
sessionState: false,
}).differences.filter((row) => sessionExclusion(row, a));
assert.equal(retention.length, 3);
});
test('allowlisted absences keep their reason and never hide changed visibility or counts', () => {
const a = snapshot(),
b = snapshot();
a.ledger.push({ family: 'lines', id: 'overlay', shown: true });
b.ledger[0].shown = false;
const result = compareColdStart(a, b, {
...options,
sessionState: false,
allow: [{ family: 'lines', id: 'overlay', reason: 'Engine overlay' }],
});
assert.equal(result.ledger.allowed[0].reason, 'Engine overlay');
assert.deepEqual(
result.differences.map((row) => row.field),
['shown'],
);
});
test('the production probe captures all requested fields and releases its frame listeners', async () => {
const engine = createFakeEngine();
const position = { x: 6378137, y: 0, z: 0 };
const dataManager = {
layers: new Map([
[
'fixture',
{
enabled: true,
module: {
getAllPositions: () => [{ id: 'one', position }],
getSelectedInfo: () => ({ id: 'one' }),
},
},
],
]),
};
const context = vm.createContext({
window: { __godsEyeView: { engine, dataManager } },
});
const page = {
evaluate: async (code) =>
vm.runInContext(typeof code === 'string' ? code : `(${code})()`, context),
};
const listeners = engine.frames.postRender.numberOfListeners;
const snapshot = await probeSnapshot(page, ['fixture']);
assert.equal(snapshot.layers.fixture.objects[0].id, 'one');
assert.equal(snapshot.layers.fixture.selected.id, 'one');
assert.equal(snapshot.following, null);
assert.equal(snapshot.movers.length, 0);
assert.equal(snapshot.ledger.length, 0);
assert.equal(engine.frames.postRender.numberOfListeners, listeners);
engine.destroy();
});
+622
View File
@@ -0,0 +1,622 @@
import { readFileSync } from 'node:fs';
import sharp from 'sharp';
import { installAlprFixture } from '../fixtures/layers/alpr.mjs';
import { installCctvFixture } from '../fixtures/layers/cctv.mjs';
import { installCombinedFixture } from '../fixtures/layers/combined.mjs';
import { installRadioFixture } from '../fixtures/layers/radio.mjs';
import {
installTransitFixture,
TRANSIT_EPOCH,
} from '../fixtures/layers/transit.mjs';
import { firmsFixture } from '../fixtures/layers/firms.mjs';
import { wildlifeRoutes } from '../fixtures/wildlife.mjs';
import { serveFixtures, newFixture } from '../qa-street-level.mjs';
import { KV_LINE } from '../fixtures/street-level/kartaView.mjs';
import { trafficFixtureResponse } from '../traffic-fixtures.mjs';
import {
satelliteFixture,
SATELLITE_FIXTURE_EPOCH,
} from '../../src/bench/satelliteFixture.js';
import { launchFixture } from '../../src/bench/launchFixture.js';
// These are the feeds used by the strict switching scenarios, with motion and
// timestamps held fixed so page startup duration cannot change the sample.
export async function coldStartFixtures(origin, group) {
const epoch =
group === 'space'
? SATELLITE_FIXTURE_EPOCH
: group === 'urban'
? TRANSIT_EPOCH
: Date.now();
const png = await sharp({
create: { width: 256, height: 256, channels: 4, background: '#40506a' },
})
.png()
.toBuffer();
const json = (body) => ({
contentType: 'application/json',
body: JSON.stringify(body),
});
const image = () => ({ contentType: 'image/png', body: png });
const routes = [];
const fixtures = [];
const route = (pattern, respond) => routes.push({ pattern, respond });
if (group === 'wildlife') routes.push(...wildlifeRoutes(origin).routes);
if (group === 'open') {
route('https://tiles.openseamap.org/*', image);
route('https://www.opensnowmap.org/*', image);
}
if (group === 'cameras') fixtures.push('https://tiles.dontgetflocked.com/*');
if (group === 'street')
fixtures.push(
'*://*.mapillary.com/*',
'https://api.kartaview.org/*',
'https://cdn.kartaview.org/*',
'*://*.openstreetcam.org/*',
'https://api.panoramax.xyz/*',
'https://panoramax.openstreetmap.fr/*',
);
if (group === 'nepal') {
route('https://i.ytimg.com/*', image);
for (const pattern of [
'https://www.youtube.com/iframe_api*',
'https://connect.facebook.net/*',
])
route(pattern, () => ({ contentType: 'text/javascript', body: '' }));
for (const pattern of [
'https://www.youtube-nocookie.com/*',
'https://www.facebook.com/*',
])
route(pattern, () => ({
contentType: 'text/html',
body: '<!doctype html><title>Fixture media</title>',
}));
}
if (group === 'traffic') {
for (const pattern of [
'https://tiles.openfreemap.org/*',
origin + '/api/tomtom/*',
])
route(
pattern,
(req) =>
trafficFixtureResponse({ url: () => req.url }) ??
json({ hasKey: false }),
);
}
if (group === 'imagery') {
route('https://cmr.earthdata.nasa.gov/*', () =>
json({
hits: 2,
items: ['2026-10-28', '2026-10-27'].map((day) => ({
umm: {
TemporalExtent: {
RangeDateTime: { BeginningDateTime: `${day}T12:00:00Z` },
},
AdditionalAttributes: [{ Name: 'CLOUD_COVERAGE', Values: ['0'] }],
SpatialExtent: {
HorizontalSpatialDomain: {
Geometry: {
BoundingRectangles: [
{
WestBoundingCoordinate: -98,
EastBoundingCoordinate: -97,
SouthBoundingCoordinate: 30,
NorthBoundingCoordinate: 31,
},
],
},
},
},
},
})),
}),
);
route('https://*.earthdata.nasa.gov/*', image);
}
if (group === 'weather') {
const times = ['2026-11-02T12:00:00.000Z', '2026-11-02T12:05:00.000Z'];
route(origin + '/api/weather/*', (req) => {
const url = new URL(req.url),
product = url.searchParams.get('product');
return url.pathname.endsWith('manifest')
? json({
schemaVersion: 1,
product,
times,
latest: times.at(-1),
maxLevel: 6,
tileSize: 256,
tilingScheme: 'geographic',
bounds: { west: -130, south: 20, east: -50, north: 60 },
display: { credit: 'Weather fixture' },
})
: image();
});
route(origin + '/api/cyclones*', () =>
json({
schemaVersion: 1,
source: 'NOAA fixture',
attribution: 'NOAA fixture',
coverage: 'Atlantic',
fetchedAt: Date.parse(times[1]),
stale: false,
unavailable: false,
reason: null,
storms: [
{
id: 'al012026',
name: 'Fixture storm',
basin: 'AL',
classification: 'TS',
position: { longitude: -79.38, latitude: 43.65 },
advisoryNumber: '10',
issuedAt: times[1],
positionAt: times[1],
windKt: 40,
pressureHpa: 1000,
movement: { directionDegrees: 45, speedKt: 10 },
advisoryUrl: null,
outlookUrl: null,
geometryStatus: 'current',
geometryAdvisoryNumber: '10',
track: {
type: 'LineString',
coordinates: [
[-79.38, 43.65],
[-79.31, 43.68],
],
},
cone: {
type: 'Polygon',
coordinates: [
[
[-79.35, 43.63],
[-79.28, 43.63],
[-79.28, 43.7],
[-79.35, 43.63],
],
],
},
forecastPoints: [
{
tauHours: 24,
position: { longitude: -79.31, latitude: 43.68 },
windKt: 50,
gustKt: 60,
},
],
},
],
}),
);
}
if (group === 'wind')
route(origin + '/api/wind/*', (req) => {
const url = new URL(req.url),
model = url.searchParams.get('model'),
kind = url.searchParams.get('overlay');
if (url.pathname.endsWith('/manifest'))
return json({
model,
overlay: kind ?? 'none',
grid: { nx: 36, ny: 19, lo1: 0, la1: 90, dx: 10, dy: 10 },
gridUrl: '/api/wind/grid/' + model + '-fixture.bin' + url.search,
cycle: {
validIso: '2026-11-02T12:00:00Z',
runIso: '2026-11-02T06:00:00Z',
},
...(kind
? {
scalar: {
kind,
units: kind === 'temperature' ? '°C' : 'hPa',
level: '2 m above ground',
},
}
: {}),
});
const count = 36 * 19,
values = new Float32Array(count * (kind ? 3 : 2));
values.fill(8, 0, count);
values.fill(6, count, 2 * count);
if (kind) values.fill(kind === 'temperature' ? 40 : 1020, 2 * count);
return {
contentType: 'application/octet-stream',
body: Buffer.from(values.buffer),
};
});
const groups =
group === 'space'
? Object.fromEntries(
[
'stations',
'visual',
'gps-ops',
'glo-ops',
'galileo',
'geo',
'starlink',
'resource',
'weather',
'planet',
'sar',
'science',
].map((id) => [id, satelliteFixture(id)]),
)
: {};
const ghost =
group === 'ghostmaps'
? [
...readFileSync(
new URL('../fixtures/ghostmaps-scenes.kmz', import.meta.url),
),
]
: [];
const firms = firmsFixture();
return {
routes,
fixtures,
epoch,
streetLongitude: KV_LINE.lon,
async install(page) {
if (group === 'urban') {
await installCombinedFixture(page);
const transit = await installTransitFixture(page);
transit.setStep(9);
}
if (group === 'radio') await installRadioFixture(page);
if (group === 'cameras') {
await installCctvFixture(page);
await installAlprFixture(page);
}
if (group === 'street')
await serveFixtures(page, newFixture({ configured: true }));
await page.evaluateOnNewDocument(installFeeds, {
group,
epoch,
groups,
ghost,
launches: launchFixture(),
firms,
});
},
};
}
function installFeeds({ group, epoch, groups, ghost, launches, firms }) {
const RealDate = Date;
globalThis.Date = class extends RealDate {
constructor(...args) {
super(...(args.length ? args : [epoch]));
}
static now() {
return epoch;
}
};
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
window.__coldStartUnexpectedApi = [];
const original = window.fetch.bind(window);
const json = (body) => Promise.resolve(Response.json(body));
const EventSource = window.EventSource;
window.EventSource = class extends EventTarget {
constructor(url, options) {
super();
if (!url.includes('/api/ghostmaps/events'))
return new EventSource(url, options);
this.readyState = 1;
}
close() {}
};
window.fetch = (input, options) => {
const url = new URL(input.url ?? input, location.href),
path = url.pathname;
if (path === '/api/flights')
return json({
time: epoch / 1000,
states:
group === 'aircraft'
? [
[
'71be26',
'GEV123',
'Fixture',
epoch / 1000,
epoch / 1000,
-125.5779,
40.2539,
9700,
false,
0,
45,
0,
null,
9700,
null,
false,
0,
],
]
: [],
});
if (path === '/api/military')
return json({
now: epoch,
ac:
group === 'aircraft'
? [
{
hex: 'aaa052',
flight: 'GEVMIL',
lon: -125.58,
lat: 40.26,
alt_geom: 31825,
alt_baro: 31825,
gs: 0,
track: 45,
seen: 0,
seen_pos: 0,
dbFlags: 1,
type: 'adsb_icao',
},
]
: [],
});
if (path.startsWith('/api/flights/')) return json({ path: [] });
if (path.startsWith('/api/adsbdb/')) return json({ response: {} });
if (path === '/api/vessels')
return json({
status: 'live',
rows: ['ground', 'contacts'].includes(group)
? [
{
mmsi: '123456789',
name: 'Fixture Toronto',
lat: 43.65,
lon: -79.38,
last_position_epoch: epoch / 1000,
},
]
: [],
});
if (path === '/api/vessels/track') return json({ samples: [] });
if (path === '/api/local-receivers/aircraft')
return json({
configured: true,
generatedAt: epoch,
records: [
{
icao: 'abc123',
callsign: 'LOCAL1',
lat: 41.534,
lon: -90.507,
altitudeFt: 11000,
groundSpeedKt: 0,
trackDeg: 5,
lastPositionAt: epoch,
lastMessageAt: epoch,
messageCount: 12,
band: '1090',
source: 'decoder',
},
],
});
if (url.hostname === 'earthquake.usgs.gov')
return json({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
id: 'fixture-boston',
geometry: { type: 'Point', coordinates: [-71.0605, 42.3554, 8] },
properties: { mag: 4, time: epoch, place: 'Fixture Boston' },
},
],
});
if (path === '/api/military-installations')
return json({
status: 'ok',
elements:
group === 'contacts'
? [
{
type: 'way',
id: 424242,
tags: { military: 'naval_base', name: 'Fixture Harbour' },
center: { lat: 43.6725, lon: -79.33 },
geometry: [
[43.665, -79.345],
[43.665, -79.315],
[43.68, -79.315],
[43.68, -79.345],
[43.665, -79.345],
].map(([lat, lon]) => ({ lat, lon })),
},
]
: [],
});
if (path.startsWith('/api/fire-perimeters/inciweb'))
return json({ rows: [] });
if (path === '/api/fire-perimeters')
return json({
rows: [
{
stableId: 'fire:fixture',
name: 'Fixture Fire',
acres: 500,
containedPct: 40,
polygons: [
[
[
[-79.47, 43.56],
[-79.29, 43.56],
[-79.29, 43.74],
[-79.47, 43.74],
[-79.47, 43.56],
],
],
],
},
],
});
if (path === '/api/gnss-integrity')
return json({
ageMs: 0,
rows: ['a1', 'a2', 'a3'].map((hex) => ({
hex,
lat: 43.65,
lon: -79.38,
nic: 0,
nacp: 0,
degraded: true,
gpsLost: false,
})),
});
if (url.hostname === 'api.weather.gc.ca')
return json({
type: 'FeatureCollection',
features:
group !== 'ground'
? []
: [
{
type: 'Feature',
...(path.includes('aqhi-stations')
? {
geometry: {
type: 'Point',
coordinates: [-79.38, 43.65],
},
properties: {
location_id: 'QA1',
location_name_en: 'Fixture Toronto',
},
}
: {
properties: {
location_id: 'QA1',
aqhi: 9,
observation_datetime: new Date().toISOString(),
},
}),
},
],
});
if (path === '/api/drone-airspace')
return json({
rows: [
{
stableId: '310',
name: 'Fixture Zone',
typeCode: 'P',
type: 'Prohibited',
lowerAltitude: 'SFC',
upperAltitude: '18,000 ft MSL',
timesOfUse: 'CONTINUOUS',
agency: null,
city: 'WASHINGTON',
state: 'DC',
polygons: [
[
[
[-77.06, 38.88],
[-77, 38.88],
[-77, 38.92],
[-77.06, 38.92],
[-77.06, 38.88],
],
],
],
},
],
stale: false,
fetchedAt: epoch,
});
if (path === '/api/overpass/status') return json({ configured: true });
if (path.startsWith('/api/overpass/'))
return json({
elements: [
{
type: 'node',
id: 1,
lat: 38.9,
lon: -77.03,
tags: { name: 'Fixture Clinic', amenity: 'clinic' },
},
],
});
if (path === '/api/ghostmaps/catalog')
return json({
commit: 'fixture',
products: [
{ id: 'cip', version: 'fixture', title: 'Fixture', defaultOn: true },
],
});
if (path.startsWith('/api/ghostmaps/file/'))
return Promise.resolve(new Response(new Uint8Array(ghost)));
if (path.startsWith('/api/celestrak/'))
return Promise.resolve(
new Response(groups[path.split('/').pop()] ?? groups.stations),
);
if (path === '/api/launches') return json(launches);
if (path === '/api/firms') return json(firms);
if (path === '/api/route') {
const [a, b] = url.searchParams
.get('coords')
.split(';')
.map((point) => point.split(',').map(Number));
const geometry = [a, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2], b];
return json({
ok: true,
geometry,
distanceM: 9000,
durationS: 800,
steps: geometry.map(([lon, lat], i) => ({
lon,
lat,
instruction: 'Route step ' + i,
distanceM: 3000,
durationS: 120,
})),
});
}
if (path === '/api/terrain/heights')
return json({
results: (url.searchParams.get('points') ?? '')
.split(';')
.map(() => ({ ellipsoid: 0 })),
});
// Same-origin proxies must not silently turn this into a live-feed test.
// Delegate only routes answered by the fixtures above or the strict harness.
if (path.startsWith('/api/')) {
if (path === '/api/google/nearby-places') return json({ places: [] });
if (path === '/api/setup/status') return json({ ready: true });
if (/^\/api\/(firms|mapillary)\/status$/.test(path) && group !== 'street')
return json({ hasKey: false, configured: false });
if (path === '/api/tomtom/status' && group !== 'traffic')
return json({ hasKey: false });
const delegated = {
urban: /^\/api\/(gbfs|transit)\//,
cameras: /^\/api\/cctv\//,
radio: /^\/api\/radio\//,
street: /^\/api\/mapillary\//,
weather: /^\/api\/(weather\/|cyclones)/,
wind: /^\/api\/wind\//,
traffic: /^\/api\/tomtom\//,
wildlife: /^\/api\/(inaturalist|acartia)\//,
}[group];
if (
!delegated?.test(path) &&
![
'/api/openai/hud-summary/status',
'/api/google/tiles-token',
'/api/realtime/debug-log',
].includes(path)
) {
window.__coldStartUnexpectedApi.push(path);
return Promise.reject(new Error(`No cold-start fixture for ${path}`));
}
}
return original(input, options);
};
}
+97
View File
@@ -0,0 +1,97 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import vm from 'node:vm';
import { coldStartFixtures } from './coldStartFixtures.mjs';
import { COLD_START_GROUPS } from './coldStart.mjs';
const origin = 'http://127.0.0.1:4309';
test('every group has valid strict fixture routes', async () => {
for (const group of Object.keys(COLD_START_GROUPS)) {
const fixture = await coldStartFixtures(origin, group);
assert.ok(Number.isFinite(fixture.epoch));
for (const { pattern } of fixture.routes)
assert.ok(new URLPattern(pattern));
for (const pattern of fixture.fixtures) assert.ok(new URLPattern(pattern));
}
});
test('imagery fixtures answer catalog, thumbnails and every GIBS tile subdomain', async () => {
const { routes } = await coldStartFixtures(origin, 'imagery');
for (const host of ['cmr', 'wvs', 'gibs-a', 'gibs-b', 'gibs-c']) {
const url = `https://${host}.earthdata.nasa.gov/fixture`;
const route = routes.find(({ pattern }) =>
new URLPattern(pattern).test(url),
);
assert.ok(route, host);
const response = await route.respond({ url });
assert.equal(
response.contentType,
host === 'cmr' ? 'application/json' : 'image/png',
);
assert.ok(response.body.length);
}
});
test('wind fixture supplies the declared grid dimensions and uniform vectors', async () => {
const { routes } = await coldStartFixtures(origin, 'wind');
const manifest = JSON.parse(
routes[0].respond({
url: origin + '/api/wind/manifest?model=gfs&overlay=temperature',
}).body,
);
const response = routes[0].respond({ url: origin + manifest.gridUrl });
const values = new Float32Array(
response.body.buffer,
response.body.byteOffset,
response.body.length / 4,
);
const count = manifest.grid.nx * manifest.grid.ny;
assert.equal(values.length, count * 3);
assert.ok(values.slice(0, count).every((value) => value === 8));
assert.ok(values.slice(count, count * 2).every((value) => value === 6));
});
test('independent pages receive identical stationary aircraft and timestamps through fetch', async () => {
const fixture = await coldStartFixtures(origin, 'aircraft');
const scripts = [];
await fixture.install({
evaluateOnNewDocument: async (fn, args) => scripts.push({ fn, args }),
});
const page = () => {
const context = vm.createContext({
URL,
Response,
EventTarget,
location: { href: origin },
localStorage: { setItem() {} },
window: {
EventSource: class {},
fetch() {
throw new Error('Unstubbed network request');
},
},
});
for (const { fn, args } of scripts)
vm.runInContext(`(${fn})(${JSON.stringify(args)})`, context);
return context;
};
const a = page(),
b = page();
for (const path of ['/api/flights', '/api/military', '/api/flights/track']) {
const read = async (context) =>
(
await vm.runInContext(`window.fetch(${JSON.stringify(path)})`, context)
).json();
assert.deepEqual(await read(a), await read(b));
}
assert.equal(vm.runInContext('Date.now()', a), fixture.epoch);
assert.equal(vm.runInContext('+new Date()', b), fixture.epoch);
await assert.rejects(
vm.runInContext("window.fetch('/api/unexpected-feed')", a),
/No cold-start fixture/,
);
assert.equal(
vm.runInContext('window.__coldStartUnexpectedApi[0]', a),
'/api/unexpected-feed',
);
});
+263
View File
@@ -0,0 +1,263 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { optionSpecs, REGISTRY_BY_ID } from '../../src/data/layerState.js';
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
import { createOsmPlacesLayer } from '../../src/layers/osmPlaces/index.js';
import { createWindLayer } from '../../src/layers/wind/index.js';
import { createApplicationLocalAdsb } from '../../src/app/layers/localAdsb.js';
import { radioLayer } from '../../src/data/radio.js';
import {
withRadioLayer,
radioStationRow,
} from '../../src/testSupport/radioLayerHarness.mjs';
import { radioTestEngine } from '../../src/testSupport/radioTestEngine.mjs';
import { loadLayerRenderers } from '../../src/app/layers/renderers.js';
import { createApplicationDirectionsLayer } from '../../src/data/directions.js';
import { createDirectionsDisplay } from '../../src/layers/directions/render/display.js';
// Each entry has a non-default retention exercise below. A new declaration
// needs an exercise; spelling a plausible field name alone cannot suppress QA.
const declarations = {
'src/layers/radio/index.js': ['selected', 'audioState'],
'src/layers/wind/index.js': ['reading'],
'src/layers/osmPlaces/index.js': ['enabled', 'results'],
'src/layers/localAdsb/index.js': ['enabled'],
'src/layers/directions/index.js': ['route'],
};
test('every declaration is covered by a real handoff exercise, without extra fields', () => {
const files = execFileSync(
'git',
['ls-files', '--cached', '--others', '--exclude-standard', 'src'],
{ encoding: 'utf8' },
)
.trim()
.split('\n');
const found = {};
for (const file of files.filter((file) => file.endsWith('.js'))) {
const text = readFileSync(file, 'utf8');
for (const match of text.matchAll(/sessionState\s*:\s*(\[[^\]]*\])/g)) {
assert.ok(!found[file], `Duplicate declaration in ${file}`);
found[file] = JSON.parse(match[1].replaceAll("'", '"'));
}
}
assert.deepEqual(found, declarations);
});
function verifyFields(layer, before, after) {
assert.deepEqual([...layer.sessionState].sort(), Object.keys(before).sort());
for (const field of layer.sessionState) {
assert.notEqual(before[field], undefined, field);
assert.deepEqual(
after[field],
before[field],
`${layer.id}.${field} survived detach/attach`,
);
assert.ok(
!optionSpecs(layer.id).some((spec) => spec.key === field),
`${field} is not a share option`,
);
}
}
test('radio selection and playback survive reattachment; volume is shareable', async () => {
await loadLayerRenderers('cesium');
const station = radioStationRow();
await withRadioLayer({ stations: [station] }, async () => {
radioLayer.selectStation(station.id, {
autoplay: true,
focus: false,
origin: 'user',
});
await new Promise((resolve) => setImmediate(resolve));
const state = () => {
const { selected, audioState } = radioLayer.getUIState();
return { selected, audioState };
};
const before = state();
assert.equal(before.selected.id, station.id);
assert.notEqual(before.audioState, 'idle');
radioLayer.detachRenderer();
radioLayer.attachRenderer({ engine: radioTestEngine() });
verifyFields(radioLayer, before, state());
assert.ok(optionSpecs('radio').some((spec) => spec.key === 'volume'));
});
});
test('wind inspection survives reattachment; pause is a share option', async () => {
const field = {
model: 'gfs',
grid: { nx: 2, ny: 2, lo1: 0, la1: 90, dx: 180, dy: 180 },
u: new Float32Array(4).fill(4),
v: new Float32Array(4).fill(3),
cycle: { validIso: '2026-11-02T12:00:00Z' },
};
const layer = createWindLayer({
feed: { getSnapshot: async () => field },
createNativeRendering: () => ({
attach() {},
start() {},
stop() {},
clear() {},
destroy() {},
setField() {},
getDiagnostics: () => ({}),
}),
});
try {
layer.init(null, { engine: createFakeEngine() });
layer.enable();
await layer.update();
layer.setParams({ inspect: true });
const before = { reading: layer.getRowControls().summary.reading };
assert.ok(before.reading);
layer.detachRenderer();
layer.attachRenderer({ engine: createFakeEngine() });
verifyFields(layer, before, {
reading: layer.getRowControls().summary.reading,
});
assert.ok(optionSpecs('wind').some((spec) => spec.key === 'paused'));
} finally {
layer.destroy();
}
});
test('OSM results and visibility survive reattachment but have no share registry entry', () => {
const host = { clearSource() {}, setEntries() {}, setVisible() {} };
const layer = createOsmPlacesLayer({
overlayHost: host,
overpassConfigured: async () => true,
});
const first = createFakeEngine(),
second = createFakeEngine();
const records = [
{ id: 'node/1', lat: 38.9, lon: -77.03, name: 'Fixture Clinic' },
];
try {
layer.init(null, { engine: first });
layer.setResults(records, { label: 'Clinics', area: 'Washington' });
layer.enable();
const before = {
enabled: first.movers.list()[0].shown,
results: layer.getAnalystRecords(),
};
assert.equal(before.enabled, true);
assert.equal(before.results.length, 1);
layer.detachRenderer();
layer.attachRenderer({ engine: second });
verifyFields(layer, before, {
enabled: second.movers.list()[0].shown,
results: layer.getAnalystRecords(),
});
assert.equal(second.movers.list()[0].count, 1);
assert.equal(REGISTRY_BY_ID.has(layer.id), false);
} finally {
layer.destroy();
first.destroy();
second.destroy();
}
});
test('local receiver visibility survives reattachment without restarting its session', async () => {
let starts = 0,
visible = false;
const layer = createApplicationLocalAdsb({
receiver: { getState: () => ({ mode: 'adsb', aircraft: [] }) },
feeds: { getState: () => ({ records: [] }), start: () => starts++ },
renderer: {
create: ({ engine }) => ({
engine,
attach() {},
detach() {},
destroy() {},
setVisible: (value) => {
visible = value;
},
view: { on: () => () => {} },
}),
},
});
const first = createFakeEngine(),
second = createFakeEngine();
try {
layer.init(null, { engine: first });
await layer.enable();
const before = { enabled: visible };
assert.equal(visible, true);
layer.detachRenderer();
visible = false;
layer.attachRenderer({ engine: second });
verifyFields(layer, before, { enabled: visible });
assert.equal(starts, 1);
assert.equal(REGISTRY_BY_ID.has(layer.id), false);
} finally {
layer.destroy();
first.destroy();
second.destroy();
}
});
test('directions retain the route and its markers; a cold layer has no route', async () => {
const geometry = [
[10.78, 55.32],
[10.8, 55.325],
[10.82, 55.33],
];
const layer = createApplicationDirectionsLayer({
source: {
getRoute: async () => ({
ok: true,
geometry,
distanceM: 9000,
durationS: 800,
steps: geometry.map(([lon, lat], i) => ({
lon,
lat,
instruction: `Step ${i}`,
distanceM: 3000,
durationS: 120,
})),
}),
},
groundFloor: {
GROUND_FLOOR_LIFT_M: 2,
cachedGroundFloor: () => 0,
warmGroundFloor() {},
},
rendererFactory: createDirectionsDisplay,
});
layer._setOverlayHostForTest({
setEntries() {},
setVisible() {},
clearSource() {},
});
const first = createFakeEngine(),
second = createFakeEngine();
try {
layer.init(null, { engine: first });
layer.enable();
const cold = layer.getRowControls().list;
layer.placeEndpoint('a', { lat: 55.32, lon: 10.78 });
layer.placeEndpoint('b', { lat: 55.33, lon: 10.82 });
await new Promise((resolve) => setImmediate(resolve));
const before = { route: layer.getRowControls().list };
assert.notDeepEqual(before.route, cold);
assert.equal(layer.getStats().count, 3);
layer.detachRenderer();
layer.attachRenderer({ engine: second });
verifyFields(layer, before, { route: layer.getRowControls().list });
assert.equal(
second.ground.list().find(({ id }) => id === 'directions-route').count,
1,
);
assert.equal(
second.movers.list().find(({ id }) => id === 'directions-markers').count,
2,
);
} finally {
layer.destroy();
first.destroy();
second.destroy();
}
});
+1
View File
@@ -1511,6 +1511,7 @@ export function createDirectionsLayer({
};
const directionsLayer = {
id: 'directions',
sessionState: ['route'],
name: 'Directions',
icon: '🧭',
source: 'OSM routing',
+1
View File
@@ -606,6 +606,7 @@ export function createLocalAdsbLayer({
const picks = { owner: LAYER_ID, predicate: (id) => markers.has(id) };
return {
id: LAYER_ID,
sessionState: ['enabled'],
name: LAYER_NAME,
icon: '📡',
source: LAYER_SOURCE,
+1
View File
@@ -72,6 +72,7 @@ export function createOsmPlacesLayer({
return {
id: OSM_PLACES_LAYER_ID,
sessionState: ['enabled', 'results'],
name: 'OSM Places',
icon: '⌖',
source: 'OpenStreetMap',
+1
View File
@@ -52,6 +52,7 @@ export function createRadioLayer({ services, source, rendererFactory }) {
parts.lifecycle.methods,
parts.ingestion?.methods,
{
sessionState: ['selected', 'audioState'],
radioGlobeLabel: parts.labels.radioGlobeLabel,
createRadioSelectedOverlayEntry:
parts.labels.createRadioSelectedOverlayEntry,
+1
View File
@@ -106,6 +106,7 @@ export function createWindLayer({
const unsubscribeClock = clock?.subscribe(notify);
const layer = {
id: 'wind',
sessionState: ['reading'],
name: 'Wind',
icon: '🌬',
source: 'GFS / ECMWF IFS · FORECAST',