Files
gods-eye-view/scripts/qa-sprites-b5.mjs
T
German Khokhlov 3907b7084e Read QA drawing evidence through the globe contract
Port the layer probes in twenty-six browser scripts to engine primitive
lists, drawn rows and layer diagnostics. Preserve Cesium as the reference
and keep the supported Navara paths on the same observations. This
changes QA observation only, with no application behaviour change.

Review the interrupted infrastructure group and retain its readiness,
drape, selection, label and floor checks. Match both installation outline
buffers. Frame each CCTV city before reading its visible coverage. Read
Radio colours and selected identities from actual mover writes. Retain
transit's GPU upload budgets using engine-owned line primitives rather
than substituting rebuild counts. Keep native inspection where a check
specifically measures GPU uploads, loaded meshes or ground readiness.

The preceding combined B/C commit owns all shared manifests, docs,
locale catalogs and the coupling baseline; this commit changes only its
own twenty-six QA scripts. Coupling remains unchanged at 344 Cesium
renderer lines, 1294 Navara renderer lines, 9 Cesium import files, 1026
viewer lines, 2 Navara kind checks, 0 engine sniff calls, 9 extension
reads, 1 ground pass, 3 SDK loads, 4 screen-handler files and 42 engine-name
lines.

The full unit suite and the boundary, format and coupling checks pass.
Browser validation remains to be run.

Browser checks to run
GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4291
node scripts/qa-alpr-journey.mjs http://localhost:4291
node scripts/qa-alpr-journey.mjs 'http://localhost:4291/?engine=navara'
node scripts/qa-cables-overlay.mjs --url http://localhost:4291
node scripts/qa-cctv-v2.mjs --url http://localhost:4291 --fixture --terrain-globe
node scripts/qa-context-arrivals.mjs http://localhost:4291
node scripts/qa-directions.mjs --url http://localhost:4291
QA_BASE_URL=http://localhost:4291 node scripts/qa-draw-tool.mjs
QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-hold.mjs
node scripts/qa-ghostmaps.mjs --url http://localhost:4291
node scripts/qa-gnss.mjs --url http://localhost:4291
node scripts/qa-height-datum.mjs --url http://localhost:4291
node scripts/qa-infra-lod.mjs --url http://localhost:4291
node scripts/qa-installation-navigation.mjs http://localhost:4291
node scripts/qa-military-names.mjs http://localhost:4291 --no-video
node scripts/qa-overpass-offload.mjs --url http://localhost:4291
node scripts/qa-radio.mjs --url http://localhost:4291
node scripts/qa-traffic-motion.mjs http://localhost:4291 --mode=osm
QA_BASE_URL=http://localhost:4291 node scripts/qa-transit-recovery.mjs
QA_BASE_URL=http://localhost:4291 node scripts/qa-transit.mjs
node scripts/qa-weather-teardown.mjs --url http://localhost:4291 --headless
for engine in cesium navara; do
  node scripts/qa-enrich-ambient.mjs --url http://localhost:4291 --engine=$engine
  QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-verify.mjs --engine=$engine
  node scripts/qa-heading-b3.mjs --url http://localhost:4291 --engine=$engine
  node scripts/qa-overlay-baseline.mjs --url http://localhost:4291 --engine=$engine
  node scripts/qa-sprites-b5.mjs --url http://localhost:4291 --engine=$engine
  node scripts/qa-vessel-datum.mjs --url 'http://localhost:4291/?qaFeed=vessels:fixture' --engine $engine
done

The military-names driver exercises installation-polish; the transit
driver exercises transit-scenes. Use only the keyless server. Cases that
require photoreal tiles or unavailable providers remain unverified.
2026-11-07 12:45:21 +04:00

