mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Make the cold-start comparison wait for the same data and attribute it
Both pages now get the same deterministic SATCAT, NASA image and wildlife data and advance animation clocks alike; the scenario waits until each display has published and the registries stay unchanged for two seconds, and late-created content is attributed to its registered owner. Known allocation counters are normalised; every slot, count and visibility is still compared. Infrastructure drew on attach before the camera was restored, seeding its level of detail at the incoming engine's default view; it now draws when the view is restored.
This commit is contained in:
1 parent
555906da6b
commit
705b7a5314
8 files changed
+587
-44
No files matched your search
@@ -11,6 +11,7 @@ import { drawLedger } from './qa/ledger.mjs';
|
|||||||
import {
|
import {
|
||||||
COLD_START_GROUPS,
|
COLD_START_GROUPS,
|
||||||
coldStartUrl,
|
coldStartUrl,
|
||||||
|
coldStartReady,
|
||||||
compareColdStart,
|
compareColdStart,
|
||||||
probeSnapshot,
|
probeSnapshot,
|
||||||
sessionExclusion,
|
sessionExclusion,
|
||||||
@@ -51,42 +52,26 @@ const evidence = (strict.extra.coldStart = {
|
|||||||
|
|
||||||
async function settle(page, cold = false) {
|
async function settle(page, cold = false) {
|
||||||
await whenSettled(page, { timeoutMs: 30000 });
|
await whenSettled(page, { timeoutMs: 30000 });
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.__coldStartStable = null;
|
||||||
|
});
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(layers, cold) => {
|
`(() => {
|
||||||
const { dataManager, engine } = window.__godsEyeView;
|
if (!(${coldStartReady})(${JSON.stringify(layers)}, ${cold})) {
|
||||||
engine.render.request('qa-cold-start-readiness');
|
window.__coldStartStable = null;
|
||||||
return layers.every((id) => {
|
return false;
|
||||||
const entry = dataManager.layers.get(id),
|
}
|
||||||
module = entry?.module;
|
const { engine } = window.__godsEyeView;
|
||||||
if (!entry) return false;
|
const key = JSON.stringify(['movers', 'lines', 'ground', 'imagery', 'meshes']
|
||||||
if (cold && module.sessionState?.includes('enabled'))
|
.flatMap((family) => engine[family].list().map(({ id, kind, count, shown }) =>
|
||||||
return !entry.enabled;
|
[family, id, kind, count, shown])).sort());
|
||||||
if (
|
const now = performance.now();
|
||||||
!entry.enabled ||
|
const last = window.__coldStartStable;
|
||||||
!entry.initialized ||
|
if (!last || last.key !== key) window.__coldStartStable = { key, at: now };
|
||||||
entry.sceneDetached ||
|
// Cover the 450 ms infrastructure walk and deferred feed/geometry publication.
|
||||||
entry.refreshing
|
return last?.key === key && now - last.at >= 2000;
|
||||||
)
|
})()`,
|
||||||
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 },
|
{ timeout: 30000, polling: 100 },
|
||||||
layers,
|
|
||||||
cold,
|
|
||||||
);
|
);
|
||||||
await whenSettled(page, { frames: 5, timeoutMs: 30000 });
|
await whenSettled(page, { frames: 5, timeoutMs: 30000 });
|
||||||
}
|
}
|
||||||
@@ -328,6 +313,26 @@ await strict.run({}, async (browser) => {
|
|||||||
sample.failureSnapshot = await probeSnapshot(page, layers).catch(
|
sample.failureSnapshot = await probeSnapshot(page, layers).catch(
|
||||||
() => null,
|
() => 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({
|
evidence.differences.push({
|
||||||
layer: layers.join(', '),
|
layer: layers.join(', '),
|
||||||
direction,
|
direction,
|
||||||
|
|||||||
+116
-1
@@ -68,6 +68,71 @@ export function coldStartUrl(href, engine) {
|
|||||||
return url.href;
|
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.
|
// Share addresses round the camera to 0.0001 degrees and whole metres/degrees.
|
||||||
// World positions must still match within a metre; projected positions allow
|
// World positions must still match within a metre; projected positions allow
|
||||||
// two CSS pixels for that camera quantization. Counts and identities are exact.
|
// 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) };
|
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. */
|
/** Keep raw samples and declared exclusions beside the complete difference list. */
|
||||||
export function compareColdStart(
|
export function compareColdStart(
|
||||||
switched,
|
switched,
|
||||||
cold,
|
cold,
|
||||||
{ direction, owners = {}, allow = [], sessionState = true },
|
{ 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 = [],
|
const differences = [],
|
||||||
skipped = [];
|
skipped = [];
|
||||||
const add = (layer, family, id, rows) =>
|
const add = (layer, family, id, rows) =>
|
||||||
differences.push(
|
differences.push(
|
||||||
...rows.map((row) => ({ layer, direction, family, id, ...row })),
|
...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);
|
const ledger = compareLedgers(switched.ledger, cold.ledger, allow);
|
||||||
for (const [side, rows] of [
|
for (const [side, rows] of [
|
||||||
['switch', ledger.onlyBefore],
|
['switch', ledger.onlyBefore],
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
compareColdStart,
|
compareColdStart,
|
||||||
sessionExclusion,
|
sessionExclusion,
|
||||||
probeSnapshot,
|
probeSnapshot,
|
||||||
|
normalizeAllocations,
|
||||||
|
coldStartReady,
|
||||||
} from './coldStart.mjs';
|
} from './coldStart.mjs';
|
||||||
|
|
||||||
const snapshot = () => ({
|
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);
|
assert.equal(engine.frames.postRender.numberOfListeners, listeners);
|
||||||
engine.destroy();
|
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';
|
} from '../../src/bench/satelliteFixture.js';
|
||||||
import { launchFixture } from '../../src/bench/launchFixture.js';
|
import { launchFixture } from '../../src/bench/launchFixture.js';
|
||||||
|
|
||||||
// These are the feeds used by the strict switching scenarios, with motion and
|
// These are the feeds used by the strict switching scenarios. Feed timestamps
|
||||||
// timestamps held fixed so page startup duration cannot change the sample.
|
// stay fixed; wall time must advance for visibility throttles and fade-in.
|
||||||
export async function coldStartFixtures(origin, group) {
|
export async function coldStartFixtures(origin, group) {
|
||||||
const epoch =
|
const epoch =
|
||||||
group === 'space'
|
group === 'space'
|
||||||
@@ -41,7 +41,8 @@ export async function coldStartFixtures(origin, group) {
|
|||||||
const routes = [];
|
const routes = [];
|
||||||
const fixtures = [];
|
const fixtures = [];
|
||||||
const route = (pattern, respond) => routes.push({ pattern, respond });
|
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') {
|
if (group === 'open') {
|
||||||
route('https://tiles.openseamap.org/*', image);
|
route('https://tiles.openseamap.org/*', image);
|
||||||
route('https://www.opensnowmap.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 }) {
|
function installFeeds({ group, epoch, groups, ghost, launches, firms }) {
|
||||||
const RealDate = Date;
|
const RealDate = Date;
|
||||||
|
const started = performance.now();
|
||||||
|
const now = () =>
|
||||||
|
group === 'space' ? epoch : epoch + performance.now() - started;
|
||||||
globalThis.Date = class extends RealDate {
|
globalThis.Date = class extends RealDate {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...(args.length ? args : [epoch]));
|
super(...(args.length ? args : [now()]));
|
||||||
}
|
}
|
||||||
static now() {
|
static now() {
|
||||||
return epoch;
|
return Math.floor(now());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
||||||
@@ -558,6 +562,18 @@ function installFeeds({ group, epoch, groups, ghost, launches, firms }) {
|
|||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
new Response(groups[path.split('/').pop()] ?? groups.stations),
|
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/launches') return json(launches);
|
||||||
if (path === '/api/firms') return json(firms);
|
if (path === '/api/firms') return json(firms);
|
||||||
if (path === '/api/route') {
|
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.
|
// 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.
|
// 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/google/nearby-places') return json({ places: [] });
|
||||||
if (path === '/api/setup/status') return json({ ready: true });
|
if (path === '/api/setup/status') return json({ ready: true });
|
||||||
if (/^\/api\/(firms|mapillary)\/status$/.test(path) && group !== 'street')
|
if (/^\/api\/(firms|mapillary)\/status$/.test(path) && group !== 'street')
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import test from 'node:test';
|
|||||||
import vm from 'node:vm';
|
import vm from 'node:vm';
|
||||||
import { coldStartFixtures } from './coldStartFixtures.mjs';
|
import { coldStartFixtures } from './coldStartFixtures.mjs';
|
||||||
import { COLD_START_GROUPS } from './coldStart.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';
|
const origin = 'http://127.0.0.1:4309';
|
||||||
test('every group has valid strict fixture routes', async () => {
|
test('every group has valid strict fixture routes', async () => {
|
||||||
@@ -62,7 +67,8 @@ test('independent pages receive identical stationary aircraft and timestamps thr
|
|||||||
URL,
|
URL,
|
||||||
Response,
|
Response,
|
||||||
EventTarget,
|
EventTarget,
|
||||||
location: { href: origin },
|
performance: { now: () => 0 },
|
||||||
|
location: { href: origin, origin },
|
||||||
localStorage: { setItem() {} },
|
localStorage: { setItem() {} },
|
||||||
window: {
|
window: {
|
||||||
EventSource: class {},
|
EventSource: class {},
|
||||||
@@ -95,3 +101,179 @@ test('independent pages receive identical stationary aircraft and timestamps thr
|
|||||||
'/api/unexpected-feed',
|
'/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. */
|
/** The five public drawing registries, with no SDK objects or volatile fields. */
|
||||||
export function drawLedger(page) {
|
export function drawLedger(page) {
|
||||||
return page.evaluate(() => {
|
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']
|
return ['movers', 'lines', 'ground', 'imagery', 'meshes']
|
||||||
.flatMap((family) =>
|
.flatMap((family) =>
|
||||||
engine[family].list().map((row) => ({
|
engine[family].list().map((row) => ({
|
||||||
id: String(row.id),
|
id: String(row.id),
|
||||||
family,
|
family,
|
||||||
|
...(ownerOf(String(row.id))
|
||||||
|
? { layer: ownerOf(String(row.id)) }
|
||||||
|
: {}),
|
||||||
...(row.kind === undefined ? {} : { kind: row.kind }),
|
...(row.kind === undefined ? {} : { kind: row.kind }),
|
||||||
...(row.count === undefined ? {} : { count: row.count }),
|
...(row.count === undefined ? {} : { count: row.count }),
|
||||||
shown: row.shown ?? true,
|
shown: row.shown ?? true,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export function createInfrastructureSessionLayer(
|
|||||||
request = null,
|
request = null,
|
||||||
abort = null,
|
abort = null,
|
||||||
display = null,
|
display = null,
|
||||||
|
sceneContext = null,
|
||||||
enabled = false,
|
enabled = false,
|
||||||
destroyed = false;
|
destroyed = false;
|
||||||
let updated = null,
|
let updated = null,
|
||||||
@@ -110,6 +111,8 @@ export function createInfrastructureSessionLayer(
|
|||||||
try {
|
try {
|
||||||
const next = await read();
|
const next = await read();
|
||||||
if (!enabled || destroyed) return;
|
if (!enabled || destroyed) return;
|
||||||
|
if (sceneContext?.viewReady === false || sceneContext?.signal?.aborted)
|
||||||
|
return;
|
||||||
const active = display;
|
const active = display;
|
||||||
if (active) {
|
if (active) {
|
||||||
error = null;
|
error = null;
|
||||||
@@ -138,8 +141,10 @@ export function createInfrastructureSessionLayer(
|
|||||||
init(_viewer, options = {}) {
|
init(_viewer, options = {}) {
|
||||||
if (options.engine) return this.attachRenderer(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');
|
if (display) throw Error('Infrastructure display already attached');
|
||||||
|
sceneContext = options;
|
||||||
display = engine ? createDisplay(engine, config, services) : null;
|
display = engine ? createDisplay(engine, config, services) : null;
|
||||||
// The dataset load belongs to the session and keeps running; only this
|
// 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
|
// 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;
|
if (!signal?.aborted) throw error;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
resumeViewport() {
|
||||||
|
// LOD keeps incumbents, so its first walk must use the restored camera.
|
||||||
|
if (enabled && display) return show();
|
||||||
|
},
|
||||||
detachRenderer() {
|
detachRenderer() {
|
||||||
if (!display) return;
|
if (!display) return;
|
||||||
const selected = services.getSelectedEntityContext?.();
|
const selected = services.getSelectedEntityContext?.();
|
||||||
@@ -158,6 +167,7 @@ export function createInfrastructureSessionLayer(
|
|||||||
: null;
|
: null;
|
||||||
display.destroy();
|
display.destroy();
|
||||||
display = null;
|
display = null;
|
||||||
|
sceneContext = null;
|
||||||
},
|
},
|
||||||
enable() {
|
enable() {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
@@ -196,6 +206,7 @@ export function createInfrastructureSessionLayer(
|
|||||||
abort?.abort();
|
abort?.abort();
|
||||||
display?.destroy();
|
display?.destroy();
|
||||||
display = data = null;
|
display = data = null;
|
||||||
|
sceneContext = null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,55 @@ import {
|
|||||||
selectEntityContext,
|
selectEntityContext,
|
||||||
} from '../../data/contextStore.js';
|
} from '../../data/contextStore.js';
|
||||||
import { layerFeedState } from '../../data/feedState.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) => {
|
test('a bundled file delivered between planets keeps multipart IDs and is read only once', async (t) => {
|
||||||
let deliver,
|
let deliver,
|
||||||
|
|||||||
Reference in new issue
Block a user