/** * qa-height-datum.mjs — height/vertical-datum fix numeric proof harness * (docs/plans/2026-07-05-entity-height-datum-fix.md Task 8). * * Scaffold reused verbatim from qa-cctv-v2.mjs: the puppeteer launcher * (Chrome executable discovery, headless flags), `QA_BASE_URL` env, * `record()`/tally pattern, and the Google-Maps-key-injection dev-server * recipe documented in its header comment. See that file for the reasoning * behind the launch flags and the SwiftShader caveats they share. * * Unlike qa-cctv-v2 (frustum GEOMETRY / raycast-count invariants), this * harness asserts NUMBERS: ground heights, altitudes, and terrain-provider * identity. Every assertion is GL-independent (no pixel/screenshot * comparisons) — it reads plain numbers off live Cesium entities and the * app's own public module APIs, exactly like qa-cctv-v2's SERIALIZE_GEOM_SRC * pattern. * * Three assertion groups (brief order): * * 1. CCTV ground correctness — for 5 London + 5 Austin + 3 SF cameras, * assert each record's placed mount ground reads finitely, then compare * its separately exposed `groundPriorM` against the * authoritative Re:Earth ellipsoidal value fetched through the app's * OWN `/api/terrain/heights` proxy (not a second upstream call — the * proxy IS the oracle here, matching Task 2/3's contract). Keeping the * prior separate is essential because Google-3D can legitimately refine * the rendered ground to the photogrammetric mesh while the immutable * Re:Earth prior remains the fallback/datum reference. The prior check is * `|groundPriorM - reearthEllipsoid| < 6m`. Google-3D after * the one-shot snap: London/Austin active-camera ground must fall in * the brief's bands (tileset-vs-DEM legitimately differs by building * height, so bands not exact-match). * * 2. Aircraft — sample >=10 live aircraft via the flights layer's public * API. `renderAltitudeM` isn't exposed as a named field on any public * method (only `flights.js`/`militaryFlights.js` module-private * `_flightData` carries `geoAltitudeM`/`renderAltitudeM` directly), so * this harness reconstructs both sides purely from public methods: * - render height: `getAllPositions()[i].altitudeM`, which is * `Cartographic.fromCartesian(billboard.position).height` — i.e. * exactly the height passed to `Cartesian3.fromDegrees(lon, lat, * renderAltitudeM)` at the placement site (flights.js ~:2100). * - aviation/baro height: `getNearby()`/`findByQuery()`'s * `altitudeM`, which is `info.altitude` (the UNTOUCHED sticky * barometric field — flights.js ~:2111) — this is the "geoAltM ?? * baroAltM" input side, not the render output. * This is the brief's documented fallback ("if geo/baro fields aren't * exposed on the public state, assert the weaker but still-meaningful * under-terrain + finite/plausible checks") PLUS a same-process, * Node-side independent recomputation of `baroM + geoidHeight(lat,lon)` * via a direct import of `src/data/geoid.js` (a pure module, imported * the same way qa-cctv-v2.mjs imports `computeFrustumGeometry` from * cctv.js) — giving a real numeric cross-check without reaching into * the layer's private closure state. * * Under-terrain detector fingerprint (tightened): a real datum-miss * renders an aircraft at the ~0m ellipsoidal SENTINEL (the pre-existing * "no data yet" default) while its actual terrain ground is nowhere * near 0 — that combination is unambiguous. A non-zero under-terrain * reading is NOT hard-failed: a legitimately climbing/descending * aircraft can transiently render close to real ground near an airport * (e.g. a climb-out a few dozen metres above a field that itself sits * well above sea level), which is correct behavior, not a bug. Those * ambiguous cases are recorded INCONCLUSIVE with the raw numbers * rather than FAILed. * * 3. Regime C re-resolve — switch the map stack to keyless OSM in-page via * `mapStackController.setStack('osm')`, assert * `viewer.terrainProvider` is NOT an `EllipsoidTerrainProvider` * (constructor-name check, matching qa-cctv-v2's "borrow statics off a * live instance's constructor" pattern — no `window.Cesium` global * exists), and that CCTV grounds re-resolve within one event cycle * (`gev:map-stack-changed` → cctv.js's regime re-arm — see its Task 5 * handler) by serializing a camera's ground height before/after the * switch settles and asserting it moved OFF any old fabricated-prior * value. * * Upstream-dependent assertions (live Re:Earth `/api/terrain/heights`, live * OpenSky aircraft) are marked INCONCLUSIVE — not FAIL — when the upstream * is unreachable this run, mirroring qa-cctv-v2's tiles-timeout handling. * GL-dependent tile settling (the Google-3D one-shot ground snap actually * completing under headless SwiftShader) is likewise inconclusive-on-timeout. * * Run: QA_BASE_URL=http://localhost:4300 node scripts/qa-height-datum.mjs * * Exit 0 = no hard failures. Non-zero = at least one hard FAIL (or harness error). */ import puppeteer from 'puppeteer'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ensureGeoidReady, geoidHeight } from '../src/data/geoid.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, '..'); const argv = process.argv.slice(2); const getOpt = (name, dflt) => { const i = argv.indexOf(name); return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt; }; const getFlag = (name) => argv.includes(name); const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173'; const APP_URL = getOpt('--url', BASE_URL); const HEADFUL = getFlag('--headful'); const SHOTS_DIR = path.join(REPO_ROOT, 'qa-shots', 'height-datum-qa'); 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. (() => { try { return puppeteer.executablePath(); } catch { return 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 { /* ignore */ } } return null; } const results = []; function record(name, ok, detail) { results.push({ name, ok, detail }); const tag = ok === null ? '\x1b[33mINCONCLUSIVE\x1b[0m' : ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'; console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`); } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); /** * Waits for the scene's 3D tileset to report tilesLoaded===true. Copied from * qa-cctv-v2.mjs (see its header comment for why this must be re-awaited * fresh before each action that depends on a REAL ground sample, not * reused from an earlier wait). * @param {import('puppeteer').Page} page * @param {number} [timeoutMs=15000] * @returns {Promise} */ function waitForTilesLoaded(page, timeoutMs = 15000) { return page.waitForFunction( () => { const scene = window.__godsEyeView.viewer.scene; const prims = scene.primitives; for (let i = 0; i < prims.length; i++) { const p = prims.get(i); if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') { return p.tilesLoaded === true; } } return true; }, { timeout: timeoutMs } ).then(() => true).catch(() => false); } /** * Reads a camera's placed mount-point cartographic height off its * `cctv--ray-tl` polyline entity (first position = the mount, per * cctv.js `buildCoverageEntities`/`applyFrustumGeometry`), returns * `{ mountAltM, groundM }` where `groundM = mountAltM - mountHeightM` — the * SAME arithmetic cctv.js itself uses in reverse * (`mountAlt = ground + mountHeightM` in `computeFrustumGeometry`). * @param {import('puppeteer').Page} page * @param {string} camId * @returns {Promise<{mountAltM:number, mountHeightM:number, groundM:number}|null>} */ async function readCameraGround(page, camId) { return page.evaluate((id) => { const viewer = window.__godsEyeView.viewer; const time = viewer.clock.currentTime; const ent = viewer.entities.getById(`cctv-${id}-ray-tl`); if (!ent || !ent.polyline) return null; const positions = ent.polyline.positions.getValue(time); if (!Array.isArray(positions) || positions.length < 1) return null; const mount = positions[0]; // No window.Cesium global (qa-cctv-v2 precedent) — borrow Cartographic's // static fromCartesian off a live Cartographic instance's constructor. const carto = viewer.scene.globe.ellipsoid.cartesianToCartographic(mount); const mountAltM = carto.height; const mod = window.__godsEyeView.dataManager.layers.get('cctv').module; const cam = mod.getUIState().cameras.find((c) => c.id === id); const mountHeightM = cam ? cam.mountHeightM : null; return { mountAltM, mountHeightM, groundM: Number.isFinite(mountHeightM) ? mountAltM - mountHeightM : null, }; }, camId); } /** * Fetches the authoritative Re:Earth ellipsoidal ground height for a * (lat, lon) pair through the APP'S OWN `/api/terrain/heights` proxy (the * same oracle terrainHeights.js's `resolveEllipsoidalGround` uses) — this is * the ground-truth reference for assertion group 1, fetched directly (not * through in-page code) so a proxy hiccup can be told apart from an app bug. * @param {number} lat * @param {number} lon * @returns {Promise} ellipsoid height in metres, or null on failure. */ async function fetchReearthEllipsoid(lat, lon) { const url = `${APP_URL}/api/terrain/heights?points=${encodeURIComponent(`${lon.toFixed(5)},${lat.toFixed(5)}`)}`; try { const res = await fetch(url, { signal: AbortSignal.timeout(30000) }); if (!res.ok) return null; const body = await res.json(); const v = Number(body?.results?.[0]?.ellipsoid); return Number.isFinite(v) ? v : null; } catch { return null; } } async function main() { console.log(`\nHeight-Datum Fix Numeric Proof`); console.log(` App URL : ${APP_URL}`); console.log(` Mode : ${HEADFUL ? 'headful' : 'headless'}\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(SHOTS_DIR, { recursive: true }); // Node-side geoid grid, used only for the aircraft baro+N cross-check // (independent of the app's own bundled copy — same package, separate // process, so this is a real recomputation, not reading the app's cache). await ensureGeoidReady(); 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=swiftshader', '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800', ], }); const consoleErrors = []; let exitCode = 0; try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); page.on('console', (msg) => { if (msg.type() === 'error') { const t = msg.text(); if (!/Failed to load resource|net::ERR|status of 4\d\d|status of 5\d\d/.test(t)) { consoleErrors.push(t); } } }); console.log('Loading app...'); await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.waitForFunction( () => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.dataManager, { timeout: 60000 } ); await sleep(4000); // ========================================================================= // Group 1: CCTV ground correctness // ========================================================================= console.log('Enabling CCTV layer (full city-packs catalog)...'); await page.evaluate(async () => { const dm = window.__godsEyeView.dataManager; const entry = dm.layers.get('cctv'); if (!entry.enabled) await dm.toggle('cctv'); }); const camCount = await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('cctv').module.getUIState().count); console.log(`CCTV catalog loaded: ${camCount} cameras. Waiting for the geometry-load queue to drain...`); // The full-catalog drain is NOT a precondition for group 1's assertions // below: every record renders correctly from `record.groundPrior` the // instant the Re:Earth prior batch lands (Task 5 contract #1/#4 — "the // prior IS the resolution" on globe stacks, and the fallback path in // google-3d before a real sample resolves), which is exactly what makes // the per-camera ground reads meaningful even mid-drain. The queue drain // itself additionally attempts ONE REAL scene.sampleHeight per camera in // google-3d regime (Task 5 contract #3/#5 — the ≤1×N invariant // qa-cctv-v2 also locks), and at this branch's full city-packs catalog // size (800 cameras: 250 Austin + 300 Caltrans + 250 TfL — measured // directly probing this harness's own dev server) that can take many // minutes under headless SwiftShader (empirically ~2-6s/sample once // real tile contention kicks in past the first few hundred). Waiting for // FULL completion here would make routine harness runs impractically // slow for no assertion gain, so this bounds the wait to a fixed, // reasonable ceiling and reports the outcome as environmental/scale // dependent (inconclusive, not a hard fail) rather than failing the // whole run — mirroring qa-cctv-v2's own tiles-timeout handling. const DRAIN_WAIT_CEILING_MS = 120000; const drained = await page.waitForFunction( () => { const mod = window.__godsEyeView.dataManager.layers.get('cctv').module; const ui = mod.getUIState(); return ui.loading && ui.loading.active === false; }, { timeout: DRAIN_WAIT_CEILING_MS } ).then(() => true).catch(() => false); record(`CCTV geometry-load queue drains within ${Math.round(DRAIN_WAIT_CEILING_MS / 1000)}s (N=${camCount})`, drained ? true : null, drained ? 'loading.active === false' : `did not fully drain within the ceiling at N=${camCount} — environmental/scale-dependent under headless SwiftShader ` + '(each un-drained camera still renders correctly from its Re:Earth prior; group 1 assertions below read that prior directly, so this does not block them)'); // Pull the full camera list, then bucket by city so we can pick 5 // London + 5 Austin + 3 SF regardless of catalog ordering. TfL tags // `city: 'London'` and Austin's own pack tags `city: 'Austin'` exactly // (vite.config.js); Caltrans tags `city` with an arbitrary upstream // `nearbyPlace` string (NOT a stable "San Francisco" literal), so SF-area // Caltrans cameras are identified by proximity to the SF anchor instead // (same anchor vite.config.js's CALTRANS_ANCHORS uses for prioritization). const allCameras = await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('cctv').module.getUIState().cameras); console.log(`Camera catalog: ${allCameras.length} total.`); const SF_ANCHOR = { lat: 37.7793, lon: -122.4193 }; const haversineKm = (lat1, lon1, lat2, lon2) => { const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); }; const londonCams = allCameras.filter((c) => c.city === 'London').slice(0, 5); const austinCams = allCameras.filter((c) => c.city === 'Austin').slice(0, 5); const sfCams = allCameras .filter((c) => c.provider === 'Caltrans' && haversineKm(c.lat, c.lon, SF_ANCHOR.lat, SF_ANCHOR.lon) < 60) .slice(0, 3); record('found >=5 London (TfL) test cameras', londonCams.length >= 5, `found ${londonCams.length}`); record('found >=5 Austin test cameras', austinCams.length >= 5, `found ${austinCams.length}`); record('found >=3 SF-area (Caltrans) test cameras', sfCams.length >= 3 ? true : null, sfCams.length >= 3 ? `found ${sfCams.length}` : `found ${sfCams.length} — live Caltrans catalog unavailable/partial this run (upstream INCONCLUSIVE, not an app datum failure)`); const cityBuckets = [ { label: 'London', cams: londonCams }, { label: 'Austin', cams: austinCams }, { label: 'SF', cams: sfCams }, ]; const groundRows = []; // for the report/console table for (const { label, cams } of cityBuckets) { for (const cam of cams) { const state = await page.evaluate((id) => { const mod = window.__godsEyeView.dataManager.layers.get('cctv').module; return mod.getUIState().cameras.find((camera) => camera.id === id) || null; }, cam.id); const geom = await readCameraGround(page, cam.id); if (!geom || !Number.isFinite(geom.groundM)) { record(`${label} ${cam.id}: record ground reads finitely`, false, 'ray-tl entity missing or non-finite height'); continue; } record(`${label} ${cam.id}: rendered ground reads finitely`, true, `renderedGround=${geom.groundM.toFixed(1)}m`); const priorM = state?.groundPriorM; if (!Number.isFinite(priorM)) { record(`${label} ${cam.id}: Re:Earth ground prior resolved`, null, `renderedGround=${geom.groundM.toFixed(1)}m; prior unavailable this run`); groundRows.push({ label, id: cam.id, groundM: geom.groundM, priorM: null, reearthM: null }); continue; } const reearth = await fetchReearthEllipsoid(cam.lat, cam.lon); if (reearth === null) { record(`${label} ${cam.id}: |ground prior - Re:Earth ellipsoid| < 6m`, null, `Re:Earth proxy unreachable this run — prior=${priorM.toFixed(1)}m rendered=${geom.groundM.toFixed(1)}m (upstream INCONCLUSIVE, not a harness/app failure)`); groundRows.push({ label, id: cam.id, groundM: geom.groundM, priorM, reearthM: null }); continue; } const delta = Math.abs(priorM - reearth); record(`${label} ${cam.id}: |ground prior - Re:Earth ellipsoid| < 6m`, delta < 6, `prior=${priorM.toFixed(1)}m rendered=${geom.groundM.toFixed(1)}m reearth=${reearth.toFixed(1)}m Δ=${delta.toFixed(1)}m`); groundRows.push({ label, id: cam.id, groundM: geom.groundM, priorM, reearthM: reearth }); } } console.log('\n Ground-height table (city, camera, rendered ground, stored prior, Re:Earth ellipsoid):'); for (const row of groundRows) { console.log(` ${row.label.padEnd(8)} ${row.id.padEnd(24)} rendered=${row.groundM.toFixed(1)}m` + (row.priorM !== null ? ` prior=${row.priorM.toFixed(1)}m` : ' prior=N/A') + (row.reearthM !== null ? ` reearth=${row.reearthM.toFixed(1)}m` : ' reearth=N/A')); } console.log(''); // ----------------------------------------------------------------------- // Google-3D band check: activate the London and Austin cameras nearest // the viewer (mirrors qa-cctv-v2's focusNearest activation pattern), // wait for the one-shot ground snap to complete (tilesLoaded gate), then // assert the ACTIVE camera's ground falls in the brief's band. Bands // (not exact) because the Google tileset surface legitimately differs // from the Re:Earth DEM by building height once the snap refines it. // ----------------------------------------------------------------------- console.log('Checking Google-3D active-camera ground BANDS (post-snap)...'); async function checkActiveCameraBand(cityLabel, targetCamId, band) { await page.evaluate((id) => { window.__godsEyeView.dataManager.layers.get('cctv').module.selectCamera(id, { focus: true, durationSec: 0.1 }); }, targetCamId); await sleep(600); const tilesReady = await waitForTilesLoaded(page, 30000); // Give the one-shot completion pass a beat to land even after tiles // report ready (update() ticks on its own interval — see cctv.js). await sleep(1500); const geom = await readCameraGround(page, targetCamId); if (!tilesReady) { record(`${cityLabel} active camera (${targetCamId}) google-3d ground ∈ [${band[0]},${band[1]}]m`, null, `tiles never reported loaded within 30s under headless GL — environmental, inconclusive (recordGround=${geom?.groundM?.toFixed?.(1) ?? 'n/a'}m)`); return; } if (!geom || !Number.isFinite(geom.groundM)) { record(`${cityLabel} active camera (${targetCamId}) google-3d ground ∈ [${band[0]},${band[1]}]m`, false, 'ground unreadable'); return; } const inBand = geom.groundM >= band[0] && geom.groundM <= band[1]; record(`${cityLabel} active camera (${targetCamId}) google-3d ground ∈ [${band[0]},${band[1]}]m`, inBand, `recordGround=${geom.groundM.toFixed(1)}m`); } if (londonCams.length) { await checkActiveCameraBand('London', londonCams[0].id, [45, 75]); } else { record('London active camera google-3d ground ∈ [45,75]m', null, 'no London camera available to test'); } if (austinCams.length) { await checkActiveCameraBand('Austin', austinCams[0].id, [110, 135]); } else { record('Austin active camera google-3d ground ∈ [110,135]m', null, 'no Austin camera available to test'); } // ========================================================================= // Group 2: Aircraft render altitude // ========================================================================= console.log('Enabling flights layer, waiting for a live aircraft sample...'); await page.evaluate(async () => { const dm = window.__godsEyeView.dataManager; const entry = dm.layers.get('flights'); if (!entry.enabled) await dm.toggle('flights'); }); // OpenSky polls on its own interval; give it real time to land a batch // (the layer polls every ~30s per the documented polling invariant). const gotAircraft = await page.waitForFunction( () => { const mod = window.__godsEyeView.dataManager.layers.get('flights').module; return mod.getAllPositions(1).length > 0; }, { timeout: 45000 } ).then(() => true).catch(() => false); if (!gotAircraft) { record('>=10 live aircraft sampled from OpenSky', null, 'no aircraft appeared within 45s — OpenSky upstream INCONCLUSIVE this run (not an app failure)'); } else { // Let a full second poll cycle land (updateInterval: 30000 — see // flights.js) before sampling. A just-appeared ON-GROUND aircraft can // legitimately render at the sticky `alt` default (0m ellipsoidal) for // ONE tick while `_warmGroundedAircraftSurfaceCache`'s batch resolve is // still in flight (fire-and-forget; picked up by cachedEllipsoidalGround // on a LATER poll — see flights.js's own comment on that function). That // window is a documented, time-bounded product behavior, not a // datum-fix regression — sampling immediately after the FIRST aircraft // appears would catch exactly that transient and misreport it as an // under-terrain violation. Waiting past a second poll interval lets the // cache warm before the under-terrain detector runs. console.log('Waiting through a second poll cycle so on-ground aircraft ground-cache warm-up completes...'); await sleep(32000); // getAllPositions gives render-height (carto.height off the actual // billboard Cartesian3 — i.e. renderAltitudeM as placed by // Cartesian3.fromDegrees(lon, lat, renderAltitudeM)); getNearby gives // the untouched aviation/baro field (info.altitude) per-icao24. Cross // the two public APIs by icao24/id to reconstruct both sides without // reaching into the layer's private _flightData closure. const allPositions = await page.evaluate(() => { const mod = window.__godsEyeView.dataManager.layers.get('flights').module; return mod.getAllPositions(60); }); // getNearby needs an ECEF center + range; use the live camera position // (a real Cartesian3 instance — no window.Cesium global available, so // this borrows a live instance rather than constructing one, matching // qa-cctv-v2's pattern) with an effectively unbounded range so it // returns everything currently shown regardless of viewer position. const nearbyList = await page.evaluate(() => { const mod = window.__godsEyeView.dataManager.layers.get('flights').module; const center = window.__godsEyeView.viewer.camera.position; return mod.getNearby(center, Number.MAX_VALUE, 200).map((a) => ({ icao24: a.icao24, altitudeM: a.altitudeM })); }); const baroById = new Map(nearbyList.map((a) => [a.icao24, a.altitudeM])); const sampleSize = Math.min(allPositions.length, 25); const sample = allPositions.slice(0, sampleSize); record(`>=10 live aircraft sampled from OpenSky (public getAllPositions/getNearby)`, sample.length >= 10 ? true : (sample.length > 0 ? null : false), `sampled ${sample.length} (getAllPositions total=${allPositions.length})`); let plausibleCount = 0; let checkedCount = 0; const underTerrainDetails = []; const baroPlusNDeltas = []; for (const ac of sample) { const renderAltM = ac.altitudeM; // carto.height off the live billboard position const baroM = baroById.get(ac.id); // Finite + plausible sanity: render altitude must be a real number in // a physically sane band (-500m .. 20000m covers everything from // Death-Valley-adjacent ground traffic to high-altitude cruise). const finitePlausible = Number.isFinite(renderAltM) && renderAltM > -500 && renderAltM < 20000; if (finitePlausible) plausibleCount += 1; // Cross-check vs baro+geoidHeight when we have the baro side (weaker // per-aircraft equality check: geoAltitudeM isn't exposed as a named // field on any public flights-layer method — only the module-private // `_flightData` carries geoAltitudeM/renderAltitudeM directly — so // this harness can only recompute the FALLBACK branch's expected // value (baro + geoid N) from the public API and log the delta, // rather than assert it individually, for aircraft where the true // render source was actually geo_altitude, which can legitimately // differ from baro+N by tens of metres). if (Number.isFinite(baroM) && Number.isFinite(renderAltM)) { const n = geoidHeight(ac.latitude, ac.longitude); const expectedBaroPlusN = baroM + n; const deltaVsBaroPlusN = Math.abs(renderAltM - expectedBaroPlusN); // Not asserted individually (geo_altitude legitimately diverges from // baro+N) — logged into the report table so the cross-check is // actually visible, not silently discarded. baroPlusNDeltas.push({ id: ac.id, renderAltM, expectedBaroPlusN, deltaVsBaroPlusN }); } checkedCount += 1; } record(`aircraft render altitudes are finite + physically plausible (-500..20000m)`, checkedCount > 0 ? plausibleCount === checkedCount : null, `${plausibleCount}/${checkedCount} plausible`); if (baroPlusNDeltas.length) { console.log('\n baro+geoidN cross-check table (id, renderAlt, expected baro+N, |Δ|):'); for (const d of baroPlusNDeltas) { console.log(` ${String(d.id).padEnd(10)} renderAlt=${d.renderAltM.toFixed(1)}m` + ` expected=${d.expectedBaroPlusN.toFixed(1)}m Δ=${d.deltaVsBaroPlusN.toFixed(1)}m`); } console.log(''); } // Under-terrain detector, tightened to its unambiguous fingerprint. // Ground truth is fetched fresh through the app's own // /api/terrain/heights proxy (same oracle as group 1), so this stays // GL-independent and upstream-verifiable rather than reaching into // private closures. // // The REAL bug (fixed by this task's Bug 1 change: flights.js's // _warmGroundedAircraftSurfaceCache now keys its warm-cache resolve off // the aircraft's RAW POLL lat/lon — the same coords // pickRenderAltitudeM's surfaceM read uses — instead of the // continuously dead-reckoned billboard position) has an unambiguous // numeric fingerprint: an aircraft rendering at the ~0m ellipsoidal // SENTINEL (the pre-existing "no geo/baro data yet" default) while its // terrain ground is nowhere near 0m. HARD-FAIL only on that exact // combination. // // Any OTHER under-terrain reading (non-zero renderAltitudeM below // ground - 50m) is ambiguous — it could legitimately be a climbing/ // descending aircraft briefly near real ground elevation close to an // airport (e.g. a departure a few dozen metres up over a field that // itself sits well above sea level) — so those are recorded // INCONCLUSIVE with the raw values, not FAILed. const SENTINEL_EPS_M = 1; // Math.abs(renderAlt) < 1 is the exact 0m default const SENTINEL_GROUND_FLOOR_M = 100; // terrain must be unambiguously non-zero let underTerrainChecked = 0; let sentinelViolations = 0; let ambiguousUnderTerrain = 0; const ambiguousDetails = []; for (const gc of sample) { const ground = await fetchReearthEllipsoid(gc.latitude, gc.longitude); if (ground === null) continue; // upstream unreachable for this point — skip, don't fail underTerrainChecked += 1; if (gc.altitudeM >= ground - 50) continue; // not under terrain at all const isSentinelMiss = Math.abs(gc.altitudeM) < SENTINEL_EPS_M && Math.abs(ground) > SENTINEL_GROUND_FLOOR_M; if (isSentinelMiss) { sentinelViolations += 1; underTerrainDetails.push(`${gc.id}: renderAlt=${gc.altitudeM.toFixed(1)}m ground=${ground.toFixed(1)}m (0m SENTINEL fingerprint)`); } else { ambiguousUnderTerrain += 1; ambiguousDetails.push(`${gc.id}: renderAlt=${gc.altitudeM.toFixed(1)}m ground=${ground.toFixed(1)}m`); } } record('no aircraft renders at the 0m sentinel while terrain ground is far from 0m (datum-miss fingerprint)', underTerrainChecked > 0 ? sentinelViolations === 0 : null, underTerrainChecked > 0 ? `${sentinelViolations} violation(s) out of ${underTerrainChecked} checked${underTerrainDetails.length ? ': ' + underTerrainDetails.slice(0, 3).join(' | ') : ''}` : 'Re:Earth proxy unreachable for every sampled point this run — INCONCLUSIVE'); if (ambiguousUnderTerrain > 0) { record('other under-terrain readings (non-zero, ambiguous — may be legit climb/descent near an airport)', null, `${ambiguousUnderTerrain} case(s): ${ambiguousDetails.slice(0, 5).join(' | ')}`); } } // ========================================================================= // Group 3: Regime C re-resolve // ========================================================================= console.log('Switching map stack to keyless OSM (regime C)...'); // Grab a London camera's ground BEFORE the switch (still whatever regime // we were in — Google-3D, given the app's default). const regimeCTargetId = londonCams[0]?.id || austinCams[0]?.id || allCameras[0]?.id; const groundBeforeSwitch = regimeCTargetId ? await readCameraGround(page, regimeCTargetId) : null; await page.evaluate(async () => { await window.__godsEyeView.mapStackController.setStack('osm'); }); // Real (non-flat) terrain loads ASYNCHRONOUSLY on a globe stack, and by two // different code paths depending on whether a Cesium Ion token is present: // - NO token → keyless Re:Earth `CesiumTerrainProvider` via a DIRECT // assignment (synchronous — ready immediately). // - token → Cesium World Terrain via `scene.setTerrain(...)`, whose // provider resolves in the background: `viewer.terrainProvider` // is transiently `null` until the World Terrain layer.json // loads (can exceed 1.5 s under SwiftShader), THEN becomes a // `CesiumTerrainProvider`. // Either way the END STATE is a real `CesiumTerrainProvider`, never the flat // `EllipsoidTerrainProvider` regime C used to be stuck on. Poll for that end // state instead of a fixed sleep (a fixed sleep races the async World Terrain // load — the old failure mode). Report whatever it settled on. let terrainCtor = null; try { await page.waitForFunction(() => { const p = window.__godsEyeView?.viewer?.terrainProvider; const name = p && p.constructor && p.constructor.name; return !!name && name !== 'EllipsoidTerrainProvider'; }, { timeout: 12000, polling: 250 }); } catch { /* timed out — fall through and report the settled ctor below */ } terrainCtor = await page.evaluate(() => { const provider = window.__godsEyeView.viewer.terrainProvider; return provider?.constructor?.name || null; }); record('globe-stack terrain is a real CesiumTerrainProvider, not the flat EllipsoidTerrainProvider (regime B/C)', terrainCtor !== null && terrainCtor !== 'EllipsoidTerrainProvider', `constructor=${terrainCtor}`); if (regimeCTargetId) { const groundAfterSwitch = await readCameraGround(page, regimeCTargetId); if (!groundBeforeSwitch || !groundAfterSwitch || !Number.isFinite(groundBeforeSwitch.groundM) || !Number.isFinite(groundAfterSwitch.groundM)) { record(`CCTV grounds re-resolved for regime C (${regimeCTargetId})`, false, 'ground unreadable before or after switch'); } else { // The prior-everywhere invariant (Task 5) means BOTH readings should // already be close to the Re:Earth ellipsoid value in every regime — // the real regression this guards is the OLD fabricated-prior bug // (London ~15m, Austin ~150m fixed catalog numbers regardless of // regime). Assert the post-switch ground is NOT stuck at either // known-bad fabricated constant, and is within a generous tolerance // of the pre-switch value (both should already reflect the same // Re:Earth prior — a big jump would indicate a stale/blank regime // record instead of a re-arm). const isLondonId = londonCams.some((c) => c.id === regimeCTargetId); const staleValue = isLondonId ? 15 : 150; const notStuckAtFabricated = Math.abs(groundAfterSwitch.groundM - staleValue) > 5; const stableAcrossSwitch = Math.abs(groundAfterSwitch.groundM - groundBeforeSwitch.groundM) < 20; record(`CCTV ground NOT stuck at the old fabricated prior (${staleValue}m) after regime C switch (${regimeCTargetId})`, notStuckAtFabricated, `before=${groundBeforeSwitch.groundM.toFixed(1)}m after=${groundAfterSwitch.groundM.toFixed(1)}m`); record(`CCTV ground re-resolved within one event cycle, consistent with the prior-everywhere invariant (${regimeCTargetId})`, stableAcrossSwitch, `Δ=${Math.abs(groundAfterSwitch.groundM - groundBeforeSwitch.groundM).toFixed(1)}m`); } } else { record('CCTV grounds re-resolved for regime C', null, 'no test camera available'); } // ----------------------------------------------------------------------- // Screenshot for human visual review (not a pass/fail gate). // ----------------------------------------------------------------------- await sleep(500); const shot = path.join(SHOTS_DIR, 'height-datum-regimeC.png'); await page.screenshot({ path: shot }); console.log(` screenshot (visual review only, not a gate) → ${path.relative(REPO_ROOT, shot)}`); record('no console errors during height-datum exercise', consoleErrors.length === 0, consoleErrors.length ? consoleErrors.slice(0, 3).join(' | ') : 'clean'); for (const r of results) { if (r.ok === false) exitCode = 1; } } finally { await browser.close(); } console.log('\n' + '─'.repeat(60)); const pass = results.filter((r) => r.ok === true).length; const fail = results.filter((r) => r.ok === false).length; const inconclusive = results.filter((r) => r.ok === null).length; console.log(` RESULT: ${pass} passed, ${fail} failed, ${inconclusive} inconclusive`); console.log('─'.repeat(60) + '\n'); process.exit(exitCode); } main().catch((e) => { console.error('\x1b[31mHarness error:\x1b[0m', e); process.exit(3); });