mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
405 lines
16 KiB
JavaScript
405 lines
16 KiB
JavaScript
// src/data/earthquakes.test.mjs
|
|
// Focused tests for the pure analyst-record mapper (analyst query engine seam).
|
|
// Pure function — no viewer/DOM needed; imported directly.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import * as Cesium from 'cesium';
|
|
import {
|
|
EARTHQUAKE_OVERLAY_COHORT_LIMIT,
|
|
EARTHQUAKE_OVERLAY_COLLISION_CAPACITY,
|
|
createEarthquakeOverlayEntry,
|
|
createEarthquakesLayer,
|
|
mapAnalystRecord,
|
|
selectEarthquakeOverlayCohort,
|
|
} from './earthquakes.js';
|
|
import { DataLayerManager } from './manager.js';
|
|
import {
|
|
getRenderGovernorDiagnostics,
|
|
installRenderGovernor,
|
|
_resetRenderGovernorForTest,
|
|
} from '../renderGovernor.js';
|
|
|
|
const FULL_RAW = {
|
|
id: 'us7000abcd',
|
|
mag: 5.2,
|
|
place: '42 km SW of Anchorage, Alaska',
|
|
time: 1_753_600_000_000,
|
|
depth: 41.7,
|
|
lat: 61.02,
|
|
lon: -150.41,
|
|
};
|
|
|
|
test('earthquake analyst record: full record maps every contract field', () => {
|
|
const r = mapAnalystRecord(FULL_RAW, 3);
|
|
assert.deepEqual(r, {
|
|
id: 'us7000abcd',
|
|
magnitude: 5.2,
|
|
depthKm: 41.7,
|
|
lat: 61.02,
|
|
lon: -150.41,
|
|
timeMs: 1_753_600_000_000,
|
|
place: '42 km SW of Anchorage, Alaska',
|
|
});
|
|
});
|
|
|
|
test('earthquake analyst record: missing USGS id falls back to index-based id', () => {
|
|
assert.equal(mapAnalystRecord({ ...FULL_RAW, id: null }, 3).id, 'QUAKE-0003');
|
|
assert.equal(mapAnalystRecord({ ...FULL_RAW, id: ' ' }, 41).id, 'QUAKE-0041');
|
|
assert.equal(mapAnalystRecord(undefined).id, 'QUAKE-0000');
|
|
});
|
|
|
|
test('earthquake analyst record: missing fields become null, never NaN/undefined', () => {
|
|
const r = mapAnalystRecord({ id: 'us1', mag: NaN, depth: undefined, place: '' }, 0);
|
|
assert.equal(r.magnitude, null);
|
|
assert.equal(r.depthKm, null);
|
|
assert.equal(r.place, null);
|
|
for (const [key, value] of Object.entries(r)) {
|
|
assert.notEqual(value, undefined, `${key} must not be undefined`);
|
|
if (typeof value === 'number') assert.ok(Number.isFinite(value), `${key} must not be NaN`);
|
|
}
|
|
});
|
|
|
|
test('earthquake analyst record: output is JSON-safe (no Cesium types)', () => {
|
|
const r = mapAnalystRecord(FULL_RAW, 0);
|
|
assert.deepEqual(JSON.parse(JSON.stringify(r)), r);
|
|
});
|
|
|
|
test('earthquake overlay copy keeps source-side magnitude formatting and bounded priority', () => {
|
|
const position = Cesium.Cartesian3.fromDegrees(-150.41, 61.02);
|
|
const entry = createEarthquakeOverlayEntry({
|
|
id: 'us7000abcd',
|
|
position,
|
|
magnitude: 5.24,
|
|
accent: '#ff0000',
|
|
});
|
|
assert.equal(entry.title, 'M5.2');
|
|
assert.equal(entry.position, position);
|
|
assert.equal(entry.variant, 'label');
|
|
assert.equal(entry.paintLane, 'ambient-label');
|
|
assert.equal(entry.collisionGroup, 'ambient-label');
|
|
assert.equal(entry.protected, undefined);
|
|
assert.equal(entry.edgeFade, 'keyhole');
|
|
assert.equal(entry.horizonCull, true);
|
|
|
|
const entries = Array.from({ length: EARTHQUAKE_OVERLAY_COHORT_LIMIT + 20 }, (_, index) => ({
|
|
id: `quake-${String(index).padStart(3, '0')}`,
|
|
priority: index,
|
|
}));
|
|
const cohort = selectEarthquakeOverlayCohort(entries);
|
|
assert.equal(cohort.length, EARTHQUAKE_OVERLAY_COHORT_LIMIT);
|
|
assert.equal(cohort[0].id, `quake-${EARTHQUAKE_OVERLAY_COHORT_LIMIT + 19}`);
|
|
assert.equal(cohort.at(-1).id, 'quake-020');
|
|
});
|
|
|
|
test('real earthquake lifecycle publishes host labels while runtime entities carry no label graphic', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const hostCalls = [];
|
|
const dataSources = [];
|
|
const overlayHost = {
|
|
setEntries: (...args) => hostCalls.push(['entries', ...args]),
|
|
setVisible: (...args) => hostCalls.push(['visible', ...args]),
|
|
clearSource: (...args) => hostCalls.push(['clear', ...args]),
|
|
};
|
|
const viewer = {
|
|
dataSources: {
|
|
add(dataSource) { dataSources.push(dataSource); return dataSource; },
|
|
remove(dataSource) {
|
|
const index = dataSources.indexOf(dataSource);
|
|
if (index >= 0) dataSources.splice(index, 1);
|
|
return index >= 0;
|
|
},
|
|
},
|
|
};
|
|
globalThis.fetch = async () => ({
|
|
ok: true,
|
|
json: async () => ({
|
|
features: [
|
|
{
|
|
id: 'us-runtime-1',
|
|
geometry: { coordinates: [-150.41, 61.02, 41.7] },
|
|
properties: { mag: 5.24, place: 'Runtime One', time: 1_753_600_000_000 },
|
|
},
|
|
{
|
|
id: 'us-runtime-2',
|
|
geometry: { coordinates: [139.7, 35.6, 310] },
|
|
properties: { mag: 3.01, place: 'Runtime Two', time: 1_753_600_100_000 },
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
const layer = createEarthquakesLayer({ overlayHost });
|
|
try {
|
|
layer.init(viewer);
|
|
layer.enable(viewer);
|
|
await layer.update(viewer);
|
|
|
|
const entities = dataSources[0].entities.values;
|
|
assert.equal(entities.length, 2, 'runtime guard requires populated real source entities');
|
|
assert.ok(entities.every((entity) => entity.label === undefined));
|
|
const publication = hostCalls.find(([type]) => type === 'entries');
|
|
assert.ok(publication, 'real update path must publish the overlay source');
|
|
assert.deepEqual(publication[2].map(({ title }) => title), ['M5.2', 'M3.0']);
|
|
assert.deepEqual(publication[3], {
|
|
cohortLimit: EARTHQUAKE_OVERLAY_COHORT_LIMIT,
|
|
collisionCapacity: EARTHQUAKE_OVERLAY_COLLISION_CAPACITY,
|
|
moving: false,
|
|
});
|
|
|
|
layer.disable(viewer);
|
|
assert.equal(dataSources[0].show, false);
|
|
assert.deepEqual(hostCalls.slice(-2), [
|
|
['clear', 'earthquakes'],
|
|
['visible', 'earthquakes', false],
|
|
]);
|
|
layer.destroy(viewer);
|
|
assert.equal(dataSources.length, 0);
|
|
assert.deepEqual(hostCalls.slice(-2), [
|
|
['clear', 'earthquakes'],
|
|
['visible', 'earthquakes', false],
|
|
]);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
// ── Perf pin: the 2026-08-20 earthquakes frame-rate cliff ────────────────────
|
|
// Every disc is a CLAMP_TO_GROUND ellipse. When its axes were a
|
|
// `CallbackProperty`, Cesium re-tessellated all 58 ground primitives on EVERY
|
|
// frame: 32.4 ms/frame and 30 fps for 58 contacts, against 1.4 ms/60 fps with
|
|
// the layer off. Static axes restored 60 fps. Both halves of the fix are
|
|
// pinned — the runtime property shape, and the source-level guards.
|
|
test('quake disc axes are STATIC — a per-frame callback re-tessellates ground geometry', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const dataSources = [];
|
|
const viewer = {
|
|
dataSources: {
|
|
add(dataSource) { dataSources.push(dataSource); return dataSource; },
|
|
remove() { return true; },
|
|
},
|
|
};
|
|
const layer = createEarthquakesLayer({
|
|
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
|
|
});
|
|
globalThis.fetch = async () => ({
|
|
ok: true,
|
|
json: async () => ({
|
|
features: [{
|
|
id: 'us-static-1',
|
|
geometry: { coordinates: [-122.4, 37.79, 8.2] },
|
|
properties: { mag: 5.5, place: 'Static One', time: 1_753_600_000_000 },
|
|
}],
|
|
}),
|
|
});
|
|
try {
|
|
layer.init(viewer);
|
|
layer.enable(viewer);
|
|
await layer.update(viewer);
|
|
|
|
const [entity] = dataSources[0].entities.values;
|
|
assert.ok(entity.ellipse, 'the disc is an ellipse graphic');
|
|
for (const axis of ['semiMajorAxis', 'semiMinorAxis']) {
|
|
const property = entity.ellipse[axis];
|
|
assert.equal(
|
|
property instanceof Cesium.CallbackProperty,
|
|
false,
|
|
`${axis} must not be a CallbackProperty — it rebuilds the ground primitive every frame`,
|
|
);
|
|
assert.equal(property.isConstant, true, `${axis} must be a constant property`);
|
|
// Magnitude 5.5 → 2^5.5 * 1000 m, unchanged by the dropped pulse.
|
|
assert.equal(property.getValue(Cesium.JulianDate.now()), Math.pow(2, 5.5) * 1000);
|
|
}
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
layer.destroy(viewer);
|
|
}
|
|
});
|
|
|
|
// Dropping the continuous-render hold is only safe if new poll data still reaches
|
|
// the screen. In idle mode nothing repaints on its own, so the manager's one-shot
|
|
// request after each tick is now load-bearing for this layer. Wires the REAL layer
|
|
// into the REAL manager with the governor installed, rather than trusting the
|
|
// arrangement by inspection.
|
|
test('a quake poll still reaches the screen with the render loop idle', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const renderRequests = [];
|
|
const dataSources = [];
|
|
const viewer = {
|
|
scene: { requestRenderMode: false, requestRender: () => renderRequests.push(Date.now()) },
|
|
dataSources: {
|
|
add(dataSource) { dataSources.push(dataSource); return dataSource; },
|
|
remove() { return true; },
|
|
},
|
|
};
|
|
const layer = createEarthquakesLayer({
|
|
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
|
|
});
|
|
globalThis.fetch = async () => ({
|
|
ok: true,
|
|
json: async () => ({
|
|
features: [{
|
|
id: 'us-idle-1',
|
|
geometry: { coordinates: [-122.4, 37.79, 8.2] },
|
|
properties: { mag: 4.2, place: 'Idle One', time: 1_753_600_000_000 },
|
|
}],
|
|
}),
|
|
});
|
|
|
|
_resetRenderGovernorForTest();
|
|
installRenderGovernor(viewer);
|
|
// Installing the governor enters idle mode, which itself paints one settling
|
|
// frame. Drop that from the baseline or the enable assertion below can pass on
|
|
// the install alone, without the manager ever requesting anything.
|
|
renderRequests.length = 0;
|
|
const manager = new DataLayerManager(viewer);
|
|
// updateInterval -1 keeps the poll loop unarmed; we drive one tick by hand.
|
|
manager.register({ ...layer, updateInterval: -1 });
|
|
try {
|
|
await manager.setEnabled('earthquakes', true, { origin: 'test' });
|
|
|
|
// The whole point of the change: enabling quakes must not pin the loop on.
|
|
assert.equal(getRenderGovernorDiagnostics().mode, 'idle', 'quakes must not force continuous render');
|
|
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
|
|
assert.equal(viewer.scene.requestRenderMode, true, 'the governor really is in idle mode');
|
|
|
|
// ...and the poll that populated the discs must still have asked for a frame,
|
|
// because in idle mode nothing repaints on its own.
|
|
assert.ok(dataSources[0].entities.values.length > 0, 'the enable poll produced discs');
|
|
assert.ok(renderRequests.length > 0, 'enabling the layer must request a render in idle mode');
|
|
// Named, not merely counted: the frame has to come from the manager's
|
|
// visibility request, not from some incidental repaint.
|
|
assert.ok(
|
|
getRenderGovernorDiagnostics().recentRequests.some(({ reason }) => reason === 'layer-visibility'),
|
|
'the enable frame must be the manager\'s layer-visibility request',
|
|
);
|
|
|
|
// A LATER poll must request its own frame too — the enable-time
|
|
// 'layer-visibility' request cannot cover refreshes that arrive minutes later.
|
|
const beforeRefresh = renderRequests.length;
|
|
await manager._runPeriodicUpdate('earthquakes', manager.layers.get('earthquakes'));
|
|
assert.ok(
|
|
renderRequests.length > beforeRefresh,
|
|
'each refresh tick must request its own render while the loop is idle',
|
|
);
|
|
const reasons = getRenderGovernorDiagnostics().recentRequests.map(({ reason }) => reason);
|
|
assert.ok(
|
|
reasons.includes('layer-tick:earthquakes'),
|
|
`the tick request must be attributed to the layer, got ${JSON.stringify(reasons)}`,
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
await manager.setEnabled('earthquakes', false, { origin: 'test' }).catch(() => {});
|
|
_resetRenderGovernorForTest();
|
|
}
|
|
});
|
|
|
|
test('the earthquakes layer installs no per-frame callback and no continuous-render hold', () => {
|
|
const source = ['index', 'model', 'source'].map(name =>
|
|
readFileSync(new URL(`../layers/earthquakes/${name}.js`, import.meta.url), 'utf8')).join('\n');
|
|
assert.doesNotMatch(
|
|
source,
|
|
/new Cesium\.CallbackProperty/,
|
|
'reverting the discs to a per-frame axis callback must fail this pin',
|
|
);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/holdContinuousRender/,
|
|
'static discs have no per-frame animator, so the layer must not pin the render loop on',
|
|
);
|
|
});
|
|
|
|
test('earthquake refresh reports failure and clears it only after a successful response', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const dataSources = [];
|
|
const viewer = {
|
|
dataSources: {
|
|
add(dataSource) { dataSources.push(dataSource); return dataSource; },
|
|
remove() { return true; },
|
|
},
|
|
};
|
|
const layer = createEarthquakesLayer({
|
|
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
|
|
});
|
|
try {
|
|
layer.init(viewer);
|
|
layer.enable(viewer);
|
|
globalThis.fetch = async () => ({ ok: false, status: 503 });
|
|
assert.equal(await layer.update(viewer), false);
|
|
assert.equal(layer.getStats().error, 'USGS HTTP 503');
|
|
|
|
globalThis.fetch = async () => ({
|
|
ok: true,
|
|
json: async () => ({ features: [] }),
|
|
});
|
|
assert.equal(await layer.update(viewer), true);
|
|
assert.equal(layer.getStats().error, null);
|
|
assert.ok(Number.isFinite(layer.getStats().lastUpdate));
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
layer.destroy(viewer);
|
|
}
|
|
});
|
|
|
|
test('malformed earthquake refresh preserves entities, overlays, count and timestamp', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const dataSources = [];
|
|
const publications = [];
|
|
const viewer = { dataSources: {
|
|
add(source) { dataSources.push(source); }, remove() { return true; },
|
|
} };
|
|
const layer = createEarthquakesLayer({ overlayHost: {
|
|
setEntries(...args) { publications.push(args); }, setVisible() {}, clearSource() {},
|
|
} });
|
|
const good = { id: 'good', geometry: { type: 'Point', coordinates: [10, 20, 5] },
|
|
properties: { mag: 4, time: 100, place: 'Fixture' } };
|
|
const respond = (features) => { globalThis.fetch = async () => ({ ok: true, json: async () => ({ features }) }); };
|
|
try {
|
|
layer.init(viewer);
|
|
layer.enable(viewer);
|
|
respond([good]);
|
|
assert.equal(await layer.update(viewer), true);
|
|
const entity = dataSources[0].entities.values[0];
|
|
const stats = layer.getStats();
|
|
for (const properties of ['invalid', 123, [], true]) {
|
|
const bad = { ...good, id: 'bad-properties', properties };
|
|
for (const features of [[bad], [good, bad]]) {
|
|
respond(features);
|
|
assert.equal(await layer.update(viewer), false);
|
|
assert.equal(dataSources[0].entities.values.length, 1);
|
|
assert.equal(dataSources[0].entities.values[0], entity);
|
|
assert.equal(layer.getStats().count, stats.count);
|
|
assert.equal(layer.getStats().lastUpdate, stats.lastUpdate);
|
|
assert.equal(layer.getStats().error, 'Malformed USGS response');
|
|
assert.equal(publications.length, 1);
|
|
}
|
|
}
|
|
for (const bad of [null, { ...good, geometry: null },
|
|
{ ...good, geometry: { type: 'LineString', coordinates: [1, 2] } },
|
|
{ ...good, geometry: { coordinates: [181, 20] } },
|
|
{ ...good, geometry: { coordinates: [10, -91] } },
|
|
{ ...good, geometry: { coordinates: [null, 20] } },
|
|
{ ...good, properties: { mag: '4' } },
|
|
{ ...good, properties: { mag: Infinity } }, good]) {
|
|
respond([good, bad]);
|
|
assert.equal(await layer.update(viewer), false);
|
|
assert.equal(dataSources[0].entities.values.length, 1);
|
|
assert.equal(dataSources[0].entities.values[0], entity);
|
|
assert.equal(layer.getStats().count, stats.count);
|
|
assert.equal(layer.getStats().lastUpdate, stats.lastUpdate);
|
|
assert.equal(layer.getStats().error, 'Malformed USGS response');
|
|
assert.equal(publications.length, 1);
|
|
}
|
|
respond([{ ...good, properties: { mag: null } }]);
|
|
assert.equal(await layer.update(viewer), true);
|
|
assert.equal(dataSources[0].entities.values.length, 0);
|
|
assert.equal(layer.getStats().error, null);
|
|
assert.equal(publications.at(-1)[1].length, 0);
|
|
respond([good]);
|
|
assert.equal(await layer.update(viewer), true);
|
|
assert.equal(dataSources[0].entities.values.length, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
layer.destroy(viewer);
|
|
}
|
|
});
|