mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'german/refactor-cold-start-fixes' into german/navara-refactor
This commit is contained in:
commit
88f434ded7
8 files changed
+587
-44
No files matched your search
@@ -11,6 +11,7 @@ import { drawLedger } from './qa/ledger.mjs';
|
||||
import {
|
||||
COLD_START_GROUPS,
|
||||
coldStartUrl,
|
||||
coldStartReady,
|
||||
compareColdStart,
|
||||
probeSnapshot,
|
||||
sessionExclusion,
|
||||
@@ -51,42 +52,26 @@ const evidence = (strict.extra.coldStart = {
|
||||
|
||||
async function settle(page, cold = false) {
|
||||
await whenSettled(page, { timeoutMs: 30000 });
|
||||
await page.evaluate(() => {
|
||||
window.__coldStartStable = null;
|
||||
});
|
||||
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;
|
||||
});
|
||||
},
|
||||
`(() => {
|
||||
if (!(${coldStartReady})(${JSON.stringify(layers)}, ${cold})) {
|
||||
window.__coldStartStable = null;
|
||||
return false;
|
||||
}
|
||||
const { engine } = window.__godsEyeView;
|
||||
const key = JSON.stringify(['movers', 'lines', 'ground', 'imagery', 'meshes']
|
||||
.flatMap((family) => engine[family].list().map(({ id, kind, count, shown }) =>
|
||||
[family, id, kind, count, shown])).sort());
|
||||
const now = performance.now();
|
||||
const last = window.__coldStartStable;
|
||||
if (!last || last.key !== key) window.__coldStartStable = { key, at: now };
|
||||
// Cover the 450 ms infrastructure walk and deferred feed/geometry publication.
|
||||
return last?.key === key && now - last.at >= 2000;
|
||||
})()`,
|
||||
{ timeout: 30000, polling: 100 },
|
||||
layers,
|
||||
cold,
|
||||
);
|
||||
await whenSettled(page, { frames: 5, timeoutMs: 30000 });
|
||||
}
|
||||
@@ -328,6 +313,26 @@ await strict.run({}, async (browser) => {
|
||||
sample.failureSnapshot = await probeSnapshot(page, layers).catch(
|
||||
() => null,
|
||||
);
|
||||
sample.readiness = await page
|
||||
?.evaluate(
|
||||
(layers) =>
|
||||
Object.fromEntries(
|
||||
layers.map((id) => {
|
||||
const module =
|
||||
window.__godsEyeView.dataManager.layers.get(id)?.module;
|
||||
return [
|
||||
id,
|
||||
{
|
||||
stats: module?.getStats?.(),
|
||||
loading: module?.getLoadingDiagnostics?.(),
|
||||
lod: module?.getLodDiagnostics?.(),
|
||||
},
|
||||
];
|
||||
}),
|
||||
),
|
||||
layers,
|
||||
)
|
||||
.catch(() => null);
|
||||
evidence.differences.push({
|
||||
layer: layers.join(', '),
|
||||
direction,
|
||||
|
||||
+116
-1
@@ -68,6 +68,71 @@ export function coldStartUrl(href, engine) {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
/** Feed completion alone does not mean its deferred display has been drawn. */
|
||||
export function coldStartReady(layers, cold = false) {
|
||||
const { dataManager, engine } = window.__godsEyeView;
|
||||
engine.render.request('qa-cold-start-readiness');
|
||||
const movers = engine.movers.list();
|
||||
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;
|
||||
if (id === 'local-dams' || id === 'local-datacenters')
|
||||
return (
|
||||
module.getLodDiagnostics().computed && engine.surface.tilesLoaded()
|
||||
);
|
||||
if (id === 'wildlife-sightings')
|
||||
return (
|
||||
movers.some((row) => row.id === id && row.count === 4) &&
|
||||
movers.some((row) => row.id === `${id}-density` && row.count === 3)
|
||||
);
|
||||
if (id === 'bhote-koshi-locator')
|
||||
return movers.some(
|
||||
(row) =>
|
||||
row.id === `${id}-bhote-koshi-incident-halo` &&
|
||||
row.count === 1 &&
|
||||
row.shown,
|
||||
);
|
||||
if (id === 'traffic')
|
||||
return (
|
||||
stats.count > 0 &&
|
||||
movers.some(
|
||||
(row) =>
|
||||
row.id === 'traffic-fill' && row.count === stats.count && row.shown,
|
||||
)
|
||||
);
|
||||
if (['flights', 'military', 'transit'].includes(id))
|
||||
return (
|
||||
stats.count > 0 &&
|
||||
movers.some(
|
||||
(row) => row.id.startsWith(id + '-') && row.count > 0 && row.shown,
|
||||
)
|
||||
);
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -168,19 +233,69 @@ export async function probeSnapshot(page, layers) {
|
||||
return { ...probe, ledger: await drawLedger(page) };
|
||||
}
|
||||
|
||||
// Display allocations are not content identities. Keep every slot and its
|
||||
// generation, but number these allocator-local sequences within each family.
|
||||
// Raw evidence retains the original IDs. A lost or extra slot still differs.
|
||||
export function normalizeAllocations(snapshot) {
|
||||
const patterns = [
|
||||
['imagery', /^(weather-)(\d+)$/],
|
||||
['imagery', /^(wind-field-)(\d+)$/],
|
||||
['imagery', /^(bhote-koshi-imagery-)(\d+)(-(?:left|right))$/],
|
||||
['lines', /^(satellites-orbit-)(\d+)$/],
|
||||
['movers', /^(satellites-points-)(\d+)(-\d+)$/],
|
||||
];
|
||||
const ids = new Map();
|
||||
for (const [family, pattern] of patterns) {
|
||||
const rows = snapshot.ledger.filter(
|
||||
(row) => row.family === family && pattern.test(row.id),
|
||||
);
|
||||
const sequence = [
|
||||
...new Set(rows.map((row) => Number(row.id.match(pattern)[2]))),
|
||||
].sort((a, b) => a - b);
|
||||
for (const row of rows) {
|
||||
const [, prefix, number, suffix = ''] = row.id.match(pattern);
|
||||
ids.set(
|
||||
`${family}:${row.id}`,
|
||||
`${prefix}${sequence.indexOf(Number(number)) + 1}${suffix}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const rename = (row, family) => ({
|
||||
...row,
|
||||
id: ids.get(`${family}:${row.id}`) ?? row.id,
|
||||
});
|
||||
return {
|
||||
...snapshot,
|
||||
ledger: snapshot.ledger.map((row) => rename(row, row.family)),
|
||||
movers: snapshot.movers.map((row) => rename(row, 'movers')),
|
||||
};
|
||||
}
|
||||
|
||||
/** Keep raw samples and declared exclusions beside the complete difference list. */
|
||||
export function compareColdStart(
|
||||
switched,
|
||||
cold,
|
||||
{ direction, owners = {}, allow = [], sessionState = true },
|
||||
) {
|
||||
const ownership = normalizeAllocations({
|
||||
ledger: Object.entries(owners).map(([key, layer]) => {
|
||||
const colon = key.indexOf(':');
|
||||
return { family: key.slice(0, colon), id: key.slice(colon + 1), layer };
|
||||
}),
|
||||
movers: [],
|
||||
});
|
||||
switched = normalizeAllocations(switched);
|
||||
cold = normalizeAllocations(cold);
|
||||
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 layerOf = (family, id) =>
|
||||
[...switched.ledger, ...cold.ledger, ...ownership.ledger].find(
|
||||
(row) => row.family === family && row.id === id && row.layer,
|
||||
)?.layer ?? '[page]';
|
||||
const ledger = compareLedgers(switched.ledger, cold.ledger, allow);
|
||||
for (const [side, rows] of [
|
||||
['switch', ledger.onlyBefore],
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
compareColdStart,
|
||||
sessionExclusion,
|
||||
probeSnapshot,
|
||||
normalizeAllocations,
|
||||
coldStartReady,
|
||||
} from './coldStart.mjs';
|
||||
|
||||
const snapshot = () => ({
|
||||
@@ -256,3 +258,153 @@ test('the production probe captures all requested fields and releases its frame
|
||||
assert.equal(engine.frames.postRender.numberOfListeners, listeners);
|
||||
engine.destroy();
|
||||
});
|
||||
|
||||
test('display allocation counters do not become content identities or hide lost slots', () => {
|
||||
const a = snapshot(),
|
||||
b = snapshot();
|
||||
for (const [family, left, right, layer] of [
|
||||
['imagery', 'weather-4', 'weather-1', 'weather-radar'],
|
||||
['imagery', 'wind-field-2', 'wind-field-1', 'wind'],
|
||||
[
|
||||
'imagery',
|
||||
'bhote-koshi-imagery-2-left',
|
||||
'bhote-koshi-imagery-1-left',
|
||||
'bhote-koshi-2026',
|
||||
],
|
||||
[
|
||||
'imagery',
|
||||
'bhote-koshi-imagery-2-right',
|
||||
'bhote-koshi-imagery-1-right',
|
||||
'bhote-koshi-2026',
|
||||
],
|
||||
['lines', 'satellites-orbit-5', 'satellites-orbit-4', 'satellites'],
|
||||
['movers', 'satellites-points-4-4', 'satellites-points-5-4', 'satellites'],
|
||||
]) {
|
||||
a.ledger.push({ family, id: left, layer, shown: true });
|
||||
b.ledger.push({ family, id: right, layer, shown: true });
|
||||
if (family === 'movers') {
|
||||
a.movers.push({ id: left, count: 10, capacity: 8192 });
|
||||
b.movers.push({ id: right, count: 10, capacity: 8192 });
|
||||
}
|
||||
}
|
||||
assert.deepEqual(compareColdStart(a, b, options).differences, []);
|
||||
assert.equal(
|
||||
a.ledger[1].id,
|
||||
'weather-4',
|
||||
'raw samples retain allocation IDs',
|
||||
);
|
||||
b.ledger.find((row) => row.id === 'weather-1').shown = false;
|
||||
b.ledger = b.ledger.filter((row) => row.id !== 'bhote-koshi-imagery-1-right');
|
||||
b.movers.at(-1).count--;
|
||||
const differences = compareColdStart(a, b, options).differences;
|
||||
assert.deepEqual(differences.map(({ field }) => field).sort(), [
|
||||
'count',
|
||||
'present',
|
||||
'shown',
|
||||
]);
|
||||
assert.ok(differences.every(({ layer }) => layer !== '[page]'));
|
||||
a.ledger.push({
|
||||
family: 'lines',
|
||||
id: 'satellites-orbit-7',
|
||||
layer: 'satellites',
|
||||
shown: false,
|
||||
});
|
||||
assert.equal(
|
||||
compareColdStart(a, b, options).differences.filter(
|
||||
({ family }) => family === 'lines',
|
||||
).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
normalizeAllocations(a).ledger.filter(({ family }) => family === 'lines')
|
||||
.length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
test('asynchronously created CCTV lines are attributed even when absent at enable', async () => {
|
||||
const engine = createFakeEngine();
|
||||
engine.lines.createLine({
|
||||
id: 'cctv-camera-ray-tl',
|
||||
widthPx: 1,
|
||||
color: [1, 1, 1, 1],
|
||||
});
|
||||
const context = vm.createContext({
|
||||
window: {
|
||||
__godsEyeView: {
|
||||
engine,
|
||||
dataManager: { layers: new Map([['cctv', { module: {} }]]) },
|
||||
},
|
||||
},
|
||||
});
|
||||
const page = {
|
||||
evaluate: async (code) =>
|
||||
vm.runInContext(typeof code === 'string' ? code : `(${code})()`, context),
|
||||
};
|
||||
const capture = await probeSnapshot(page, ['cctv']);
|
||||
assert.equal(capture.ledger[0].layer, 'cctv');
|
||||
engine.destroy();
|
||||
});
|
||||
|
||||
test('readiness waits for fixture publications, LOD and the locator reveal', () => {
|
||||
const engine = createFakeEngine();
|
||||
const dataManager = { layers: new Map() };
|
||||
const context = vm.createContext({
|
||||
window: { __godsEyeView: { engine, dataManager } },
|
||||
});
|
||||
const ready = (id) =>
|
||||
vm.runInContext(`(${coldStartReady})([${JSON.stringify(id)}])`, context);
|
||||
const entry = (id, extra = {}) =>
|
||||
dataManager.layers.set(id, {
|
||||
enabled: true,
|
||||
initialized: true,
|
||||
module: { getStats: () => ({ count: 10 }), ...extra },
|
||||
});
|
||||
entry('local-dams', { getLodDiagnostics: () => ({ computed: false }) });
|
||||
assert.equal(ready('local-dams'), false);
|
||||
dataManager.layers.get('local-dams').module.getLodDiagnostics = () => ({
|
||||
computed: true,
|
||||
});
|
||||
assert.equal(ready('local-dams'), true);
|
||||
entry('wildlife-sightings');
|
||||
const animals = engine.movers.createSet({
|
||||
id: 'wildlife-sightings',
|
||||
kind: 'point',
|
||||
capacity: 4,
|
||||
point: { sizePx: 4 },
|
||||
});
|
||||
animals.setCount(3);
|
||||
assert.equal(ready('wildlife-sightings'), false);
|
||||
animals.setCount(4);
|
||||
const density = engine.movers.createSet({
|
||||
id: 'wildlife-sightings-density',
|
||||
kind: 'point',
|
||||
capacity: 3,
|
||||
point: { sizePx: 4 },
|
||||
});
|
||||
density.setCount(3);
|
||||
assert.equal(ready('wildlife-sightings'), true);
|
||||
entry('bhote-koshi-locator');
|
||||
const halo = engine.movers.createSet({
|
||||
id: 'bhote-koshi-locator-bhote-koshi-incident-halo',
|
||||
kind: 'point',
|
||||
capacity: 1,
|
||||
point: { sizePx: 4 },
|
||||
});
|
||||
assert.equal(ready('bhote-koshi-locator'), false);
|
||||
halo.setCount(1);
|
||||
assert.equal(ready('bhote-koshi-locator'), true);
|
||||
entry('transit');
|
||||
const bus = engine.movers.createSet({
|
||||
id: 'transit-1',
|
||||
kind: 'point',
|
||||
capacity: 1,
|
||||
point: { sizePx: 4 },
|
||||
});
|
||||
bus.set(0, { show: false });
|
||||
bus.setCount(1);
|
||||
assert.equal(ready('transit'), false);
|
||||
bus.set(0, { show: true });
|
||||
assert.equal(ready('transit'), true);
|
||||
engine.destroy();
|
||||
});
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
} 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.
|
||||
// These are the feeds used by the strict switching scenarios. Feed timestamps
|
||||
// stay fixed; wall time must advance for visibility throttles and fade-in.
|
||||
export async function coldStartFixtures(origin, group) {
|
||||
const epoch =
|
||||
group === 'space'
|
||||
@@ -41,7 +41,8 @@ export async function coldStartFixtures(origin, group) {
|
||||
const routes = [];
|
||||
const fixtures = [];
|
||||
const route = (pattern, respond) => routes.push({ pattern, respond });
|
||||
if (group === 'wildlife') routes.push(...wildlifeRoutes(origin).routes);
|
||||
if (group === 'wildlife')
|
||||
routes.push(...wildlifeRoutes(origin, { now: () => epoch }).routes);
|
||||
if (group === 'open') {
|
||||
route('https://tiles.openseamap.org/*', image);
|
||||
route('https://www.opensnowmap.org/*', image);
|
||||
@@ -286,12 +287,15 @@ export async function coldStartFixtures(origin, group) {
|
||||
|
||||
function installFeeds({ group, epoch, groups, ghost, launches, firms }) {
|
||||
const RealDate = Date;
|
||||
const started = performance.now();
|
||||
const now = () =>
|
||||
group === 'space' ? epoch : epoch + performance.now() - started;
|
||||
globalThis.Date = class extends RealDate {
|
||||
constructor(...args) {
|
||||
super(...(args.length ? args : [epoch]));
|
||||
super(...(args.length ? args : [now()]));
|
||||
}
|
||||
static now() {
|
||||
return epoch;
|
||||
return Math.floor(now());
|
||||
}
|
||||
};
|
||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
||||
@@ -558,6 +562,18 @@ function installFeeds({ group, epoch, groups, ghost, launches, firms }) {
|
||||
return Promise.resolve(
|
||||
new Response(groups[path.split('/').pop()] ?? groups.stations),
|
||||
);
|
||||
if (path.startsWith('/api/celestrak-satcat/')) {
|
||||
const tle = groups[path.split('/').pop()];
|
||||
if (tle) {
|
||||
const lines = tle.trim().split('\n');
|
||||
const rows = [
|
||||
'OBJECT_NAME,NORAD_CAT_ID,OBJECT_TYPE,OPS_STATUS_CODE,OWNER',
|
||||
];
|
||||
for (let i = 0; i < lines.length; i += 3)
|
||||
rows.push(`${lines[i]},${Number(lines[i + 1].slice(2, 7))},PAY,+,US`);
|
||||
return Promise.resolve(new Response(rows.join('\n')));
|
||||
}
|
||||
}
|
||||
if (path === '/api/launches') return json(launches);
|
||||
if (path === '/api/firms') return json(firms);
|
||||
if (path === '/api/route') {
|
||||
@@ -588,7 +604,7 @@ function installFeeds({ group, epoch, groups, ghost, launches, firms }) {
|
||||
});
|
||||
// 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 (url.origin === location.origin && 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')
|
||||
|
||||
@@ -3,6 +3,11 @@ import test from 'node:test';
|
||||
import vm from 'node:vm';
|
||||
import { coldStartFixtures } from './coldStartFixtures.mjs';
|
||||
import { COLD_START_GROUPS } from './coldStart.mjs';
|
||||
import { createSatelliteSource } from '../../src/layers/satellites/source.js';
|
||||
import { parseSatcatCsv } from '../../src/layers/satellites/classes.js';
|
||||
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
|
||||
import { createTrafficDisplay } from '../../src/layers/traffic/render/display.js';
|
||||
import { createAnimation } from '../../src/layers/traffic/animation.js';
|
||||
|
||||
const origin = 'http://127.0.0.1:4309';
|
||||
test('every group has valid strict fixture routes', async () => {
|
||||
@@ -62,7 +67,8 @@ test('independent pages receive identical stationary aircraft and timestamps thr
|
||||
URL,
|
||||
Response,
|
||||
EventTarget,
|
||||
location: { href: origin },
|
||||
performance: { now: () => 0 },
|
||||
location: { href: origin, origin },
|
||||
localStorage: { setItem() {} },
|
||||
window: {
|
||||
EventSource: class {},
|
||||
@@ -95,3 +101,179 @@ test('independent pages receive identical stationary aircraft and timestamps thr
|
||||
'/api/unexpected-feed',
|
||||
);
|
||||
});
|
||||
|
||||
async function installedPage(group) {
|
||||
const fixture = await coldStartFixtures(origin, group);
|
||||
const scripts = [];
|
||||
await fixture.install({
|
||||
evaluateOnNewDocument: async (fn, args) => scripts.push({ fn, args }),
|
||||
});
|
||||
let elapsed = 0;
|
||||
const forwarded = [];
|
||||
const context = vm.createContext({
|
||||
URL,
|
||||
Response,
|
||||
EventTarget,
|
||||
performance: { now: () => elapsed },
|
||||
location: { href: origin, origin },
|
||||
localStorage: { setItem() {} },
|
||||
window: {
|
||||
EventSource: class {},
|
||||
async fetch(input) {
|
||||
const url = new URL(input.url ?? input, origin).href;
|
||||
forwarded.push(url);
|
||||
const route = fixture.routes.find(({ pattern }) =>
|
||||
new URLPattern(pattern).test(url),
|
||||
);
|
||||
assert.ok(route, url);
|
||||
const response = await route.respond({ url });
|
||||
return new Response(response.body, {
|
||||
status: response.status ?? 200,
|
||||
headers: { 'content-type': response.contentType },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const { fn, args } of scripts)
|
||||
vm.runInContext(`(${fn})(${JSON.stringify(args)})`, context);
|
||||
return {
|
||||
fixture,
|
||||
context,
|
||||
forwarded,
|
||||
advance: (ms) => {
|
||||
elapsed += ms;
|
||||
},
|
||||
fetch: (url) =>
|
||||
vm.runInContext(`window.fetch(${JSON.stringify(url)})`, context),
|
||||
};
|
||||
}
|
||||
|
||||
test('the page fetch guard delegates NASA snapshot APIs to their image fixture', async () => {
|
||||
const page = await installedPage('imagery');
|
||||
const response = await page.fetch(
|
||||
'https://wvs.earthdata.nasa.gov/api/v1/snapshot?WIDTH=256',
|
||||
);
|
||||
assert.equal(response.headers.get('content-type'), 'image/png');
|
||||
assert.ok((await response.arrayBuffer()).byteLength > 0);
|
||||
assert.equal(page.context.window.__coldStartUnexpectedApi.length, 0);
|
||||
await assert.rejects(page.fetch('/api/v1/snapshot'), /No cold-start fixture/);
|
||||
});
|
||||
|
||||
test('every TLE group has matching deterministic SATCAT records on both pages', async () => {
|
||||
const a = await installedPage('space'),
|
||||
b = await installedPage('space');
|
||||
for (const group of [
|
||||
'stations',
|
||||
'visual',
|
||||
'gps-ops',
|
||||
'glo-ops',
|
||||
'galileo',
|
||||
'geo',
|
||||
'starlink',
|
||||
'resource',
|
||||
'weather',
|
||||
'planet',
|
||||
'sar',
|
||||
'science',
|
||||
]) {
|
||||
const source = createSatelliteSource({ fetchImpl: a.fetch });
|
||||
const catalog = await source.readSatcat(group);
|
||||
assert.equal(catalog.ok, true);
|
||||
assert.equal(
|
||||
catalog.text,
|
||||
await (await b.fetch(`/api/celestrak-satcat/${group}`)).text(),
|
||||
);
|
||||
const tle = (await source.readGroup(group)).text;
|
||||
assert.equal(
|
||||
parseSatcatCsv(catalog.text).size,
|
||||
tle.trim().split('\n').length / 3,
|
||||
);
|
||||
}
|
||||
assert.equal(a.context.window.__coldStartUnexpectedApi.length, 0);
|
||||
});
|
||||
|
||||
test('wall time advances for fades and visibility while the orbit epoch stays fixed', async () => {
|
||||
for (const group of ['aircraft', 'traffic', 'space']) {
|
||||
const page = await installedPage(group);
|
||||
const before = vm.runInContext('Date.now()', page.context);
|
||||
page.advance(2000);
|
||||
assert.equal(
|
||||
vm.runInContext('Date.now()', page.context) - before,
|
||||
group === 'space' ? 0 : 2000,
|
||||
);
|
||||
assert.equal(
|
||||
vm.runInContext('+new Date()', page.context),
|
||||
vm.runInContext('Date.now()', page.context),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('traffic uses deterministic road and flow tiles', async () => {
|
||||
const page = await installedPage('traffic');
|
||||
assert.equal(
|
||||
(await (await page.fetch('/api/tomtom/status')).json()).hasKey,
|
||||
true,
|
||||
);
|
||||
for (const url of [
|
||||
'https://tiles.openfreemap.org/qa-traffic/14/3743/6745.pbf',
|
||||
'/api/tomtom/flow/12/935/1686.pbf',
|
||||
])
|
||||
assert.ok((await (await page.fetch(url)).arrayBuffer()).byteLength > 0);
|
||||
});
|
||||
|
||||
test('traffic dots finish their birth fade with the fixture clock', async (t) => {
|
||||
const page = await installedPage('traffic');
|
||||
t.mock.method(Date, 'now', () => vm.runInContext('Date.now()', page.context));
|
||||
const engine = createFakeEngine();
|
||||
const display = createTrafficDisplay(engine);
|
||||
t.after(() => {
|
||||
display.destroy();
|
||||
engine.destroy();
|
||||
});
|
||||
const point = {
|
||||
position: { x: 6378137, y: 0, z: 0 },
|
||||
show: true,
|
||||
pixelSize: 6,
|
||||
color: { red: 1, green: 1, blue: 1, alpha: 1 },
|
||||
outlineWidth: 0,
|
||||
};
|
||||
const dot = {
|
||||
point,
|
||||
born: Date.now(),
|
||||
nominalSize: 6,
|
||||
direction: 1,
|
||||
segIdx: 0,
|
||||
numSegments: 1,
|
||||
t: 0.5,
|
||||
mps: 1,
|
||||
stoppedUntil: 0,
|
||||
segmentDist: [1000],
|
||||
waypoints: [point.position, { x: 6378137, y: 1000, z: 0 }],
|
||||
};
|
||||
const animation = createAnimation({
|
||||
state: {
|
||||
_enabled: true,
|
||||
_renderHeld: true,
|
||||
_engine: engine,
|
||||
_display: display,
|
||||
_dots: [dot],
|
||||
_lastAnimTime: Date.now(),
|
||||
_animFrame: 0,
|
||||
},
|
||||
services: {},
|
||||
parts: { rendering: {}, retention: { easeHeights() {} } },
|
||||
});
|
||||
animation.animate();
|
||||
animation.drawDots();
|
||||
assert.equal(
|
||||
engine.movers.list().find(({ id }) => id === 'traffic-fill').count,
|
||||
0,
|
||||
);
|
||||
page.advance(200);
|
||||
animation.animate();
|
||||
animation.drawDots();
|
||||
assert.equal(
|
||||
engine.movers.list().find(({ id }) => id === 'traffic-fill').count,
|
||||
1,
|
||||
);
|
||||
});
|
||||
+14
-1
@@ -1,12 +1,25 @@
|
||||
/** The five public drawing registries, with no SDK objects or volatile fields. */
|
||||
export function drawLedger(page) {
|
||||
return page.evaluate(() => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
const { engine, dataManager } = window.__godsEyeView;
|
||||
const owners = [...(dataManager?.layers.keys() ?? [])].sort(
|
||||
(a, b) => b.length - a.length,
|
||||
);
|
||||
const ownerOf = (id) =>
|
||||
owners.find((owner) => id === owner || id.startsWith(owner + '-')) ??
|
||||
(id.startsWith('wind-field-') ? 'wind' : undefined) ??
|
||||
(id.startsWith('bhote-koshi-imagery-') ||
|
||||
id.startsWith('bhote-koshi-flood-')
|
||||
? 'bhote-koshi-2026'
|
||||
: undefined);
|
||||
return ['movers', 'lines', 'ground', 'imagery', 'meshes']
|
||||
.flatMap((family) =>
|
||||
engine[family].list().map((row) => ({
|
||||
id: String(row.id),
|
||||
family,
|
||||
...(ownerOf(String(row.id))
|
||||
? { layer: ownerOf(String(row.id)) }
|
||||
: {}),
|
||||
...(row.kind === undefined ? {} : { kind: row.kind }),
|
||||
...(row.count === undefined ? {} : { count: row.count }),
|
||||
shown: row.shown ?? true,
|
||||
|
||||
@@ -78,6 +78,7 @@ export function createInfrastructureSessionLayer(
|
||||
request = null,
|
||||
abort = null,
|
||||
display = null,
|
||||
sceneContext = null,
|
||||
enabled = false,
|
||||
destroyed = false;
|
||||
let updated = null,
|
||||
@@ -110,6 +111,8 @@ export function createInfrastructureSessionLayer(
|
||||
try {
|
||||
const next = await read();
|
||||
if (!enabled || destroyed) return;
|
||||
if (sceneContext?.viewReady === false || sceneContext?.signal?.aborted)
|
||||
return;
|
||||
const active = display;
|
||||
if (active) {
|
||||
error = null;
|
||||
@@ -138,8 +141,10 @@ export function createInfrastructureSessionLayer(
|
||||
init(_viewer, options = {}) {
|
||||
if (options.engine) return this.attachRenderer(options);
|
||||
},
|
||||
attachRenderer({ engine = null, signal = null } = {}) {
|
||||
attachRenderer(options = {}) {
|
||||
const { engine = null, signal = null } = options;
|
||||
if (display) throw Error('Infrastructure display already attached');
|
||||
sceneContext = options;
|
||||
display = engine ? createDisplay(engine, config, services) : null;
|
||||
// The dataset load belongs to the session and keeps running; only this
|
||||
// scene's wait for it ends when the scene is released. A released
|
||||
@@ -149,6 +154,10 @@ export function createInfrastructureSessionLayer(
|
||||
if (!signal?.aborted) throw error;
|
||||
});
|
||||
},
|
||||
resumeViewport() {
|
||||
// LOD keeps incumbents, so its first walk must use the restored camera.
|
||||
if (enabled && display) return show();
|
||||
},
|
||||
detachRenderer() {
|
||||
if (!display) return;
|
||||
const selected = services.getSelectedEntityContext?.();
|
||||
@@ -158,6 +167,7 @@ export function createInfrastructureSessionLayer(
|
||||
: null;
|
||||
display.destroy();
|
||||
display = null;
|
||||
sceneContext = null;
|
||||
},
|
||||
enable() {
|
||||
enabled = true;
|
||||
@@ -196,6 +206,7 @@ export function createInfrastructureSessionLayer(
|
||||
abort?.abort();
|
||||
display?.destroy();
|
||||
display = data = null;
|
||||
sceneContext = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,55 @@ import {
|
||||
selectEntityContext,
|
||||
} from '../../data/contextStore.js';
|
||||
import { layerFeedState } from '../../data/feedState.js';
|
||||
import { createLayerSceneContext } from '../../data/layerSceneBinding.js';
|
||||
|
||||
test('reattached infrastructure first computes LOD at the restored camera', async (t) => {
|
||||
t.mock.method(globalThis, 'fetch', async () => ({
|
||||
ok: true,
|
||||
text: async () =>
|
||||
JSON.stringify({
|
||||
type: 'Feature',
|
||||
id: 'dam',
|
||||
properties: {},
|
||||
geometry: { type: 'Point', coordinates: [10, 20] },
|
||||
}),
|
||||
}));
|
||||
const draws = [];
|
||||
const layer = createInfrastructureSessionLayer(
|
||||
{ id: 'local-dams', url: '/dams' },
|
||||
{},
|
||||
(engine) => ({
|
||||
show: () => draws.push(engine.camera.height()),
|
||||
hide() {},
|
||||
destroy() {},
|
||||
}),
|
||||
);
|
||||
const engine = createFakeEngine();
|
||||
t.after(() => {
|
||||
layer.destroy();
|
||||
engine.destroy();
|
||||
});
|
||||
layer.init(null, { engine });
|
||||
await layer.enable();
|
||||
layer.detachRenderer();
|
||||
draws.length = 0;
|
||||
let restore;
|
||||
const context = createLayerSceneContext({
|
||||
engine,
|
||||
viewRestored: new Promise((resolve) => {
|
||||
restore = resolve;
|
||||
}),
|
||||
});
|
||||
await layer.attachRenderer(context);
|
||||
engine.fake.frame();
|
||||
assert.deepEqual(draws, [], 'the incoming default view never seeds LOD');
|
||||
engine.fake.setPose({ height: 20000 });
|
||||
restore();
|
||||
await context.viewRestored;
|
||||
await layer.resumeViewport();
|
||||
assert.equal(draws.length, 1);
|
||||
assert.ok(Math.abs(draws[0] - 20000) < 1e-6);
|
||||
});
|
||||
|
||||
test('a bundled file delivered between planets keeps multipart IDs and is read only once', async (t) => {
|
||||
let deliver,
|
||||
|
||||
Reference in new issue
Block a user