578 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* qa-sprites-b5.mjs — throwaway QA harness for skylight Tasks 8+9
* (type-aware billboard silhouettes + per-class 2D/3D scale; Batch 5).
*
* Injects synthetic STRAIGHT-FLYING aircraft covering ALL 8 classifyAircraft
* classes — the flights layer via OpenSky extended categories (state[17]),
* the military layer via adsb.lol type codes — using the same fetch-shim +
* history-priming pattern as scripts/qa-heading-b3.mjs (positions analytic at
* serve time so every fix is self-consistent through the render delay).
*
* Machine assertions:
* B1 ingest : both layers ingest the synthetic planes
* B2 billboards : every fleet billboard carries EXACTLY the expected
* per-class glyph (image data-URI === aircraftIcon(klass),
* imported from src) and per-class scale (flights:
* CLASS_SCALE_2D; military: 0.7 × CLASS_SCALE_2D)
* B3 models : with 3D on, fleet Cesium.Model.scale === the layer's
* MODEL_SCALE (both 1; legacy scale is baked into the GLB)
* × CLASS_SCALE_3D[klass] per icao; tracked too
* B4 clean : no console errors
*
* Visual output (qa-shots/b5/, untracked): top-down 2D spreads of both rows,
* per-row close-ups, a tracked-cyan tint shot, and 3D cluster shots at two
* zooms per layer showing per-class model scale differences. The PNGs are the
* acceptance test — read them.
*
* Run: node scripts/qa-sprites-b5.mjs --url http://localhost:4300
* Exits non-zero if any assertion fails. Never commits anything.
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { classifyAircraft, CLASS_SCALE_2D, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
// Hangar fleet (2026-08-16): real per-class GLBs render at scale 1; military
// heavies (airliner/quadjet/glider) render airplane.glb at 1 x class; only
// fastjet/unknown keep the per-layer jet/airplane MODEL_SCALE formula.
const flightsWantScale = (klass) => (CLASS_MODEL_REAL[klass] ? 1 : FL_MODEL_SCALE * CLASS_SCALE_3D[klass]);
const MIL_PLANE_CLASSES = new Set(['airliner', 'quadjet', 'glider']);
const militaryWantScale = (klass) => (CLASS_MODEL_REAL[klass] ? 1
: MIL_PLANE_CLASSES.has(klass) ? MIL_PLANE_MODEL_SCALE * CLASS_SCALE_3D[klass]
: MIL_MODEL_SCALE * CLASS_SCALE_3D[klass]);
import { aircraftIcon } from '../src/data/aircraftIcons.js';
// ---------------------------------------------------------------------------
// Args (same shape as qa-heading-b3.mjs)
// ---------------------------------------------------------------------------
const argv = process.argv.slice(2);
const getFlag = (name) => argv.includes(name);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const BASE_URL = getOpt('--url', 'http://localhost:4173');
// --engine=cesium|navara: on both engines the layers are read through the QA
// probe (window.__godsEyeView.layers.drawn, the dev server's); the military
// checks are skipped where the engine has no military layer.
const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
const APP_URL =
ENGINE === 'cesium' ? BASE_URL : `${BASE_URL.replace(/\/$/, '')}/?engine=${ENGINE}`;
const HEADFUL = getFlag('--headful');
const SHOT_DIR = path.resolve(ENGINE === 'cesium' ? 'qa-shots/b5' : `qa-shots/b5-${ENGINE}`);
const FL_MODEL_SCALE = 1; // flights.js (airplane.glb is transform-applied and meter-scale)
const MIL_PLANE_MODEL_SCALE = 1; // military heavy classes share the same baked airplane.glb
const MIL_MODEL_SCALE = 1; // militaryFlights.js (jet.glb is already real-world scale, native radius ~29.8 m)
const MIL_BILLBOARD_SCALE = 0.7; // militaryFlights BILLBOARD_SCALE
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
await puppeteer.executablePath().catch(() => null),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* skip */ }
}
return null;
}
const results = [];
function record(name, ok, detail) {
results.push({ name, ok, detail });
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
// ---------------------------------------------------------------------------
// Synthetic straight-flying aircraft.
// Rows (2D spread): flights row at lat 30.32 (6 category-driven classes),
// military row at lat 30.28 (all 8 classes via type codes). Clusters (3D
// scale comparison): tight 130–150 m spacing so the follow-camera frames
// several models at once below the minimumPixelSize clamp distance.
// courseDeg 0 = flying north (glyph nose-up in a north-up top-down view);
// two planes fly rotated courses to confirm sprites still follow the path.
// ---------------------------------------------------------------------------
const ROW_LON0 = -97.82;
const ROW_STEP_DEG = 0.012; // ~1.15 km — full military row (8 planes) fits the unobstructed frame center
const CLUSTER_LON0 = -97.7431;
const mDegLon = (m, lat) => m / (111320 * Math.cos((lat * Math.PI) / 180));
const SPRITES = {
timeOffsetSec: 0,
// OpenSky extended categories: 2 light, 4 large airliner, 6 heavy/widebody,
// 7 high-perf fastjet, 8 rotorcraft, 9 glider. (quadjet/turboprop need type
// codes, which OpenSky lacks — the military row covers those.)
flights: [
{ icao: 'caa001', callsign: 'FL-LGT', lon0: ROW_LON0 + 0 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 80, altM: 2500, category: 2 },
{ icao: 'caa002', callsign: 'FL-AIR', lon0: ROW_LON0 + 1 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 80, altM: 2600, category: 4 },
{ icao: 'caa003', callsign: 'FL-WID', lon0: ROW_LON0 + 2 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 80, altM: 2700, category: 6 },
{ icao: 'caa004', callsign: 'FL-FJ', lon0: ROW_LON0 + 3 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 90, speedMps: 80, altM: 2800, category: 7 },
{ icao: 'caa005', callsign: 'FL-HEL', lon0: ROW_LON0 + 4 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 40, altM: 1200, category: 8 },
{ icao: 'caa006', callsign: 'FL-GLD', lon0: ROW_LON0 + 5 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 40, altM: 2000, category: 9 },
// 3D cluster (150 m spacing): widebody vs airliner (tracked) vs fastjet
{ icao: 'cbb001', callsign: '3D-WID', lon0: CLUSTER_LON0 - mDegLon(150, 30.10), lat0: 30.10, courseDeg: 0, speedMps: 60, altM: 3000, category: 6 },
{ icao: 'cbb002', callsign: '3D-AIR', lon0: CLUSTER_LON0, lat0: 30.10, courseDeg: 0, speedMps: 60, altM: 3000, category: 4 },
{ icao: 'cbb003', callsign: '3D-FJ', lon0: CLUSTER_LON0 + mDegLon(150, 30.10), lat0: 30.10, courseDeg: 0, speedMps: 60, altM: 3000, category: 7 },
],
military: [
{ hex: 'dda001', flight: 'ML-LGT', t: 'C172', lon0: ROW_LON0 + 0 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 60, altFt: 9000 },
{ hex: 'dda002', flight: 'ML-GLD', t: 'DISC', lon0: ROW_LON0 + 1 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 40, altFt: 8000 },
{ hex: 'dda003', flight: 'ML-TPR', t: 'C130', lon0: ROW_LON0 + 2 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 10000 },
{ hex: 'dda004', flight: 'ML-AIR', t: 'A320', lon0: ROW_LON0 + 3 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 11000 },
{ hex: 'dda005', flight: 'ML-WID', t: 'C17', lon0: ROW_LON0 + 4 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 12000 },
{ hex: 'dda006', flight: 'ML-QUAD', t: 'B744', lon0: ROW_LON0 + 5 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 13000 },
{ hex: 'dda007', flight: 'ML-HEL', t: 'H60', lon0: ROW_LON0 + 6 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 40, altFt: 3000 },
// course 0 like the rest — keeps the row a clean size-comparison line (2D
// rotation is demoed by caa004 and gated by qa-heading-b3 anyway).
{ hex: 'dda008', flight: 'ML-FJ', t: 'F16', lon0: ROW_LON0 + 7 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 15000 },
// 3D cluster: C17 vs F16 (tracked) vs B744 vs C130. jet.glb is a
// real-world-scale asset (native bounding radius ~29.8) rendered at
// MODEL_SCALE 1, so models are ~22–43 m radius — same 150 m spacing and
// 450–900 m shot ranges as the flights cluster.
{ hex: 'ddb001', flight: '3DMWID', t: 'C17', lon0: CLUSTER_LON0 - mDegLon(150, 30.05), lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
{ hex: 'ddb002', flight: '3DMFJ', t: 'F16', lon0: CLUSTER_LON0, lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
{ hex: 'ddb003', flight: '3DMQUA', t: 'B744', lon0: CLUSTER_LON0 + mDegLon(150, 30.05), lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
{ hex: 'ddb004', flight: '3DMTPR', t: 'C130', lon0: CLUSTER_LON0 - mDegLon(300, 30.05), lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
],
};
// Node-side expected classification (same inputs the layers see).
const expectedFlights = new Map(SPRITES.flights.map((p) => [p.icao, classifyAircraft({ category: p.category })]));
const expectedMilitary = new Map(SPRITES.military.map((p) => [p.hex, classifyAircraft({ typeCode: p.t })]));
// ---------------------------------------------------------------------------
async function main() {
console.log(`\nType-aware sprite QA (Batch 5 / skylight Tasks 8+9)`);
console.log(` App URL : ${APP_URL}\n`);
console.log(` Expected classes — flights: ${[...expectedFlights.entries()].map(([k, v]) => `${k}:${v}`).join(' ')}`);
console.log(` Expected classes — military: ${[...expectedMilitary.entries()].map(([k, v]) => `${k}:${v}`).join(' ')}\n`);
try {
const res = await fetch(APP_URL, { method: 'GET' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`);
process.exit(2);
}
fs.mkdirSync(SHOT_DIR, { recursive: true });
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
`--use-angle=${process.env.QA_ANGLE || 'swiftshader'}`,
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1280,800',
],
});
const consoleErrors = [];
const httpErrors = [];
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
page.on('console', (msg) => {
if (msg.type() === 'error') {
const text = msg.text();
if (!/Failed to load resource.*404/i.test(text)) consoleErrors.push(text);
}
});
page.on('pageerror', (err) => consoleErrors.push(`pageerror: ${err.message}`));
page.on('response', (response) => {
if (response.status() >= 500) httpErrors.push(`${response.status()} ${response.url()}`);
});
// ---- Straight-flight fetch shim (installed before any app code runs) ----
await page.evaluateOnNewDocument((spec) => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
window.__SPR = spec;
window.__SPR.epochMs = Date.now();
window.__SPR_HITS = { opensky: 0, mil: 0 };
// Straight line from (lon0, lat0) along courseDeg at speedMps.
window.__SPR.stateAt = (p, tSec) => {
const cRad = (p.courseDeg * Math.PI) / 180;
const dist = p.speedMps * tSec;
const east = dist * Math.sin(cRad);
const north = dist * Math.cos(cRad);
const lat = p.lat0 + north / 111320;
const lon = p.lon0 + east / (111320 * Math.cos((p.lat0 * Math.PI) / 180));
return { lon, lat, course: p.courseDeg, speedMps: p.speedMps };
};
const realFetch = window.fetch.bind(window);
const jsonResponse = (obj) => new Response(JSON.stringify(obj), {
status: 200, headers: { 'Content-Type': 'application/json' },
});
window.fetch = (input, init) => {
const url = typeof input === 'string' ? input : (input && input.url) || '';
const S = window.__SPR;
if (url.includes('/api/openai/hud-summary')) {
return Promise.resolve(jsonResponse({ summary: 'Aircraft scale QA' }));
}
const nowSec = Date.now() / 1000 + (S.timeOffsetSec || 0);
const tRel = nowSec - S.epochMs / 1000;
if (url.includes('/api/flights/track')) return Promise.resolve(jsonResponse({ path: [] }));
if (url.includes('/api/military/track')) {
return Promise.resolve(jsonResponse({ timestamp: Math.floor(nowSec), trace: [] }));
}
if (url.includes('/api/flights')) {
window.__SPR_HITS.opensky++;
const states = S.flights.map((f) => {
const s = S.stateAt(f, tRel);
return [
f.icao, f.callsign, 'Synthetica',
Math.floor(nowSec), Math.floor(nowSec),
s.lon, s.lat, f.altM,
false, // on_ground
s.speedMps, s.course,
0, null, null, null, false, 0,
f.category, // state[17] — extended emitter category
];
});
return Promise.resolve(jsonResponse({ time: Math.floor(nowSec), states }));
}
if (url.includes('/api/military')) {
window.__SPR_HITS.mil++;
const ac = S.military.map((m) => {
const s = S.stateAt(m, tRel);
return {
hex: m.hex, flight: m.flight,
lon: s.lon, lat: s.lat, alt_baro: m.altFt,
track: s.course, gs: s.speedMps * 1.9438,
t: m.t, r: `SY-${m.hex.slice(-3)}`, ownOp: 'SYNTH AF',
seen_pos: Math.max(0, -(S.timeOffsetSec || 0)),
};
});
return Promise.resolve(jsonResponse({ msg: 'No error', now: Date.now(), ac }));
}
// Synthetic categories/types already exercise every classification
// branch. ADSBDB enrichment is incidental to this visual-scale check,
// so keep an unavailable public enrichment provider from generating an
// unrelated console error.
if (url.includes('/api/adsbdb/')) return Promise.resolve(jsonResponse({ found: false }));
return realFetch(input, init);
};
}, SPRITES);
console.log('Loading app...');
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView && window.__godsEyeView.engine && window.__godsEyeView.dataManager && window.__godsEyeView.layers,
// A cold dev server compiles the Navara chunks on first load.
{ timeout: 180000, polling: 200 }
);
await page.evaluate(() => window.__godsEyeView.styleManager.initialRestorePromise);
await page.waitForFunction(() => document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 180000 });
await page.evaluate(() => window.__godsEyeView.styleManager.orbitController.stop());
console.log(' App globals ready.');
// ---- In-page probes: billboard walk, model walk, tracked-model finder ---
await page.evaluate(() => {
// Every fleet glyph the flights and military layers draw, through the
// engine-neutral probe (layers.drawn: the layers' own sprite handles),
// on either engine. The layers no longer own Cesium billboards: on
// Cesium the contract's batched billboards carry atlas keys, not the
// glyph data-URIs.
window.__collectBillboards = function () {
return ['flights', 'military'].flatMap((id) => window.__godsEyeView.layers.drawn(id) ?? [])
.filter((row) => !row.trackedModel)
.map(({ id, image, scale, show }) => ({ id, image, scale, show }));
};
// Every fleet and tracked 3D model, with id + scale (the probe's model
// handles). The world bounding radius is not on the contract: null.
window.__collectModels = function () {
return ['flights', 'military'].flatMap((id) => window.__godsEyeView.layers.drawn(id) ?? [])
.filter((row) => row.model && (row.trackedModel || !row.tracked))
.map(({ id, model }) => ({ id, ...model, radius: null }));
};
// Displayed (render-delayed) latitude of a synthetic plane right now.
window.__displayedState = function (layer, idx, renderDelaySec) {
const S = window.__SPR;
const p = S[layer][idx];
const tRel = (Date.now() - S.epochMs) / 1000 - renderDelaySec;
return S.stateAt(p, tRel);
};
// Canvas coordinates of every synthetic plane at its displayed position —
// ground truth for identifying which glyph is which in a screenshot.
window.__planeCanvasCoords = async function (layer, renderDelaySec, altOf) {
const geo = await import('/src/geo/index.js');
const { engine } = window.__godsEyeView;
const S = window.__SPR;
return S[layer].map((p, i) => {
const s = window.__displayedState(layer, i, renderDelaySec);
const pos = geo.cartesian3.fromDegrees(s.lon, s.lat, altOf === 'ft' ? p.altFt * 0.3048 : p.altM);
const c = engine.projector.worldToWindow(pos);
return `${p.icao || p.hex}=(${c ? Math.round(c.x) : '-'},${c ? Math.round(c.y) : '-'})`;
}).join(' ');
};
});
// ---- Prime history through the render delay (same pattern as b3) -------
console.log('Priming straight-flight history through the render delay (30 s / 15 s)...');
const primed = await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const v = window.__godsEyeView.viewer ?? null;
const hasMil = dm.layers.has('military');
window.__SPR.timeOffsetSec = -32;
await dm.setEnabled('flights', true);
if (hasMil) await dm.setEnabled('military', true);
const fl = dm.layers.get('flights').module;
const mil = hasMil ? dm.layers.get('military').module : null;
for (const off of [-24, -16, -8, 0]) {
window.__SPR.timeOffsetSec = off;
await fl.update(v);
await mil?.update(v);
}
window.__SPR.timeOffsetSec = 0;
window.__SPR_DRIVER = setInterval(() => { fl.update(v); mil?.update(v); }, 15000);
return { fl: fl.getStats().count, mil: mil ? mil.getStats().count : null, hits: window.__SPR_HITS };
});
console.log(` flights count=${primed.fl} military count=${primed.mil} | shim hits opensky=${primed.hits.opensky} mil=${primed.hits.mil}`);
const hasMilitary = primed.mil !== null;
if (!hasMilitary) console.log(` [SKIP] the military rows — no military layer on ${ENGINE}`);
record('B1 ingest: synthetic planes in both layers',
primed.fl === SPRITES.flights.length && (!hasMilitary || primed.mil === SPRITES.military.length),
`flights=${primed.fl}/${SPRITES.flights.length} military=${hasMilitary ? `${primed.mil}/${SPRITES.military.length}` : 'n/a'}`);
if (!(primed.fl > 0 && (!hasMilitary || primed.mil > 0))) { finish(); return; }
// ========================================================================
// B2 — billboard glyph + scale per class (machine check)
// ========================================================================
console.log('\nB2 — per-class billboard glyph + scale');
const bbs = await page.evaluate(() => window.__collectBillboards());
const bbById = new Map(bbs.map((b) => [b.id, b]));
let glyphBad = [];
let scaleBad = [];
for (const [id, klass] of expectedFlights) {
const bb = bbById.get(id);
const wantImg = [aircraftIcon(klass), aircraftIcon(klass, 192)];
const wantScale = CLASS_SCALE_2D[klass] || 1;
if (!bb || !wantImg.includes(bb.image)) glyphBad.push(`${id}(${klass})${bb ? ':wrong-image' : ':missing'}`);
if (bb && Math.abs(bb.scale - wantScale) > 1e-9) scaleBad.push(`${id}: ${bb.scale} != ${wantScale}`);
}
for (const [id, klass] of hasMilitary ? expectedMilitary : []) {
const bb = bbById.get(id);
const wantImg = [aircraftIcon(klass), aircraftIcon(klass, 192)];
const wantScale = MIL_BILLBOARD_SCALE * (CLASS_SCALE_2D[klass] || 1);
if (!bb || !wantImg.includes(bb.image)) glyphBad.push(`${id}(${klass})${bb ? ':wrong-image' : ':missing'}`);
if (bb && Math.abs(bb.scale - wantScale) > 1e-9) scaleBad.push(`${id}: ${bb.scale} != ${wantScale}`);
}
record('B2a: every synthetic billboard has its class glyph (data-URI match)',
glyphBad.length === 0, glyphBad.length ? glyphBad.join(' ') : `${expectedFlights.size + (hasMilitary ? expectedMilitary.size : 0)} billboards matched`);
record('B2b: every synthetic billboard has its class scale',
scaleBad.length === 0, scaleBad.length ? scaleBad.join(' ') : 'all scales exact');
const distinctGlyphs = new Set([...expectedFlights.values(), ...(hasMilitary ? expectedMilitary.values() : [])].map((k) => aircraftIcon(k)));
const wantGlyphs = hasMilitary ? 8 : new Set(expectedFlights.values()).size;
record(`B2c: all ${wantGlyphs} classes exercised with ${wantGlyphs} distinct glyphs`,
distinctGlyphs.size === wantGlyphs, `${distinctGlyphs.size} distinct data URIs`);
// ========================================================================
// 2D screenshots
// ========================================================================
console.log('\n2D screenshots → qa-shots/b5/');
const topDown = async (lat, lon, height) => {
await page.evaluate(async ({ lat, lon, height }) => {
const camera = window.__godsEyeView.engine.camera;
camera.cancelFlight(); // the boot fly-to-Austin otherwise stomps setView
const geo = await import('/src/geo/index.js');
camera.setView({
destination: geo.cartesian3.fromDegrees(lon, lat, height),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
}, { lat, lon, height });
await sleep(1800); // let fleet tick land positions + rotations at the new pose
};
// Zoom the follow-camera to an absolute range from the tracked entity.
// In tracked mode camera.position lives in the ENTITY's reference frame, so
// its magnitude IS the range — fixed zoomIn() amounts compound across phases
// (the second tracked plane inherits the previous phase's close-in offset).
const zoomToRange = async (want) => {
await page.evaluate((want) => {
const v = window.__godsEyeView.viewer;
if (!v) return; // the engine follow keeps its own range (screenshots only)
const p = v.camera.position;
const d = Math.hypot(p.x, p.y, p.z);
if (d > 1e6) return; // not in a tracked reference frame — leave it alone
v.camera.zoomIn(d - want);
}, want);
await sleep(1500);
};
// Rows drift north while flying; recenter on the DISPLAYED (render-delayed) row.
const rowLat = async (layer, idx, delay) =>
(await page.evaluate(({ layer, idx, delay }) => window.__displayedState(layer, idx, delay), { layer, idx, delay })).lat;
const flRowLat = await rowLat('flights', 2, 30);
const milRowLat = await rowLat('military', 4, 15);
await topDown((flRowLat + milRowLat) / 2, ROW_LON0 + 3 * ROW_STEP_DEG, 16000);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-spread-bothrows.png') });
console.log(' saved b5-2d-spread-bothrows.png (flights row top, military row bottom)');
// Row widths: military spans 7 steps (~8.1 km) → frame ~13.9 km at 12 km up;
// flights spans 5 steps (~5.8 km, +caa004's eastward drift) → ~10.4 km at 9 km up.
if (hasMilitary) {
await topDown(await rowLat('military', 4, 15), ROW_LON0 + 3.5 * ROW_STEP_DEG, 17000);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-military-row.png') });
console.log(' saved b5-2d-military-row.png (8 classes, amber)');
console.log(` glyph positions: ${await page.evaluate(() => window.__planeCanvasCoords('military', 15, 'ft'))}`);
}
await topDown(await rowLat('flights', 2, 30), ROW_LON0 + 2.5 * ROW_STEP_DEG, 12000);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-flights-row.png') });
console.log(' saved b5-2d-flights-row.png (6 category classes, white)');
console.log(` glyph positions: ${await page.evaluate(() => window.__planeCanvasCoords('flights', 30, 'm'))}`);
// Tracked tint: commercial tracked plane goes cyan; neighbors stay white.
await page.evaluate((icao) => {
window.__godsEyeView.dataManager.layers.get('flights').module.trackById(icao);
}, 'caa002');
await sleep(3000);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-tracked-cyan.png') });
console.log(' saved b5-2d-tracked-cyan.png (tracked airliner cyan, follow camera)');
// ========================================================================
// B3 + 3D screenshots — flights cluster
// ========================================================================
console.log('\n3D — flights cluster (airplane.glb, per-class scale)');
await page.evaluate(() => {
const dm = window.__godsEyeView.dataManager;
dm.layers.get('flights').module.setParams({ models3d: true });
dm.layers.get('flights').module.trackById('cbb002');
});
const flModelsUp = await page.waitForFunction(() => {
const models = window.__collectModels().filter((m) => m.ready && m.show);
const ids = new Set(models.map((m) => m.id));
return ids.has('cbb001') && ids.has('cbb002') && ids.has('cbb003');
}, { timeout: 40000, polling: 400 }).then(() => true).catch(() => false);
record('3D flights: tracked model + 2 fleet cluster models rendered', flModelsUp, flModelsUp ? 'all up' : 'timed out');
if (flModelsUp) {
const flModels = await page.evaluate(() => window.__collectModels().filter((m) => m.ready));
const modelScaleBad = [];
for (const [id, want] of [
['cbb001', flightsWantScale(expectedFlights.get('cbb001'))],
['cbb003', flightsWantScale(expectedFlights.get('cbb003'))],
]) {
const m = flModels.find((x) => x.id === id);
if (!m || Math.abs(m.scale - want) > 1e-9) modelScaleBad.push(`${id}: ${m ? m.scale : 'missing'} != ${want}`);
}
const trackedM = flModels.find((x) => x.id === 'cbb002');
const trackedWant = flightsWantScale(expectedFlights.get('cbb002'));
if (!trackedM || Math.abs(trackedM.scale - trackedWant) > 1e-9) {
modelScaleBad.push(`tracked: ${trackedM ? trackedM.scale : 'missing'} != ${trackedWant}`);
}
record('B3a flights: model scales match the per-class registry (all GLBs meter-scale; shared @1×class)',
modelScaleBad.length === 0,
modelScaleBad.length ? modelScaleBad.join(' ') : 'all exact per registry');
console.log(` world bounding radii (m): ${flModels.filter((m) => m.scale !== undefined).map((m) => `${m.id ?? 'tracked'}=${m.radius?.toFixed(1)}`).join(' ')}`);
await sleep(1500);
await zoomToRange(900);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-flights-mid.png') });
console.log(' saved b5-3d-flights-mid.png (900 m range — near the min-pixel clamp)');
await zoomToRange(450);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-flights-close.png') });
console.log(' saved b5-3d-flights-close.png (450 m range: widebody left, airliner center, fastjet right)');
}
// ========================================================================
// B3 + 3D screenshots — military cluster
// ========================================================================
if (!hasMilitary) {
console.log(` [SKIP] 3D military — no military layer on ${ENGINE}`);
} else {
console.log('\n3D — military cluster (jet.glb, per-class scale)');
await page.evaluate(() => {
const dm = window.__godsEyeView.dataManager;
dm.layers.get('military').module.setParams({ models3d: true });
dm.layers.get('military').module.trackById('ddb002');
});
const milModelsUp = await page.waitForFunction(() => {
const models = window.__collectModels().filter((m) => m.ready && m.show);
const ids = new Set(models.map((m) => m.id));
return ids.has('ddb001') && ids.has('ddb002') && ids.has('ddb003') && ids.has('ddb004');
}, { timeout: 40000, polling: 400 }).then(() => true).catch(() => false);
record('3D military: tracked model + 3 fleet cluster models rendered', milModelsUp, milModelsUp ? 'all up' : 'timed out');
if (milModelsUp) {
const milModels = await page.evaluate(() => window.__collectModels().filter((m) => m.ready));
const modelScaleBad = [];
for (const [id, want] of [
['ddb001', militaryWantScale(expectedMilitary.get('ddb001'))],
['ddb003', militaryWantScale(expectedMilitary.get('ddb003'))],
['ddb004', militaryWantScale(expectedMilitary.get('ddb004'))],
]) {
const m = milModels.find((x) => x.id === id);
if (!m || Math.abs(m.scale - want) > 1e-9) modelScaleBad.push(`${id}: ${m ? m.scale : 'missing'} != ${want}`);
}
const trackedM = milModels.find((x) => x.id === 'ddb002');
const trackedWant = militaryWantScale(expectedMilitary.get('ddb002'));
if (!trackedM || Math.abs(trackedM.scale - trackedWant) > 1e-9) {
modelScaleBad.push(`tracked: ${trackedM ? trackedM.scale : 'missing'} != ${trackedWant}`);
}
record('B3b military: model scales match the weight-class registry (real/heavy/jet assets @1×class)',
modelScaleBad.length === 0,
modelScaleBad.length ? modelScaleBad.join(' ') : 'all exact per registry');
console.log(` world bounding radii (m): ${milModels.filter((m) => m.scale !== undefined).map((m) => `${m.id ?? 'tracked'}=${m.radius?.toFixed(1)}`).join(' ')}`);
await sleep(1500);
// jet.glb at MODEL_SCALE 1 is real-world size — same shot ranges as the
// flights cluster (close zoom no longer puts the camera inside a model).
await zoomToRange(900);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-military-far.png') });
console.log(' saved b5-3d-military-far.png (900 m range: C130/C17 left, F16 center, B744 right)');
await zoomToRange(450);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-military-close.png') });
console.log(' saved b5-3d-military-close.png (450 m range: C17 left, F16 center, B744 right)');
}
}
record('B4: no console errors during QA run', consoleErrors.length === 0,
consoleErrors.length
? `${consoleErrors.length}: ${consoleErrors.slice(0, 3).join(' | ')}; HTTP: ${httpErrors.slice(0, 8).join(' | ') || 'none observed'}`
: 'clean');
finish();
} finally {
await browser.close();
}
function finish() {
const failed = results.filter((r) => r.ok === false);
const passed = results.filter((r) => r.ok === true);
console.log(`\n${'─'.repeat(60)}`);
console.log(` RESULT: ${passed.length} passed, ${failed.length} failed`);
console.log(`${'─'.repeat(60)}\n`);
process.exitCode = failed.length > 0 ? 1 : 0;
}
}
main().catch((err) => {
console.error('\n\x1b[31mQA harness error:\x1b[0m', err && err.stack ? err.stack : err);
process.exit(2);
});