4066 lines
215 KiB
JavaScript
4066 lines
215 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* track-regression.mjs
|
||
*
|
||
* DETERMINISTIC regression harness for 3D flight TRACKING.
|
||
*
|
||
* Background: tracked-plane 3D has regressed FOUR times (flicker → freeze →
|
||
* jitter → pull-out → cross-layer orphan), each fix sometimes revealing the
|
||
* next. This script locks the invariants so they can't silently break again.
|
||
*
|
||
* It drives the REAL app (http://localhost:4173) in headless Chromium with the
|
||
* same WebGL launch flags the existing Cesium render harness uses
|
||
* (tools/cesium-render.mjs): --use-gl=angle / --use-angle=swiftshader.
|
||
*
|
||
* DETERMINISM: it does NOT depend on live OpenSky / adsb.lol / AISStream
|
||
* (all optional or rate-limited in local QA). Instead it installs a persistent
|
||
* `fetch` shim in the page that intercepts the poll endpoints and returns
|
||
* SYNTHETIC aircraft positioned near the camera, in the EXACT upstream payload
|
||
* shapes the layers parse:
|
||
* - flights → GET /api/opensky → { states: [ <state-vector[]> ] }
|
||
* - military → GET /api/adsblol/mil → { ac: [ <adsblol-aircraft{}> ] }
|
||
* - vessels → GET /api/ais-live → connected, zero-row snapshot
|
||
* The shim stays installed so the layers' setInterval pollers keep the
|
||
* synthetic planes alive (under MISSING_POLL_LIMIT=3 so they're never pruned).
|
||
*
|
||
* The four invariants, each one assertion block:
|
||
* 1. NO JITTER — the tracked plane's 3D MODEL position and its
|
||
* getDetectableObjects() (HUD/detection) position use
|
||
* the SAME per-frame value. We sample both over ~30
|
||
* frames and assert max|visual-centre−detectable| ≈ 0.
|
||
* 2. NO PULL-OUT — tracking plane B while tracking A must not fly the
|
||
* camera to a high overview (deselect/switch never
|
||
* moves the camera — it stays in the follow band).
|
||
* 3. NO CROSS-LAYER — tracking military then commercial (and vice versa)
|
||
* ORPHAN clears the FIRST layer (its getTrackedInfo()===null),
|
||
* via the per-layer viewer.trackedEntityChanged listener.
|
||
* 4. INIT CLEAN — app loads, all 12 layers present, no console errors,
|
||
* [Detection] Initialized + [TrackedReadout] Initialized.
|
||
*
|
||
* Plus two pre-ship-audit (2026-07-01) regressions appended at the end:
|
||
* H1. CLICK-OWN-PLANE — the tracked standalone model exposes its pick id,
|
||
* and a real mouse click on the tracked plane is a
|
||
* NO-OP (no deselect, no camera spike).
|
||
* M3. AGE-OUT RELEASE — when the tracked plane's fixes stop arriving
|
||
* (3 missed polls via the shim), tracking clears and
|
||
* the camera is RELEASED IN PLACE: viewer.trackedEntity
|
||
* undefined, NO jump (owner decision 2026-07-02 —
|
||
* the old ~80 km overview flyTo is gone).
|
||
*
|
||
* And the landing-ghost polish (2026-07-02): a LOW+SLOW (landed) plane that
|
||
* vanishes from the feed is removed after ONE missed poll, while a cruise
|
||
* plane keeps the full 3-poll grace (it absorbs real feed gaps); the tracked
|
||
* readout carries a "· STALE" cue while a tracked plane coasts through its
|
||
* missed-poll grace, and drops it when the plane reappears.
|
||
*
|
||
* And the ground-traffic feature (2026-07-03, owner reversal): present-but-
|
||
* grounded planes render FULL-STRENGTH in the airborne tint pipeline
|
||
* (white / amber-military; the day-1 gray mute was killed the same day) at
|
||
* ×0.8 scale and stay detectable; the on_ground flip restyles the SAME
|
||
* billboard in place (landing/takeoff transition — never a removal); a
|
||
* grounded plane that then vanishes from the feed still fast-culls after one
|
||
* missed poll (both layers; military keys off adsb.lol's alt_baro === "ground").
|
||
* Ground billboards render depth-test-free (disableDepthTestDistance = ∞) so
|
||
* the photoreal tile skin can't bury them up close; takeoff restores the test.
|
||
*
|
||
* And GROUND 3D (2026-07-03, owner decision LOCKED: "when I have 3D mode —
|
||
* proximity or all — I want that respected regardless of whether a plane is
|
||
* on the ground or in the air. No distinction."): a synthetic on_ground plane
|
||
* is model-ELIGIBLE and gets a model under the existing cap (both layers); its
|
||
* model matrix height reads the ONE-SHOT cached ground snap — a stubbed
|
||
* scene.sampleHeight (qa-cctv-drift-b9b style) + the per-layer belly offset —
|
||
* never the buried/floating feed altitude, and never re-samples per frame;
|
||
* the TRACKED grounded plane gets the standalone tracked model too (the
|
||
* owner's "tracked SWA143 at 0 kts showed a 2D billboard" case).
|
||
*
|
||
* And arrival-rotation freshness (2026-07-03 ground-polish): camera.moveEnd
|
||
* forces a full rotation pass on the next frame (fly-to settles the
|
||
* pose-signature gate can eat), and a billboard flipping INTO view while the
|
||
* camera idles re-aims the same tick instead of wearing a stale nose for up
|
||
* to ROTATION_REFRESH_MS. Both layers.
|
||
*
|
||
* Run: npm run test:track (assumes dev server already up on :4173)
|
||
* node scripts/track-regression.mjs
|
||
*
|
||
* Exits non-zero if ANY invariant fails. DOES NOT COMMIT anything.
|
||
*
|
||
* Flags:
|
||
* --url <url> App URL (default http://localhost:4173)
|
||
* --headful Show the browser (debugging)
|
||
* --keep-open Leave the browser open after the run (debugging)
|
||
*/
|
||
|
||
import fs from 'node:fs';
|
||
import puppeteer from 'puppeteer';
|
||
import { classifyAircraft, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
|
||
import { ensureGeoidReady, geoidHeight } from '../src/data/geoid.js';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Args
|
||
// ---------------------------------------------------------------------------
|
||
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 APP_URL = getOpt('--url', 'http://localhost:4173');
|
||
const APP_ORIGIN = new URL(APP_URL).origin;
|
||
const HEADFUL = getFlag('--headful');
|
||
const KEEP_OPEN = getFlag('--keep-open');
|
||
|
||
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 inaccessible candidates and let Puppeteer fall back to its cache.
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Pretty PASS/FAIL reporting
|
||
// ---------------------------------------------------------------------------
|
||
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 skip(name, why) {
|
||
results.push({ name, ok: null, detail: why });
|
||
console.log(` [\x1b[33mSKIP\x1b[0m] ${name} — ${why}`);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Synthetic aircraft (positioned near Austin, the app's load view).
|
||
// Commercial ICAOs deliberately NOT in any known-military set so the flights
|
||
// layer doesn't suppress them; military hexes are whatever adsb.lol returns and
|
||
// the military layer registers them itself on parse.
|
||
// ---------------------------------------------------------------------------
|
||
const SYNTH = {
|
||
// Austin load view is (-97.7431, 30.2672). Spread a few planes a few km apart.
|
||
flights: [
|
||
// [icao24, callsign, country, time_position, last_contact, lon, lat, baro_alt, on_ground, velocity, true_track]
|
||
{ icao: 'aaa001', callsign: 'SYN001', lon: -97.7431, lat: 30.2672, alt: 9000, vel: 230, track: 90 },
|
||
{ icao: 'aaa002', callsign: 'SYN002', lon: -97.7600, lat: 30.2800, alt: 9500, vel: 210, track: 45 },
|
||
{ icao: 'aaa003', callsign: 'SYN003', lon: -97.7300, lat: 30.2550, alt: 8700, vel: 250, track: 135 },
|
||
],
|
||
// Two real, well-formed TLEs (ISS + Hubble). SGP4 needs a valid element set;
|
||
// these are static public catalog entries, so the propagated positions are
|
||
// reproducible run to run.
|
||
tle: [
|
||
'ISS (ZARYA)',
|
||
'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9004',
|
||
'2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.49814310 12345',
|
||
'HST',
|
||
'1 20580U 90037B 24001.50000000 .00001250 00000-0 67000-4 0 9992',
|
||
'2 20580 28.4700 288.8102 0002613 321.7208 133.2117 15.09299130 12345',
|
||
'',
|
||
].join('\n'),
|
||
// One dense (Starlink-shell) element set, so dense mode has a real subject
|
||
// to track. Same orbital elements as the ISS entry with a distinct satnum,
|
||
// which keeps SGP4 valid and the propagation reproducible.
|
||
denseTle: [
|
||
'STARLINK-1007',
|
||
'1 44713U 19074A 24001.50000000 .00016717 00000-0 10270-3 0 9004',
|
||
'2 44713 53.0000 247.4627 0006703 130.5360 325.0288 15.06000000 12345',
|
||
'',
|
||
].join('\n'),
|
||
military: [
|
||
// adsb.lol record fields: hex, flight, lon, lat, alt_baro (ft), track, gs (kt), t, r, ownOp, seen_pos
|
||
{ hex: 'bbb101', flight: 'MIL101', lon: -97.7500, lat: 30.2700, altFt: 28000, track: 270, gsKt: 420, t: 'F16', r: 'AF-101' },
|
||
{ hex: 'bbb102', flight: 'MIL102', lon: -97.7350, lat: 30.2600, altFt: 26000, track: 315, gsKt: 400, t: 'F18', r: 'AF-102' },
|
||
],
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Main
|
||
// ---------------------------------------------------------------------------
|
||
async function main() {
|
||
console.log(`\n3D Tracking Regression Harness`);
|
||
console.log(` App URL : ${APP_URL}`);
|
||
console.log(` Mode : ${HEADFUL ? 'headful' : 'headless'}\n`);
|
||
|
||
// Verify the dev server is up before launching a browser.
|
||
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`);
|
||
console.error(`Start it first: ./scripts/dev-fresh.sh (or: npm run dev)`);
|
||
process.exit(2);
|
||
}
|
||
|
||
const chromeExecutable = findChromeExecutable();
|
||
if (chromeExecutable) {
|
||
console.log(` Browser : ${chromeExecutable}\n`);
|
||
}
|
||
|
||
const browser = await puppeteer.launch({
|
||
headless: HEADFUL ? false : 'new',
|
||
// This machine intermittently stalls single CDP calls past the 180 s
|
||
// default under SwiftShader load (same hardening as qa-focus-evidence).
|
||
protocolTimeout: 300_000,
|
||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
||
args: [
|
||
'--no-sandbox',
|
||
'--disable-setuid-sandbox',
|
||
// SwiftShader only in headless: forcing it in headful defeats the whole
|
||
// point of --headful (real GPU). Diagnosed 2026-08-03: under this
|
||
// machine's Chrome 145 SwiftShader, loading the tracked military GLB
|
||
// kills frame production entirely (rAF stops while timers/evals stay
|
||
// alive), wedging any rAF-awaiting evaluation. Same policy as
|
||
// qa-focus-evidence: headful = real GPU.
|
||
...(HEADFUL ? [] : ['--use-gl=angle', '--use-angle=swiftshader']),
|
||
'--disable-dev-shm-usage',
|
||
'--disable-web-security',
|
||
'--disable-background-timer-throttling',
|
||
'--disable-renderer-backgrounding',
|
||
// macOS stops frame production for fully-occluded windows, which kills
|
||
// rAF (and thus every rAF-awaiting evaluation) in headful runs whose
|
||
// window opens behind others — the qa-focus-evidence bringToFront
|
||
// lesson, enforced belt-and-braces here.
|
||
'--disable-backgrounding-occluded-windows',
|
||
'--window-size=1280,800',
|
||
],
|
||
});
|
||
|
||
const consoleErrors = [];
|
||
const failedResponses = [];
|
||
const sawLog = { detection: false, readout: false };
|
||
|
||
try {
|
||
const page = await browser.newPage();
|
||
await page.setViewport({ width: 1280, height: 800 });
|
||
await page.setRequestInterception(true);
|
||
page.on('request', (request) => {
|
||
const url = new URL(request.url());
|
||
if (url.origin === APP_ORIGIN && url.pathname === '/api/openai/hud-summary') {
|
||
request.respond({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ summary: 'QA globe ready' }),
|
||
});
|
||
return;
|
||
}
|
||
request.continue();
|
||
});
|
||
|
||
page.on('console', (msg) => {
|
||
const text = msg.text();
|
||
const type = msg.type();
|
||
if (text.includes('[Detection] Initialized')) sawLog.detection = true;
|
||
if (text.includes('[TrackedReadout] Initialized')) sawLog.readout = true;
|
||
if (type === 'error') {
|
||
// Ignore benign network resource 404s (e.g. an un-shimmed upstream
|
||
// endpoint with no live data, a missing tile/favicon). Those are
|
||
// environmental noise, not a tracking-invariant regression. Real JS
|
||
// errors (TypeError, unhandled rejection, etc.) are still captured.
|
||
const isBenign404 = /Failed to load resource.*404/i.test(text);
|
||
const sourceUrl = msg.location()?.url || '';
|
||
if (!isBenign404) consoleErrors.push(sourceUrl ? `${text} [${sourceUrl}]` : text);
|
||
}
|
||
// Surface a trace for debugging, but keep it quiet.
|
||
if (process.env.GEV_TEST_VERBOSE) console.log(` [page:${type}] ${text}`);
|
||
});
|
||
page.on('pageerror', (err) => consoleErrors.push(`pageerror: ${err.message}`));
|
||
page.on('response', (response) => {
|
||
if (response.status() >= 500) {
|
||
failedResponses.push(`HTTP ${response.status()} ${response.url()}`);
|
||
}
|
||
});
|
||
|
||
// ---- Install the synthetic-fetch shim BEFORE any app code runs ----------
|
||
// Must be in place before the layers' first update() fires fetch(API_URL).
|
||
await page.evaluateOnNewDocument((synth, appOrigin) => {
|
||
// Stash the planes so the page can mutate them later (e.g. to advance
|
||
// positions), and so we can confirm the shim is live.
|
||
window.__SYNTH = synth;
|
||
window.__SYNTH_HITS = { opensky: 0, mil: 0 };
|
||
|
||
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 requestUrl = typeof input === 'string' || input instanceof URL
|
||
? String(input)
|
||
: input?.url;
|
||
const url = new URL(requestUrl, window.location.href);
|
||
const isAppRequest = url.origin === appOrigin;
|
||
// OpenSky (commercial flights): { states: [ state-vector[] ] }
|
||
if (isAppRequest && url.pathname === '/api/opensky') {
|
||
window.__SYNTH_HITS.opensky++;
|
||
// A withheld contact models the ordinary case where the recipient's
|
||
// first authoritative refresh does not yet carry the shared aircraft.
|
||
const withheld = (() => {
|
||
try { return window.sessionStorage.getItem('__gevWithhold') || ''; } catch { return ''; }
|
||
})();
|
||
const states = window.__SYNTH.flights
|
||
.filter((f) => f.icao !== withheld)
|
||
.map((f) => ([
|
||
f.icao, // 0 icao24
|
||
f.callsign, // 1 callsign
|
||
'Synthetica', // 2 origin_country
|
||
Math.floor(Date.now() / 1000), // 3 time_position
|
||
Math.floor(Date.now() / 1000), // 4 last_contact
|
||
f.lon, // 5 longitude
|
||
f.lat, // 6 latitude
|
||
f.alt, // 7 baro_altitude (m)
|
||
f.onGround === true, // 8 on_ground (ground-traffic phase flips this live)
|
||
f.vel, // 9 velocity (m/s)
|
||
f.track, // 10 true_track (deg)
|
||
0, null, null, null, false, 0, // padding to match state-vector length
|
||
]));
|
||
return Promise.resolve(jsonResponse({ time: Math.floor(Date.now() / 1000), states }));
|
||
}
|
||
// Trail backfill (fires on trackById). Stub with valid-but-empty
|
||
// payloads so the deterministic run never 404s against the dev proxy.
|
||
// flights: { path: [ [time, lat, lon, baroAlt, true_track, on_ground] ] }
|
||
if (isAppRequest && url.pathname === '/api/opensky-track') {
|
||
return Promise.resolve(jsonResponse({ path: [] }));
|
||
}
|
||
// military: { timestamp: <epochSec>, trace: [ [secAfter, lat, lon, ...] ] }
|
||
if (isAppRequest && url.pathname === '/api/adsblol/trace') {
|
||
return Promise.resolve(jsonResponse({ timestamp: Math.floor(Date.now() / 1000), trace: [] }));
|
||
}
|
||
// adsbdb enrichment (fires for tracked/model-eligible planes): empty
|
||
// object → typeCode stays null → the synthetic planes' class (and so
|
||
// their 3D scale + ground-snap belly offset) is DETERMINISTIC, never a
|
||
// live-proxy lookup of a fake hex.
|
||
if (isAppRequest && /^\/api\/adsbdb\/(?:type|route)\/[^/]+$/.test(url.pathname)) {
|
||
return Promise.resolve(jsonResponse({}));
|
||
}
|
||
// Contacts enables the optional AIS source alongside aircraft. Keep
|
||
// this tracking harness independent of local AIS credentials while
|
||
// preserving the truthful "awaiting first message" lifecycle.
|
||
if (isAppRequest && url.pathname === '/api/ais-live') {
|
||
return Promise.resolve(jsonResponse({
|
||
status: 'connected',
|
||
rows: [],
|
||
lastMessageAt: null,
|
||
}));
|
||
}
|
||
// CelesTrak TLE groups. Served from a fixed two-satellite catalog so
|
||
// the satellite assertions are deterministic and the run never depends
|
||
// on an upstream that rate-limits (it was answering 403 the night this
|
||
// was written).
|
||
if (isAppRequest && url.pathname.startsWith('/api/celestrak/')) {
|
||
const group = url.pathname.slice('/api/celestrak/'.length);
|
||
// One named group fails on demand. Failing a group the subject is
|
||
// NOT in is the reclassification case: CelesTrak moves satellites
|
||
// between groups, so a subject missing from its old group may have
|
||
// moved into the one that just failed.
|
||
if (window.__SYNTH.failGroup && group === window.__SYNTH.failGroup) {
|
||
return Promise.resolve(new Response('upstream unavailable', { status: 503 }));
|
||
}
|
||
if (group === 'starlink') {
|
||
// A dense load that FAILS is the case that erases the dense
|
||
// request: settling a failure reverts the catalog mode to core.
|
||
if (window.__SYNTH.failDense) {
|
||
return Promise.resolve(new Response('upstream unavailable', { status: 503 }));
|
||
}
|
||
return Promise.resolve(new Response(window.__SYNTH.denseTle, {
|
||
status: 200, headers: { 'Content-Type': 'text/plain' },
|
||
}));
|
||
}
|
||
// EVERY core group serves the same catalog. Dedupe keeps the first
|
||
// (stations) tag, so the catalog is unchanged — but no group is
|
||
// EMPTY, and an empty group counts as failed, which would make an
|
||
// `accepted` refresh (and therefore any release) unreachable here.
|
||
return Promise.resolve(new Response(window.__SYNTH.tle, {
|
||
status: 200, headers: { 'Content-Type': 'text/plain' },
|
||
}));
|
||
}
|
||
// Voice scene context reverse-geocodes the view target and asks Places
|
||
// for nearby names. Answer both with empty results: the entity-context
|
||
// assertions are about the SELECTED contact, and a live lookup would
|
||
// make the run non-hermetic (and bill the owner's Google quota).
|
||
if (url.hostname === 'maps.googleapis.com' && url.pathname.startsWith('/maps/api/geocode')) {
|
||
return Promise.resolve(jsonResponse({ status: 'ZERO_RESULTS', results: [] }));
|
||
}
|
||
if (isAppRequest && url.pathname === '/api/google/nearby-places') {
|
||
return Promise.resolve(jsonResponse({ places: [] }));
|
||
}
|
||
// adsb.lol military: { ac: [ aircraft{} ] } (must come AFTER /trace check)
|
||
if (isAppRequest && url.pathname === '/api/adsblol/mil') {
|
||
window.__SYNTH_HITS.mil++;
|
||
const ac = window.__SYNTH.military.map((m) => ({
|
||
hex: m.hex,
|
||
flight: m.flight,
|
||
lon: m.lon,
|
||
lat: m.lat,
|
||
alt_baro: m.altFt,
|
||
track: m.track,
|
||
gs: m.gsKt,
|
||
t: m.t,
|
||
r: m.r,
|
||
ownOp: 'SYNTH AF',
|
||
seen_pos: 0,
|
||
}));
|
||
return Promise.resolve(jsonResponse({ msg: 'No error', now: Date.now(), ac }));
|
||
}
|
||
return realFetch(input, init);
|
||
};
|
||
}, SYNTH, APP_ORIGIN);
|
||
|
||
console.log('Loading app...');
|
||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||
if (HEADFUL) await page.bringToFront();
|
||
|
||
// Wait for the app to expose its globals (Cesium viewer + dataManager).
|
||
await page.waitForFunction(
|
||
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.dataManager,
|
||
{ timeout: 60000, polling: 200 }
|
||
);
|
||
console.log(' App globals ready.\n');
|
||
|
||
// Small helper: run an expression in the page after the next render frame,
|
||
// so reads happen on a settled frame.
|
||
const evalPage = (fn, ...args) => page.evaluate(fn, ...args);
|
||
|
||
// ============================================================
|
||
// INVARIANT 4: INIT CLEAN (run first — establishes baseline)
|
||
// ============================================================
|
||
console.log('Invariant 4 — init clean');
|
||
// Give the app a moment to finish first-frame init (detection/readout log).
|
||
await page.waitForFunction(
|
||
() => {
|
||
const dm = window.__godsEyeView.dataManager;
|
||
return dm && dm.layers && dm.layers.size >= 12;
|
||
},
|
||
{ timeout: 30000, polling: 200 }
|
||
).catch(() => {});
|
||
|
||
const layerInfo = await evalPage(() => {
|
||
const dm = window.__godsEyeView.dataManager;
|
||
return {
|
||
count: dm.layers.size,
|
||
ids: [...dm.layers.keys()],
|
||
hasFlights: dm.layers.has('flights'),
|
||
hasMilitary: dm.layers.has('military'),
|
||
};
|
||
});
|
||
record('init: >=12 layers registered', layerInfo.count >= 12,
|
||
`${layerInfo.count} layers [${layerInfo.ids.join(', ')}]`);
|
||
record('init: flights + military layers present',
|
||
layerInfo.hasFlights && layerInfo.hasMilitary,
|
||
`flights=${layerInfo.hasFlights} military=${layerInfo.hasMilitary}`);
|
||
|
||
// Detection + TrackedReadout init logs are emitted during app boot.
|
||
// Give them a beat in case they fire just after globals appear.
|
||
await waitFor(() => sawLog.detection && sawLog.readout, 8000).catch(() => {});
|
||
record('init: [Detection] Initialized logged', sawLog.detection);
|
||
record('init: [TrackedReadout] Initialized logged', sawLog.readout);
|
||
|
||
record('init: no console errors during boot', consoleErrors.length === 0,
|
||
consoleErrors.length ? `${consoleErrors.length} error(s): ${consoleErrors.slice(0, 3).join(' | ')}` : 'clean');
|
||
|
||
// Independent GLB capability control. Product model absence cannot prove
|
||
// that the browser backend is incapable: that is the condition this
|
||
// harness is meant to catch. Load the already-approved airplane asset
|
||
// directly through Cesium, outside the Flights/Military model pipelines,
|
||
// then remove the control before exercising product behavior.
|
||
const glbControlStarted = await evalPage(async () => {
|
||
window.__qaGlbControl = null;
|
||
window.__qaGlbControlError = null;
|
||
const asset = await fetch('/models/airplane.glb');
|
||
const assetBytes = asset.ok ? (await asset.arrayBuffer()).byteLength : 0;
|
||
if (!asset.ok || assetBytes === 0) {
|
||
throw new Error(`GLB control asset unavailable: HTTP ${asset.status}`);
|
||
}
|
||
const Cesium = await import('/node_modules/cesium/Build/Cesium/index.js');
|
||
const viewer = window.__godsEyeView.viewer;
|
||
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(
|
||
Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 9000),
|
||
);
|
||
void Cesium.Model.fromGltfAsync({
|
||
url: '/models/airplane.glb',
|
||
modelMatrix,
|
||
scale: 1,
|
||
}).then((model) => {
|
||
model.id = 'qa:independent-glb-control';
|
||
window.__qaGlbControl = viewer.scene.primitives.add(model);
|
||
viewer.scene.requestRender();
|
||
}).catch((error) => {
|
||
window.__qaGlbControlError = String(error?.message || error);
|
||
});
|
||
return true;
|
||
}).catch((error) => {
|
||
throw new Error(`Independent GLB capability control could not start: ${error.message}`);
|
||
});
|
||
const glbBackendCapable = glbControlStarted && await page.waitForFunction(() => {
|
||
const control = window.__qaGlbControl;
|
||
window.__godsEyeView.viewer.scene.requestRender();
|
||
return Boolean(control?.ready);
|
||
}, { timeout: 40000, polling: 250 }).then(() => true).catch(() => false);
|
||
const glbControlDetail = await evalPage((capable) => {
|
||
const viewer = window.__godsEyeView.viewer;
|
||
const control = window.__qaGlbControl;
|
||
const error = window.__qaGlbControlError;
|
||
if (control) viewer.scene.primitives.remove(control);
|
||
delete window.__qaGlbControl;
|
||
delete window.__qaGlbControlError;
|
||
return capable ? 'ready' : (error || 'did not become ready');
|
||
}, glbBackendCapable);
|
||
console.log(` GLB capability control: ${glbBackendCapable ? 'ready' : 'unavailable'} (${glbControlDetail})`);
|
||
|
||
// Hermeticity (round 5, 2026-07-06): the mesh-floor sampler probes the
|
||
// REAL scene once per cell per poll. Under headless GL, Google tiles
|
||
// sometimes stream at the synthetic-plane coords and sometimes don't, so
|
||
// billboard datums varied run-to-run and flaked the ground/arrival
|
||
// groups. Stub sampleHeight to "no tiles anywhere" for the whole run; the
|
||
// ground-3d group REPLACES this with its counting 187.5 stub, restoring
|
||
// the deterministic pre-round-5 datums everywhere else.
|
||
await evalPage(() => {
|
||
window.__godsEyeView.viewer.scene.sampleHeight = () => undefined;
|
||
});
|
||
|
||
// ============================================================
|
||
// Enable flights + military layers and feed synthetic data.
|
||
// ============================================================
|
||
console.log('\nEnabling flights + military layers (synthetic feed)...');
|
||
const enabled = await evalPage(async () => {
|
||
const dm = window.__godsEyeView.dataManager;
|
||
// setEnabled→toggle→init→enable→update(immediate fetch, shimmed).
|
||
await dm.setEnabled('flights', true);
|
||
await dm.setEnabled('military', true);
|
||
// Drive 3D through the actual DISPLAY-rail control. This preserves the
|
||
// user-facing ownership seam and proves that one toggle drives both
|
||
// commercial and military layers before model-capacity assertions run.
|
||
//
|
||
// Since 2026-08-22 the toggle DEFAULTS ON, so the former "click it if it
|
||
// is dark" arming step would have become a no-op and this check would
|
||
// have quietly degraded into a restatement of the default. Drive a full
|
||
// round trip instead — OFF, then back ON — so both directions are proven
|
||
// through the real control no matter which way the default points.
|
||
const fl = dm.layers.get('flights').module;
|
||
const mil = dm.layers.get('military').module;
|
||
const models3dButton = document.getElementById('models3d-toggle');
|
||
const models3dOff = { button: null, fl: null, mil: null };
|
||
if (models3dButton) {
|
||
if (!models3dButton.classList.contains('active')) models3dButton.click();
|
||
models3dButton.click(); // → OFF: both layers must follow the one control
|
||
models3dOff.button = models3dButton.classList.contains('active');
|
||
models3dOff.fl = fl.getParams().models3d;
|
||
models3dOff.mil = mil.getParams().models3d;
|
||
models3dButton.click(); // → ON: the state the rest of the run needs
|
||
}
|
||
// Force one more update each so freshly-shimmed data is ingested.
|
||
await fl.update(window.__godsEyeView.viewer);
|
||
await mil.update(window.__godsEyeView.viewer);
|
||
return {
|
||
flEnabled: dm.isEnabled('flights'),
|
||
milEnabled: dm.isEnabled('military'),
|
||
flStats: fl.getStats(),
|
||
milStats: mil.getStats(),
|
||
models3dButtonActive: models3dButton?.classList.contains('active') === true,
|
||
flModels3d: fl.getParams().models3d,
|
||
milModels3d: mil.getParams().models3d,
|
||
models3dOff,
|
||
hits: window.__SYNTH_HITS,
|
||
};
|
||
});
|
||
console.log(` flights enabled=${enabled.flEnabled} count=${enabled.flStats.count} | military enabled=${enabled.milEnabled} count=${enabled.milStats.count}`);
|
||
console.log(` shim hits: opensky=${enabled.hits.opensky} mil=${enabled.hits.mil}`);
|
||
|
||
const injectionWorked = enabled.flStats.count > 0 && enabled.milStats.count > 0;
|
||
record('inject: synthetic flights ingested (count>0)', enabled.flStats.count > 0,
|
||
`flights count=${enabled.flStats.count}`);
|
||
record('inject: synthetic military ingested (count>0)', enabled.milStats.count > 0,
|
||
`military count=${enabled.milStats.count}`);
|
||
record('DISPLAY-rail 3D toggle drives both aircraft layers, both directions',
|
||
enabled.models3dButtonActive && enabled.flModels3d && enabled.milModels3d
|
||
&& enabled.models3dOff.button === false
|
||
&& enabled.models3dOff.fl === false && enabled.models3dOff.mil === false,
|
||
`on: button=${enabled.models3dButtonActive} flights=${enabled.flModels3d} military=${enabled.milModels3d}`
|
||
+ ` | off: button=${enabled.models3dOff.button} flights=${enabled.models3dOff.fl} military=${enabled.models3dOff.mil}`);
|
||
|
||
if (!injectionWorked) {
|
||
skip('jitter / pull-out / orphan (need synthetic planes)',
|
||
'synthetic injection produced 0 planes — see inject failures above');
|
||
finishAndExit();
|
||
return;
|
||
}
|
||
|
||
// ============================================================
|
||
// SHARE LINK V2 — real reload restores the same logical plane.
|
||
// ============================================================
|
||
console.log('\nShare Link v2 — tracked aircraft survives a full reload');
|
||
const shareTracked = await evalPage(() => (
|
||
window.__godsEyeView.dataManager.layers
|
||
.get('flights').module.trackById('aaa001', { origin: 'user' })
|
||
));
|
||
await page.waitForFunction(
|
||
() => {
|
||
const flights = window.__godsEyeView?.dataManager?.layers?.get('flights')?.module;
|
||
// `lo=` alone is NOT discriminating — the 3D-models option already
|
||
// put an `lo` field in the hash before any aircraft was selected.
|
||
// Wait for the tracking assignment itself.
|
||
return location.hash.includes('v=2')
|
||
&& /[?&#]lo=[^&]*f\.t\.aaa001/.test(location.hash)
|
||
&& flights?.getParams?.().selectedFlightsTrackingId === 'aaa001';
|
||
},
|
||
// Share serialization is driven by the rendered state transition. On a
|
||
// saturated headless GPU, ten seconds can expire before that frame even
|
||
// though tracking is already accepted; keep the assertion identical but
|
||
// give the callback the same 30 s budget as other reload/latch checks.
|
||
{ timeout: 30000, polling: 100 },
|
||
);
|
||
const trackedShareUrl = page.url();
|
||
record(
|
||
'share-v2: explicit aircraft selection reaches the generated URL',
|
||
// Pin the ACTUAL tracking assignment (`f.t.<icao>`), not the mere
|
||
// presence of an `lo` field that the 3D-models option already supplies.
|
||
shareTracked
|
||
&& /[?&#]lo=[^&]*f\.t\.aaa001/.test(trackedShareUrl)
|
||
&& !trackedShareUrl.includes('f.t.aaa002'),
|
||
trackedShareUrl,
|
||
);
|
||
|
||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 60000 });
|
||
await page.waitForFunction(
|
||
() => window.__godsEyeView?.dataManager?.layers?.size >= 12,
|
||
{ timeout: 60000, polling: 200 },
|
||
);
|
||
await page.waitForFunction(
|
||
() => {
|
||
const manager = window.__godsEyeView?.dataManager;
|
||
const tracked = manager?.layers?.get('flights')?.module?.getTrackedInfo?.();
|
||
return manager?.isEnabled?.('flights')
|
||
&& tracked?.icao24 === 'aaa001'
|
||
&& window.__godsEyeView.viewer.trackedEntity?.gevTrackedId === 'flights:aaa001';
|
||
},
|
||
{ timeout: 30000, polling: 100 },
|
||
);
|
||
const shareReload = await evalPage(() => {
|
||
const manager = window.__godsEyeView.dataManager;
|
||
const flights = manager.layers.get('flights').module;
|
||
return {
|
||
flightsEnabled: manager.isEnabled('flights'),
|
||
trackedId: flights.getTrackedInfo()?.icao24 || null,
|
||
durableId: flights.getParams()?.selectedFlightsTrackingId || null,
|
||
viewerTrackedId: window.__godsEyeView.viewer.trackedEntity?.gevTrackedId || null,
|
||
};
|
||
});
|
||
record(
|
||
'share-v2: full reload restores the selected logical aircraft',
|
||
shareReload.flightsEnabled
|
||
&& shareReload.trackedId === 'aaa001'
|
||
&& shareReload.durableId === 'aaa001',
|
||
JSON.stringify(shareReload),
|
||
);
|
||
record(
|
||
'share-v2: restored aircraft owns the Cesium follow camera',
|
||
shareReload.viewerTrackedId === 'flights:aaa001',
|
||
`viewerTrackedId=${shareReload.viewerTrackedId}`,
|
||
);
|
||
|
||
// Prove the URL itself owns the result. Remove the same-origin saved
|
||
// preference, then open the captured link as a clean recipient would.
|
||
await evalPage(() => localStorage.removeItem('gev:layer-state:v2'));
|
||
await page.goto(trackedShareUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||
await page.waitForFunction(
|
||
() => window.__godsEyeView?.dataManager?.layers?.size >= 12,
|
||
{ timeout: 60000, polling: 200 },
|
||
);
|
||
await page.waitForFunction(
|
||
() => {
|
||
const manager = window.__godsEyeView?.dataManager;
|
||
const tracked = manager?.layers?.get('flights')?.module?.getTrackedInfo?.();
|
||
return manager?.isEnabled?.('flights')
|
||
&& tracked?.icao24 === 'aaa001'
|
||
&& window.__godsEyeView.viewer.trackedEntity?.gevTrackedId === 'flights:aaa001';
|
||
},
|
||
{ timeout: 30000, polling: 100 },
|
||
);
|
||
const cleanRecipient = await evalPage(() => {
|
||
const manager = window.__godsEyeView.dataManager;
|
||
const flights = manager.layers.get('flights').module;
|
||
return {
|
||
flightsEnabled: manager.isEnabled('flights'),
|
||
trackedId: flights.getTrackedInfo()?.icao24 || null,
|
||
durableId: flights.getParams()?.selectedFlightsTrackingId || null,
|
||
viewerTrackedId: window.__godsEyeView.viewer.trackedEntity?.gevTrackedId || null,
|
||
};
|
||
});
|
||
record(
|
||
'share-v2: clean recipient magic link restores the selected aircraft',
|
||
cleanRecipient.flightsEnabled
|
||
&& cleanRecipient.trackedId === 'aaa001'
|
||
&& cleanRecipient.durableId === 'aaa001'
|
||
&& cleanRecipient.viewerTrackedId === 'flights:aaa001',
|
||
JSON.stringify(cleanRecipient),
|
||
);
|
||
// ---- Not-yet-arrived shared subject: PENDING, silent, then latches ----
|
||
// Reload-from-local always survived a slow first refresh via the layer's
|
||
// deferred-restore latch. The shared path used to decide on ONE refresh,
|
||
// clear the subject, and post a failure notice seconds into startup.
|
||
await evalPage(() => window.sessionStorage.setItem('__gevWithhold', 'aaa001'));
|
||
// A goto whose URL differs only by hash is a SAME-DOCUMENT navigation: the
|
||
// page would never reload and the app would keep its existing tracking.
|
||
// Leave the document first so this is a real cold boot of the shared link.
|
||
await page.goto('about:blank', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.goto(trackedShareUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||
await page.waitForFunction(
|
||
() => window.__godsEyeView?.dataManager?.layers?.size >= 12,
|
||
{ timeout: 60000, polling: 200 },
|
||
);
|
||
await page.waitForFunction(
|
||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||
{ timeout: 60000, polling: 200 },
|
||
);
|
||
await new Promise((resolve) => setTimeout(resolve, 6000));
|
||
const pendingShare = await evalPage(() => ({
|
||
tracked: window.__godsEyeView.dataManager.layers
|
||
.get('flights').module.getTrackedInfo()?.icao24 || null,
|
||
notice: (document.getElementById('global-loading-label')?.textContent || '').trim(),
|
||
noticeShown: !document.getElementById('global-loading-status')?.hidden,
|
||
}));
|
||
record(
|
||
'share-v2: a not-yet-arrived subject is held pending, with no failure notice',
|
||
pendingShare.tracked === null
|
||
&& !/unavailable|expired|could not be restored/i.test(
|
||
pendingShare.noticeShown ? pendingShare.notice : '',
|
||
),
|
||
JSON.stringify(pendingShare),
|
||
);
|
||
|
||
// The aircraft now arrives on a later poll; the latch must take it.
|
||
await evalPage(() => window.sessionStorage.removeItem('__gevWithhold'));
|
||
await evalPage(async () => {
|
||
const flights = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
for (let i = 0; i < 3; i += 1) {
|
||
await flights.update(window.__godsEyeView.viewer);
|
||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||
}
|
||
});
|
||
await page.waitForFunction(
|
||
() => window.__godsEyeView?.dataManager?.layers?.get('flights')?.module
|
||
?.getTrackedInfo?.()?.icao24 === 'aaa001',
|
||
{ timeout: 30000, polling: 200 },
|
||
).catch(() => {});
|
||
const latchedShare = await evalPage(() => ({
|
||
tracked: window.__godsEyeView.dataManager.layers
|
||
.get('flights').module.getTrackedInfo()?.icao24 || null,
|
||
viewerTrackedId: window.__godsEyeView.viewer.trackedEntity?.gevTrackedId || null,
|
||
}));
|
||
record(
|
||
'share-v2: the pending subject latches on when it arrives on a later poll',
|
||
latchedShare.tracked === 'aaa001'
|
||
&& latchedShare.viewerTrackedId === 'flights:aaa001',
|
||
JSON.stringify(latchedShare),
|
||
);
|
||
|
||
// The captured link intentionally contains only the explicitly owned
|
||
// Flights layer. Restore the harness-only Military companion before the
|
||
// legacy cross-layer invariants continue.
|
||
await evalPage(async () => {
|
||
const manager = window.__godsEyeView.dataManager;
|
||
await manager.setEnabled('military', true);
|
||
await manager.layers.get('military').module.update(window.__godsEyeView.viewer);
|
||
});
|
||
await evalPage(() => {
|
||
window.__godsEyeView.viewer.scene.sampleHeight = () => undefined;
|
||
});
|
||
|
||
// ============================================================
|
||
// VOICE ENTITY CONTEXT — a click-selected contact IS "the selected
|
||
// entity". The aircraft layers publish selection on their own awareness
|
||
// lane (the readout card + Contacts subject), and for the whole life of
|
||
// the voice tools they never wrote the SHARED context slot that
|
||
// `get_entity_context` reads. So with a plane plainly selected on screen,
|
||
// `{scope:'selected'}` silently downgraded to `'in_view'` and the model
|
||
// answered "there isn't a plane currently selected" (owner field session,
|
||
// 2026-08-21). Drives the real tool runner; costs no model turns.
|
||
// ============================================================
|
||
console.log('\nVoice entity context — a click-selected contact answers scope:selected');
|
||
const runnerReady = await evalPage(() => typeof window.__gevVoiceCommands?.runner === 'function');
|
||
if (!runnerReady) {
|
||
skip('voice-context: click-selected aircraft answers scope:selected',
|
||
'voice command runner not exposed on this build');
|
||
} else {
|
||
const flightSelected = await evalPage(async () => {
|
||
const flights = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
// The exact call the canvas click handler makes on a billboard pick.
|
||
flights.trackById('aaa001', { origin: 'user' });
|
||
const result = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
return {
|
||
scope: result?.scope || null,
|
||
layerId: result?.selected?.layerId || null,
|
||
id: result?.selected?.id || null,
|
||
name: result?.selected?.name || null,
|
||
latitude: result?.selected?.latitude ?? null,
|
||
callsign: result?.selected?.properties?.callsign || null,
|
||
};
|
||
});
|
||
record(
|
||
'voice-context: click-selected aircraft answers scope:selected',
|
||
flightSelected.scope === 'selected'
|
||
&& flightSelected.layerId === 'flights'
|
||
&& flightSelected.id === 'aaa001',
|
||
JSON.stringify(flightSelected),
|
||
);
|
||
record(
|
||
'voice-context: the selected contact carries its identity and position',
|
||
flightSelected.name === 'SYN001'
|
||
&& flightSelected.callsign === 'SYN001'
|
||
&& Number.isFinite(flightSelected.latitude),
|
||
JSON.stringify(flightSelected),
|
||
);
|
||
|
||
// Selecting a different contact must MOVE the subject, not stack a
|
||
// second one: a frozen record left behind would be narrated later at a
|
||
// position its aircraft has long since left.
|
||
const switchedSubject = await evalPage(async () => {
|
||
const manager = window.__godsEyeView.dataManager;
|
||
manager.layers.get('flights').module.trackById('aaa002', { origin: 'user' });
|
||
const result = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
const store = window.__gevContextStore;
|
||
const flightRecords = [...store.entities.values()].filter((r) => r.layerId === 'flights');
|
||
return {
|
||
scope: result?.scope || null,
|
||
id: result?.selected?.id || null,
|
||
flightRecordIds: flightRecords.map((r) => r.id),
|
||
};
|
||
});
|
||
record(
|
||
'voice-context: switching contacts moves the subject and leaves no stale record',
|
||
switchedSubject.scope === 'selected'
|
||
&& switchedSubject.id === 'aaa002'
|
||
&& switchedSubject.flightRecordIds.length === 1
|
||
&& switchedSubject.flightRecordIds[0] === 'aaa002',
|
||
JSON.stringify(switchedSubject),
|
||
);
|
||
|
||
// A military contact is the same click, on the sibling layer.
|
||
const militarySelected = await evalPage(async () => {
|
||
const manager = window.__godsEyeView.dataManager;
|
||
manager.layers.get('flights').module.stopTracking();
|
||
manager.layers.get('military').module.trackById('bbb101', { origin: 'user' });
|
||
const result = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
return {
|
||
scope: result?.scope || null,
|
||
layerId: result?.selected?.layerId || null,
|
||
id: result?.selected?.id || null,
|
||
name: result?.selected?.name || null,
|
||
};
|
||
});
|
||
record(
|
||
'voice-context: click-selected military contact answers scope:selected',
|
||
militarySelected.scope === 'selected'
|
||
&& militarySelected.layerId === 'military'
|
||
&& militarySelected.id === 'bbb101'
|
||
&& militarySelected.name === 'MIL101',
|
||
JSON.stringify(militarySelected),
|
||
);
|
||
|
||
// Deselecting must give the slot back — a subject that outlives its
|
||
// selection is the same bug pointed the other way.
|
||
const deselected = await evalPage(async () => {
|
||
const manager = window.__godsEyeView.dataManager;
|
||
manager.layers.get('military').module.stopTracking();
|
||
const result = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
return { scope: result?.scope || null, selected: result?.selected ?? null };
|
||
});
|
||
record(
|
||
'voice-context: deselecting releases the subject back to in_view',
|
||
deselected.scope === 'in_view' && deselected.selected === null,
|
||
JSON.stringify(deselected),
|
||
);
|
||
|
||
// ============================================================
|
||
// SATELLITES — same PRD gap as the aircraft layers: a tracked satellite
|
||
// published only its awareness event, so voice could not see it either.
|
||
// ============================================================
|
||
console.log('\nVoice entity context — a tracked satellite is a selected subject');
|
||
const satelliteContext = await evalPage(async () => {
|
||
const manager = window.__godsEyeView.dataManager;
|
||
await manager.setEnabled('satellites', true);
|
||
const satellites = manager.layers.get('satellites').module;
|
||
// Wait for the shimmed stations catalog to build its satrecs.
|
||
for (let i = 0; i < 80; i += 1) {
|
||
if (satellites.getStats?.().count > 0) break;
|
||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||
}
|
||
const tracked = satellites.trackById(25544, { origin: 'user' });
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
const selected = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
// Switching subjects must move the slot, not stack a second orbit.
|
||
satellites.trackById(20580, { origin: 'user' });
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
const switched = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
const satelliteRecordIds = [...window.__gevContextStore.entities.values()]
|
||
.filter((record) => record.layerId === 'satellites')
|
||
.map((record) => record.id);
|
||
// CATALOG REBUILD. The tracked satellite's catalog entry is replaced
|
||
// wholesale by a TLE refresh, so a surviving subject must re-resolve
|
||
// against the new satrec — and a subject that did NOT survive must
|
||
// release the slot rather than linger frozen at its last position.
|
||
satellites.trackById(25544, { origin: 'user' });
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
await satellites.update(window.__godsEyeView.viewer);
|
||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||
const afterRebuild = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
|
||
// RECLASSIFICATION: the subject vanishes from the catalog while a
|
||
// DIFFERENT group — one it could have been moved into — failed to
|
||
// load. Its own group loaded fine, so a group-scoped proof would call
|
||
// this a disappearance. It is not: absence is only proven when every
|
||
// potential carrier loaded.
|
||
const fullTle = window.__SYNTH.tle;
|
||
const truncatedTle = fullTle.split('\n').slice(3).join('\n');
|
||
window.__SYNTH.tle = truncatedTle;
|
||
window.__SYNTH.failGroup = 'geo';
|
||
await satellites.update(window.__godsEyeView.viewer);
|
||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||
const afterPartial = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
window.__SYNTH.failGroup = null;
|
||
window.__SYNTH.tle = fullTle;
|
||
// Recover, so the release case below starts from a healthy subject.
|
||
await satellites.update(window.__godsEyeView.viewer);
|
||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||
|
||
// Now the subject vanishes from a COMPLETE catalog — proven absence.
|
||
window.__SYNTH.tle = fullTle.split('\n').slice(3).join('\n');
|
||
await satellites.update(window.__godsEyeView.viewer);
|
||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||
const afterSubjectGone = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
window.__SYNTH.tle = fullTle;
|
||
|
||
// DENSE CARRIER FAILURE. Track a dense-shell satellite, then let the
|
||
// dense load fail on the next rebuild. Settling that failure reverts
|
||
// the catalog mode to 'core', so anything reading the mode AFTER the
|
||
// await no longer sees that dense was ever a potential carrier — and
|
||
// would delete a subject the dense catalog might have carried.
|
||
satellites.setParams({ catalog: 'dense' });
|
||
for (let i = 0; i < 80; i += 1) {
|
||
if (satellites.getStats?.().count > 2) break;
|
||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||
}
|
||
const denseTracked = satellites.trackById(44713, { origin: 'user' });
|
||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||
const denseSelected = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
window.__SYNTH.failDense = true;
|
||
await satellites.update(window.__godsEyeView.viewer);
|
||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||
const afterDenseFailure = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
const catalogModeAfterFailure = satellites.getParams?.().catalog ?? null;
|
||
window.__SYNTH.failDense = false;
|
||
satellites.setParams({ catalog: 'core' });
|
||
satellites.stopTracking?.();
|
||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||
|
||
satellites.trackById(25544, { origin: 'user' });
|
||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||
satellites.stopTracking?.();
|
||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||
const released = await window.__gevVoiceCommands.runner('get_entity_context', { scope: 'selected' });
|
||
await manager.setEnabled('satellites', false);
|
||
// Following a satellite left the camera in orbit. Hand it back to the
|
||
// synthetic fleet so later groups see the view they were written for.
|
||
manager.layers.get('flights').module.trackById('aaa001', { origin: 'user' });
|
||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||
return {
|
||
tracked,
|
||
scope: selected?.scope ?? null,
|
||
layerId: selected?.selected?.layerId ?? null,
|
||
id: selected?.selected?.id ?? null,
|
||
name: selected?.selected?.name ?? null,
|
||
latitude: selected?.selected?.latitude ?? null,
|
||
switchedId: switched?.selected?.id ?? null,
|
||
satelliteRecordIds,
|
||
releasedScope: released?.scope ?? null,
|
||
releasedSelected: released?.selected ?? null,
|
||
rebuiltScope: afterRebuild?.scope ?? null,
|
||
rebuiltId: afterRebuild?.selected?.id ?? null,
|
||
rebuiltLatitude: afterRebuild?.selected?.latitude ?? null,
|
||
goneScope: afterSubjectGone?.scope ?? null,
|
||
goneSelected: afterSubjectGone?.selected ?? null,
|
||
partialScope: afterPartial?.scope ?? null,
|
||
partialId: afterPartial?.selected?.id ?? null,
|
||
denseTracked,
|
||
denseSelectedId: denseSelected?.selected?.id ?? null,
|
||
denseFailureScope: afterDenseFailure?.scope ?? null,
|
||
denseFailureId: afterDenseFailure?.selected?.id ?? null,
|
||
catalogModeAfterFailure,
|
||
};
|
||
});
|
||
record(
|
||
'voice-context: a tracked satellite answers scope:selected with its identity',
|
||
satelliteContext.tracked === true
|
||
&& satelliteContext.scope === 'selected'
|
||
&& satelliteContext.layerId === 'satellites'
|
||
&& satelliteContext.id === '25544'
|
||
&& /ISS/.test(satelliteContext.name || '')
|
||
&& Number.isFinite(satelliteContext.latitude),
|
||
JSON.stringify(satelliteContext),
|
||
);
|
||
record(
|
||
'voice-context: switching satellites moves the subject and leaves no stale orbit',
|
||
satelliteContext.switchedId === '20580'
|
||
&& satelliteContext.satelliteRecordIds.length === 1
|
||
&& satelliteContext.satelliteRecordIds[0] === '20580',
|
||
JSON.stringify(satelliteContext),
|
||
);
|
||
record(
|
||
'voice-context: the subject survives a catalog rebuild under the same id',
|
||
satelliteContext.rebuiltScope === 'selected'
|
||
&& satelliteContext.rebuiltId === '25544'
|
||
&& Number.isFinite(satelliteContext.rebuiltLatitude),
|
||
JSON.stringify(satelliteContext),
|
||
);
|
||
record(
|
||
'voice-context: a subject missing while ANOTHER potential carrier failed is preserved',
|
||
satelliteContext.partialScope === 'selected' && satelliteContext.partialId === '25544',
|
||
JSON.stringify(satelliteContext),
|
||
);
|
||
record(
|
||
'voice-context: a subject that does not survive the rebuild releases the slot',
|
||
satelliteContext.goneScope === 'in_view' && satelliteContext.goneSelected === null,
|
||
JSON.stringify(satelliteContext),
|
||
);
|
||
record(
|
||
'voice-context: a failed DENSE load preserves its subject, even though the mode reverts',
|
||
satelliteContext.denseTracked === true
|
||
&& satelliteContext.denseSelectedId === '44713'
|
||
&& satelliteContext.catalogModeAfterFailure === 'core'
|
||
&& satelliteContext.denseFailureScope === 'selected'
|
||
&& satelliteContext.denseFailureId === '44713',
|
||
JSON.stringify(satelliteContext),
|
||
);
|
||
record(
|
||
'voice-context: deselecting a satellite releases the subject',
|
||
satelliteContext.releasedScope === 'in_view' && satelliteContext.releasedSelected === null,
|
||
JSON.stringify(satelliteContext),
|
||
);
|
||
|
||
// ============================================================
|
||
// PANEL === SPOKEN. Not the shared helper as a stand-in: the REAL
|
||
// Contacts pipeline (awareness selects a subject, evaluateSubject fills
|
||
// the snapshot, the panel renders it) against the REAL voice tool. A
|
||
// divergence introduced at the call site itself — not in the helper —
|
||
// has to go red here.
|
||
// ============================================================
|
||
console.log('\nPanel === spoken — one number, through both real call sites');
|
||
const panelVsSpoken = await evalPage(async () => {
|
||
const manager = window.__godsEyeView.dataManager;
|
||
if (!manager.isEnabled('flights')) await manager.setEnabled('flights', true);
|
||
if (!manager.isEnabled('military')) await manager.setEnabled('military', true);
|
||
// Enter Contacts the way the operator does, then select a contact so
|
||
// awareness has a real subject.
|
||
const button = document.getElementById('global-context-flights-btn');
|
||
if (button && !window.__godsEyeView.styleManager.getContextModeState?.().mode) button.click();
|
||
for (let i = 0; i < 80; i += 1) {
|
||
if (window.__godsEyeView.styleManager.getContextModeState?.().mode) break;
|
||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||
}
|
||
manager.layers.get('flights').module.trackById('aaa001', { origin: 'user' });
|
||
// Awareness recomputes on its own cadence; wait for a real snapshot.
|
||
const awareness = manager.layers.get('military-awareness')?.module;
|
||
let snapshot = null;
|
||
for (let i = 0; i < 80; i += 1) {
|
||
snapshot = awareness?.getContextSnapshot?.() || null;
|
||
if (snapshot?.cohorts?.length) break;
|
||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||
}
|
||
if (!snapshot?.subject) return { error: 'awareness produced no subject' };
|
||
const countFor = (id) => {
|
||
const cohort = snapshot.cohorts.find((entry) => entry?.id === id);
|
||
return Number.isFinite(cohort?.count) ? cohort.count : null;
|
||
};
|
||
// What the PANEL shows, read from the snapshot it renders.
|
||
const panelAircraft = (countFor('flights') ?? 0) + (countFor('military') ?? 0);
|
||
// What VOICE says, through the real tool runner.
|
||
const spoken = await window.__gevVoiceCommands.runner('analyst_query', {
|
||
layers: ['flights', 'military'],
|
||
scope: { kind: 'radius', km: Math.round((snapshot.radiusM || 250000) / 1000) },
|
||
});
|
||
return {
|
||
subject: snapshot.subject.label || snapshot.subject.id || null,
|
||
panelAircraft,
|
||
spokenCount: spoken?.count ?? null,
|
||
spokenEngine: spoken?.window?.engine ?? null,
|
||
spokenCenteredOn: spoken?.window?.centeredOn ?? null,
|
||
};
|
||
});
|
||
record(
|
||
'panel===spoken: the Contacts panel and the voice count are one number',
|
||
!panelVsSpoken.error
|
||
&& panelVsSpoken.spokenEngine === 'contacts-window'
|
||
&& panelVsSpoken.spokenCount === panelVsSpoken.panelAircraft,
|
||
JSON.stringify(panelVsSpoken),
|
||
);
|
||
record(
|
||
'panel===spoken: the spoken answer names the subject whose window it read',
|
||
!panelVsSpoken.error && panelVsSpoken.spokenCenteredOn === panelVsSpoken.subject,
|
||
JSON.stringify(panelVsSpoken),
|
||
);
|
||
// Leave Contacts as the vocabulary group expects to find it.
|
||
await evalPage(async () => {
|
||
try { await window.__gevVoiceCommands.runner('set_context_mode', { mode: 'off' }); } catch {}
|
||
});
|
||
|
||
// ============================================================
|
||
// CONTEXT VOCABULARY — state output must speak the words the tools
|
||
// accept. The Contacts mode's internal id is 'flights' while
|
||
// `set_context_mode` takes 'contacts', and state surfaces reported the
|
||
// internal id: the model read `mode:'flights'`, concluded Contacts was
|
||
// off, and refused to answer from the Contacts window counts carried in
|
||
// the same payload (owner field session, 2026-08-21).
|
||
// ============================================================
|
||
console.log('\nContext vocabulary — state output speaks the tools\' own words');
|
||
const manualContacts = await evalPage(async () => {
|
||
const button = document.getElementById('global-context-flights-btn');
|
||
if (!button) return { error: 'Contacts control not found' };
|
||
// The MANUAL path is the one that broke silently — voice never set it.
|
||
button.click();
|
||
for (let i = 0; i < 60; i += 1) {
|
||
if (window.__godsEyeView.styleManager.getContextModeState?.().mode) break;
|
||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||
}
|
||
const state = await window.__gevVoiceCommands.runner('get_current_view_state');
|
||
return {
|
||
internalMode: window.__godsEyeView.styleManager.getContextModeState?.().mode || null,
|
||
reportedMode: state?.context?.mode ?? null,
|
||
reportedInternal: state?.context?.modeInternal ?? null,
|
||
active: state?.context?.active ?? null,
|
||
};
|
||
});
|
||
record(
|
||
'context-vocab: a manually activated Contacts reports as "contacts", not its internal id',
|
||
manualContacts.internalMode === 'flights'
|
||
&& manualContacts.reportedMode === 'contacts'
|
||
&& manualContacts.reportedInternal === 'flights'
|
||
&& manualContacts.active === true,
|
||
JSON.stringify(manualContacts),
|
||
);
|
||
|
||
// Round-trip: whatever state output reports must be a word the tool
|
||
// accepts. Feeding the reported mode straight back must not be rejected.
|
||
const roundTrip = await evalPage(async (reported) => {
|
||
const result = await window.__gevVoiceCommands.runner('set_context_mode', { mode: reported });
|
||
const state = await window.__gevVoiceCommands.runner('get_current_view_state');
|
||
return {
|
||
ok: result?.ok ?? null,
|
||
error: result?.error || null,
|
||
resultMode: result?.mode ?? null,
|
||
resultInternal: result?.modeInternal ?? null,
|
||
stateMode: state?.context?.mode ?? null,
|
||
};
|
||
}, manualContacts.reportedMode);
|
||
record(
|
||
'context-vocab: the reported mode round-trips back through set_context_mode',
|
||
roundTrip.ok === true
|
||
&& !roundTrip.error
|
||
&& roundTrip.resultMode === 'contacts'
|
||
&& roundTrip.resultInternal === 'flights'
|
||
&& roundTrip.stateMode === 'contacts',
|
||
JSON.stringify(roundTrip),
|
||
);
|
||
|
||
const contextOff = await evalPage(async () => {
|
||
const result = await window.__gevVoiceCommands.runner('set_context_mode', { mode: 'off' });
|
||
const state = await window.__gevVoiceCommands.runner('get_current_view_state');
|
||
return {
|
||
resultMode: result?.mode ?? null,
|
||
stateMode: state?.context?.mode ?? null,
|
||
stateInternal: state?.context?.modeInternal ?? null,
|
||
};
|
||
});
|
||
record(
|
||
'context-vocab: no context reports as "off", never a bare null',
|
||
contextOff.resultMode === 'off'
|
||
&& contextOff.stateMode === 'off'
|
||
&& contextOff.stateInternal === null,
|
||
JSON.stringify(contextOff),
|
||
);
|
||
|
||
// ============================================================
|
||
// ANALYST -> TRACK HANDOFF — the tool instructions tell the model to
|
||
// look a contact up with analyst_query and hand that identity to
|
||
// track_entity. The analyst's `id` is a DISPLAY label (callsign, else
|
||
// registration, else hex) and the lookup matched callsigns and hex
|
||
// only, so a callsign-less contact came back as its tail number and
|
||
// "Nothing matched" — the model then burned the turn on retries (owner
|
||
// field session 2026-08-21, 23:48: three failed track_entity calls
|
||
// before a fallback stuck).
|
||
// ============================================================
|
||
console.log('\nAnalyst handoff — a looked-up contact can actually be tracked');
|
||
const handoff = await evalPage(async () => {
|
||
const manager = window.__godsEyeView.dataManager;
|
||
// This group owns its precondition: earlier groups toggle Contacts,
|
||
// which snapshots and restores layer state around itself.
|
||
if (!manager.isEnabled('military')) await manager.setEnabled('military', true);
|
||
const mil = manager.layers.get('military').module;
|
||
const priorFixture = window.__SYNTH.military.slice();
|
||
window.__SYNTH.military = [...priorFixture, {
|
||
hex: 'bbb777', flight: '', lon: -97.7440, lat: 30.2680,
|
||
altFt: 1800, track: 210, gsKt: 130, t: 'UH60', r: '6606',
|
||
}];
|
||
try {
|
||
await mil.update(window.__godsEyeView.viewer);
|
||
// Scope is deliberately 'anywhere': what is under test is the
|
||
// identity handoff, not spatial filtering, and an earlier group may
|
||
// have left the camera somewhere else entirely.
|
||
const analyst = await window.__gevVoiceCommands.runner('analyst_query', {
|
||
layers: ['military'], scope: { kind: 'anywhere' }, limit: 10,
|
||
});
|
||
const item = (analyst?.items || []).find((entry) => entry.id === '6606') || null;
|
||
const tracked = item
|
||
? await window.__gevVoiceCommands.runner('track_entity', { query: item.id, layerId: 'military' })
|
||
: null;
|
||
return {
|
||
militaryEnabled: manager.isEnabled('military'),
|
||
militaryCount: mil.getStats?.().count ?? null,
|
||
itemCount: (analyst?.items || []).length,
|
||
itemFound: Boolean(item),
|
||
itemId: item?.id ?? null,
|
||
itemIcao: item?.icao24 ?? null,
|
||
trackOk: tracked?.ok ?? null,
|
||
trackError: tracked?.error ?? null,
|
||
trackedIcao: mil.getTrackedInfo()?.icao24 ?? null,
|
||
};
|
||
} finally {
|
||
window.__SYNTH.military = priorFixture;
|
||
mil.stopTracking();
|
||
await mil.update(window.__godsEyeView.viewer);
|
||
}
|
||
});
|
||
record(
|
||
'analyst-handoff: a callsign-less contact is named by the tail number the app shows',
|
||
handoff.itemFound && handoff.itemId === '6606',
|
||
JSON.stringify(handoff),
|
||
);
|
||
record(
|
||
'analyst-handoff: the identity the analyst returned is trackable, first try',
|
||
handoff.trackOk === true
|
||
&& !handoff.trackError
|
||
&& handoff.trackedIcao === 'bbb777',
|
||
JSON.stringify(handoff),
|
||
);
|
||
record(
|
||
'analyst-handoff: the result carries the hex key the tracker keys on',
|
||
handoff.itemIcao === 'bbb777',
|
||
JSON.stringify(handoff),
|
||
);
|
||
|
||
// Restore the state the downstream invariants were written against:
|
||
// flights following aaa001, military idle.
|
||
await evalPage(() => {
|
||
window.__godsEyeView.dataManager.layers.get('flights').module.trackById('aaa001', { origin: 'user' });
|
||
});
|
||
await page.waitForFunction(
|
||
() => window.__godsEyeView?.dataManager?.layers?.get('flights')?.module
|
||
?.getTrackedInfo?.()?.icao24 === 'aaa001',
|
||
{ timeout: 15000, polling: 100 },
|
||
);
|
||
}
|
||
|
||
// NOTE: we do NOT manually reposition the camera. Tracking sets
|
||
// viewer.trackedEntity + a calibrated viewFrom, so the follow camera lands
|
||
// in the few-km band (altitude*1.1+2500, clamped 3–30 km) — far below the
|
||
// MODEL_ALT_CEIL_M = 800 km ceiling, so _modelRegimeActive() is true and the
|
||
// standalone tracked model renders. That's the exact path the invariants
|
||
// protect, so we exercise it rather than a synthetic setView.
|
||
|
||
// ============================================================
|
||
// INVARIANT 1: NO JITTER
|
||
// model visual centre === getDetectableObjects() position, per frame.
|
||
// ============================================================
|
||
console.log('\nInvariant 1 — no jitter (model visual centre === detectable pos)');
|
||
|
||
// Install a scene-graph model finder in the page FIRST (used by the wait
|
||
// below). The tracked model is a standalone Cesium.Model added to a
|
||
// PrimitiveCollection under scene.primitives. We locate the shown Model whose
|
||
// matrix translation is nearest the tracked display position.
|
||
await evalPage(() => {
|
||
window.__findTrackedModel = function () {
|
||
const v = window.__godsEyeView.viewer;
|
||
const prims = v.scene.primitives;
|
||
const out = [];
|
||
const walk = (coll) => {
|
||
const n = coll.length;
|
||
for (let i = 0; i < n; i++) {
|
||
const p = coll.get(i);
|
||
if (!p) continue;
|
||
// PrimitiveCollection has .length + .get
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
// A Cesium.Model has a modelMatrix (Matrix4: column-major Float64Array-like)
|
||
if (p.modelMatrix && typeof p.ready !== 'undefined') out.push(p);
|
||
}
|
||
};
|
||
walk(prims);
|
||
// Prefer shown+ready models.
|
||
const shown = out.filter((m) => m.show && m.ready);
|
||
const pool = shown.length ? shown : out;
|
||
if (!pool.length) return null;
|
||
// If tracking, pick the model nearest the tracked display position.
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
// Prefer the tracked model's explicit H1 pick id. A neighboring fleet
|
||
// model can temporarily be nearer while the follow camera/model matrix
|
||
// settles after the cross-layer switch sequence, causing this probe to
|
||
// report the neighbor's id even though the correct tracked model is up.
|
||
const trackedInfo = fl.getTrackedInfo();
|
||
const exactPickModel = trackedInfo
|
||
? pool.find((model) => String(model.id) === trackedInfo.icao24)
|
||
: null;
|
||
if (exactPickModel) return exactPickModel;
|
||
const ent = window.__godsEyeView.viewer.trackedEntity;
|
||
let tracked = null;
|
||
if (ent && typeof ent.gevDisplayPosition === 'function') tracked = ent.gevDisplayPosition();
|
||
if (!tracked) return pool[0];
|
||
let best = null, bestD = Infinity;
|
||
for (const m of pool) {
|
||
const mm = m.modelMatrix;
|
||
const dx = mm[12] - tracked.x, dy = mm[13] - tracked.y, dz = mm[14] - tracked.z;
|
||
const d = dx * dx + dy * dy + dz * dz;
|
||
if (d < bestD) { bestD = d; best = m; }
|
||
}
|
||
// The tracked model rides AT the display position. While the tracked
|
||
// GLB is still loading, the nearest ready model is some OTHER plane's
|
||
// fleet model km away — returning it made the model-up waits resolve
|
||
// spuriously and H1 read a neighbor's pick id. Anything beyond 1 km
|
||
// is "tracked model not up yet", not a match.
|
||
return bestD < 1000 * 1000 ? best : null;
|
||
};
|
||
});
|
||
|
||
// Track a synthetic commercial plane. Tracking sets viewer.trackedEntity +
|
||
// viewFrom, so the follow camera lands in the few-km band and the standalone
|
||
// 3D model renders (model regime active below the 800 km ceiling).
|
||
const trackedIcao = SYNTH.flights[0].icao;
|
||
await evalPage((icao) => {
|
||
window.__godsEyeView.dataManager.layers.get('flights').module.trackById(icao);
|
||
}, trackedIcao);
|
||
await sleep(1500); // let the follow camera settle + the GLB load kick off
|
||
|
||
// Wait until the tracked Cesium.Model primitive exists, is ready & shown.
|
||
const modelUp = await page.waitForFunction((icao) => {
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const ti = fl.getTrackedInfo();
|
||
if (!ti || ti.icao24 !== icao) return false;
|
||
const found = window.__findTrackedModel ? window.__findTrackedModel() : null;
|
||
return !!(found && found.ready && found.show && String(found.id) === icao);
|
||
}, { timeout: 60000, polling: 250 }, trackedIcao).then(() => true).catch(() => false);
|
||
record('3D: tracked commercial model becomes ready through the DISPLAY toggle', modelUp,
|
||
modelUp ? `ready+shown id=${trackedIcao}` : 'tracked product model never became ready+shown');
|
||
|
||
let jitterResult;
|
||
if (!modelUp) {
|
||
// Model didn't come up — still test the underlying invariant: the tracked
|
||
// entity's exposed display position (what the model uses) must equal the
|
||
// getDetectableObjects() position. This is the exact shared-source contract.
|
||
jitterResult = await sampleFrames(page, 30, (icao) => {
|
||
const dm = window.__godsEyeView.dataManager;
|
||
const fl = dm.layers.get('flights').module;
|
||
const ent = window.__godsEyeView.viewer.trackedEntity;
|
||
const disp = ent && typeof ent.gevDisplayPosition === 'function' ? ent.gevDisplayPosition() : null;
|
||
const dets = fl.getDetectableObjects({ maxCount: 1000 });
|
||
const me = dets.find((d) => d.id === icao || d.id === (fl.getTrackedInfo() && fl.getTrackedInfo().callsign));
|
||
if (!disp || !me || !me.position) return null;
|
||
const dx = disp.x - me.position.x, dy = disp.y - me.position.y, dz = disp.z - me.position.z;
|
||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||
}, trackedIcao);
|
||
console.log(' (3D model did not render under SwiftShader; testing the shared display-position contract directly)');
|
||
} else {
|
||
// Full check: independently transform airplane.glb's measured AABB centre
|
||
// through the actual 3D model matrix, then compare it with detection.
|
||
// glTF [x,y,z] → Cesium-local [x,-z,y]. The effective rendered
|
||
// computedScale is separate from modelMatrix and includes any
|
||
// minimumPixelSize enlargement, so include it just as the renderer does.
|
||
jitterResult = await sampleFrames(page, 30, (icao) => {
|
||
const dm = window.__godsEyeView.dataManager;
|
||
const fl = dm.layers.get('flights').module;
|
||
const ti = fl.getTrackedInfo();
|
||
const model = window.__findTrackedModel();
|
||
if (!model) return null;
|
||
const mm = model.modelMatrix;
|
||
const scale = Number.isFinite(model.computedScale)
|
||
? model.computedScale
|
||
: (Number.isFinite(model.scale) ? model.scale : 1);
|
||
// airplane.glb is now bounding-box centred with transforms applied.
|
||
const x = 0 * scale;
|
||
const y = 0 * scale;
|
||
const z = 0 * scale;
|
||
const visual = {
|
||
x: mm[0] * x + mm[4] * y + mm[8] * z + mm[12],
|
||
y: mm[1] * x + mm[5] * y + mm[9] * z + mm[13],
|
||
z: mm[2] * x + mm[6] * y + mm[10] * z + mm[14],
|
||
};
|
||
const dets = fl.getDetectableObjects({ maxCount: 1000 });
|
||
const me = dets.find((d) => d.skipLabel) // tracked entry is skipLabel:true
|
||
|| dets.find((d) => d.id === (ti && ti.callsign))
|
||
|| dets.find((d) => d.id === icao);
|
||
if (!me || !me.position) return null;
|
||
const dx = visual.x - me.position.x;
|
||
const dy = visual.y - me.position.y;
|
||
const dz = visual.z - me.position.z;
|
||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||
}, trackedIcao);
|
||
}
|
||
|
||
// Tolerance: the model and detectable both read _cachedDRPosition, so the
|
||
// delta should be ~0. A regression (model recomputing dead-reckon on a later
|
||
// frameNumber) produced a visible front-back jitter = tens to hundreds of m.
|
||
// Allow 5 m for float noise / partial-frame sampling.
|
||
const JITTER_TOL_M = 5;
|
||
const samples = jitterResult.values.filter((v) => v != null);
|
||
if (samples.length < 5) {
|
||
skip('jitter: model visual centre === detectable pos', `only ${samples.length} usable samples (positions unavailable)`);
|
||
} else {
|
||
const maxDelta = Math.max(...samples);
|
||
record('jitter: max|visual centre − detectable| ≈ 0 over frames', maxDelta <= JITTER_TOL_M,
|
||
`max=${maxDelta.toFixed(3)} m over ${samples.length} frames (tol ${JITTER_TOL_M} m, mode=${modelUp ? 'model-matrix' : 'display-contract'})`);
|
||
}
|
||
|
||
// ============================================================
|
||
// INVARIANT 2: NO PULL-OUT on a track switch
|
||
// tracking B while tracking A must not fly to ~80 km overview.
|
||
// ============================================================
|
||
console.log('\nInvariant 2 — no pull-out on track switch');
|
||
// We're currently tracking SYN001. Record camera height, then switch to
|
||
// SYN002 and watch the camera height across frames. A pull-out = the
|
||
// deselect flyTo to an ~80 km overview; it should be skipped on a switch.
|
||
const planeA = SYNTH.flights[0].icao;
|
||
const planeB = SYNTH.flights[1].icao;
|
||
|
||
// Ensure we are tracking A and have a settled (low) follow height first.
|
||
await evalPage((icao) => {
|
||
window.__godsEyeView.dataManager.layers.get('flights').module.trackById(icao);
|
||
}, planeA);
|
||
await sleep(1500);
|
||
|
||
const heightBefore = await evalPage(() => window.__godsEyeView.viewer.camera.positionCartographic.height);
|
||
|
||
// Switch to B and immediately start sampling camera height every frame.
|
||
const switchSamples = await page.evaluate(async (icao, frames) => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const heights = [];
|
||
const onTick = () => { heights.push(v.camera.positionCartographic.height); };
|
||
const remove = v.scene.postRender.addEventListener(onTick);
|
||
fl.trackById(icao); // the SWITCH — must NOT trigger the deselect pull-out flyTo
|
||
await new Promise((res) => {
|
||
let n = 0;
|
||
let settled = false;
|
||
const finish = () => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
stop();
|
||
res();
|
||
};
|
||
const stop = v.scene.postRender.addEventListener(() => {
|
||
if (++n >= frames) {
|
||
finish();
|
||
return;
|
||
}
|
||
v.scene.requestRender();
|
||
});
|
||
const timer = setTimeout(finish, Math.max(15000, frames * 1500));
|
||
v.scene.requestRender();
|
||
});
|
||
remove();
|
||
return heights;
|
||
}, planeB, 45);
|
||
|
||
const maxHeight = Math.max(heightBefore, ...switchSamples);
|
||
// The follow camera for these planes sits a few km up (altitude*1.1+2500,
|
||
// clamped 3-30 km). The "pull-out" bug flew to ~80 km. Fail if the camera
|
||
// ever climbs past 50 km — comfortably above the legit follow band, well
|
||
// below the 80 km overview.
|
||
const PULLOUT_CEIL_M = 50000;
|
||
record('pull-out: camera stays in follow band on switch (no ~80 km spike)',
|
||
maxHeight < PULLOUT_CEIL_M,
|
||
`maxHeight=${(maxHeight / 1000).toFixed(1)} km (before=${(heightBefore / 1000).toFixed(1)} km, ceil ${PULLOUT_CEIL_M / 1000} km)`);
|
||
|
||
// Confirm the switch actually took effect (we are now tracking B).
|
||
const nowTrackingB = await evalPage((icao) => {
|
||
const ti = window.__godsEyeView.dataManager.layers.get('flights').module.getTrackedInfo();
|
||
return !!(ti && ti.icao24 === icao);
|
||
}, planeB);
|
||
record('pull-out: track switch actually landed on plane B', nowTrackingB,
|
||
nowTrackingB ? 'tracking B' : 'switch did not land');
|
||
|
||
// ============================================================
|
||
// INVARIANT 3: NO CROSS-LAYER ORPHAN
|
||
// track military, then commercial → military.getTrackedInfo()===null,
|
||
// and the reverse.
|
||
// ============================================================
|
||
console.log('\nInvariant 3 — no cross-layer orphan');
|
||
const milHex = SYNTH.military[0].hex;
|
||
const comIcao = SYNTH.flights[2].icao;
|
||
|
||
// (a) military first, then commercial → military must clear.
|
||
const cross1 = await evalPage(async (hex, icao) => {
|
||
const dm = window.__godsEyeView.dataManager;
|
||
const mil = dm.layers.get('military').module;
|
||
const fl = dm.layers.get('flights').module;
|
||
mil.trackById(hex);
|
||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||
const milTrackedBefore = !!mil.getTrackedInfo();
|
||
fl.trackById(icao); // tracking commercial flips viewer.trackedEntity → military listener fires
|
||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||
return {
|
||
milTrackedBefore,
|
||
milTrackedAfter: !!mil.getTrackedInfo(),
|
||
flTrackedAfter: !!fl.getTrackedInfo(),
|
||
};
|
||
}, milHex, comIcao);
|
||
record('orphan: military clears when commercial is tracked',
|
||
cross1.milTrackedBefore && cross1.milTrackedAfter === false && cross1.flTrackedAfter === true,
|
||
`mil before=${cross1.milTrackedBefore} mil after=${cross1.milTrackedAfter} fl after=${cross1.flTrackedAfter}`);
|
||
|
||
// (b) commercial first, then military → commercial must clear.
|
||
const cross2 = await evalPage(async (hex, icao) => {
|
||
const dm = window.__godsEyeView.dataManager;
|
||
const mil = dm.layers.get('military').module;
|
||
const fl = dm.layers.get('flights').module;
|
||
fl.trackById(icao);
|
||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||
const flTrackedBefore = !!fl.getTrackedInfo();
|
||
mil.trackById(hex);
|
||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||
return {
|
||
flTrackedBefore,
|
||
flTrackedAfter: !!fl.getTrackedInfo(),
|
||
milTrackedAfter: !!mil.getTrackedInfo(),
|
||
};
|
||
}, milHex, comIcao);
|
||
record('orphan: commercial clears when military is tracked',
|
||
cross2.flTrackedBefore && cross2.flTrackedAfter === false && cross2.milTrackedAfter === true,
|
||
`fl before=${cross2.flTrackedBefore} fl after=${cross2.flTrackedAfter} mil after=${cross2.milTrackedAfter}`);
|
||
|
||
// No console errors should have accumulated during the tracking exercises.
|
||
record('no console errors during tracking exercises', consoleErrors.length === 0,
|
||
consoleErrors.length ? `${consoleErrors.length}: ${consoleErrors.slice(0, 3).join(' | ')}` : 'clean');
|
||
|
||
// ============================================================
|
||
// REGRESSION H1 (pre-ship audit 2026-07-01): clicking the very plane
|
||
// you're tracking (3D on) must be a NO-OP — no deselect, no 80 km
|
||
// pull-out. The tracked standalone model previously carried NO pick id,
|
||
// so scene.pick read the click as empty space → the deselect flyTo.
|
||
// ============================================================
|
||
console.log('\nRegression H1 — click on the tracked plane is a no-op');
|
||
const h1Icao = SYNTH.flights[0].icao;
|
||
await evalPage((icao) => {
|
||
window.__godsEyeView.dataManager.layers.get('flights').module.trackById(icao);
|
||
}, h1Icao);
|
||
await sleep(1500); // follow camera settles; tracked GLB load kicks off
|
||
|
||
// (i) The tracked standalone model must expose its pick id (= the icao).
|
||
const h1ModelUp = await page.waitForFunction((icao) => {
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const ti = fl.getTrackedInfo();
|
||
if (!ti || ti.icao24 !== icao) return false;
|
||
const found = window.__findTrackedModel ? window.__findTrackedModel() : null;
|
||
return !!(found && found.ready && found.show && String(found.id) === icao);
|
||
}, { timeout: 60000, polling: 250 }, h1Icao).then(() => true).catch(() => false);
|
||
|
||
if (h1ModelUp) {
|
||
const modelPickId = await evalPage(() => {
|
||
const m = window.__findTrackedModel();
|
||
return m && m.id !== undefined && m.id !== null ? String(m.id) : null;
|
||
});
|
||
record('H1: tracked standalone model exposes its pick id', modelPickId === h1Icao,
|
||
`model.id=${JSON.stringify(modelPickId)} (want "${h1Icao}")`);
|
||
} else if (!glbBackendCapable) {
|
||
skip('H1: tracked standalone model exposes its pick id',
|
||
`independent GLB control unavailable (${glbControlDetail})`);
|
||
} else {
|
||
record('H1: tracked standalone model exposes its pick id', false,
|
||
'independent GLB control rendered, but the product tracked model did not');
|
||
}
|
||
|
||
// (ii) Synthetic click at the tracked plane's screen position. Search a
|
||
// small grid around the projected display position for a point where
|
||
// scene.pick actually returns the tracked plane (its model's icao pick id
|
||
// or the tracked entity) so the click is deterministic — a miss would be
|
||
// a LEGIT empty-space deselect, not the H1 signature.
|
||
const h1ClickPoint = await evalPage(() => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const ti = fl.getTrackedInfo();
|
||
if (!ti) return null;
|
||
const ent = v.trackedEntity;
|
||
const disp = ent && typeof ent.gevDisplayPosition === 'function' ? ent.gevDisplayPosition() : null;
|
||
const pos = disp || ti.position;
|
||
if (!pos) return null;
|
||
const center = v.scene.cartesianToCanvasCoordinates(pos);
|
||
if (!center) return null;
|
||
const isTrackedPick = (picked) => {
|
||
if (!picked) return null;
|
||
if (picked.id === ent) return 'entity';
|
||
const raw = typeof picked.id === 'string' ? picked.id : (picked.primitive && picked.primitive.id);
|
||
return raw === ti.icao24 ? 'icao-pick' : null;
|
||
};
|
||
const offsets = [[0, 0]];
|
||
for (const r of [3, 6, 10, 16, 24]) {
|
||
for (const [dx, dy] of [[r, 0], [-r, 0], [0, r], [0, -r], [r, r], [-r, -r], [r, -r], [-r, r]]) {
|
||
offsets.push([dx, dy]);
|
||
}
|
||
}
|
||
for (const [dx, dy] of offsets) {
|
||
const pt = { x: center.x + dx, y: center.y + dy };
|
||
let picked = null;
|
||
try { picked = v.scene.pick(pt); } catch { picked = null; }
|
||
const via = isTrackedPick(picked);
|
||
if (via) return { x: pt.x, y: pt.y, via };
|
||
}
|
||
return { x: center.x, y: center.y, via: null };
|
||
});
|
||
|
||
if (!h1ClickPoint || !h1ClickPoint.via) {
|
||
skip('H1: click on tracked plane keeps tracking (no deselect)',
|
||
'scene.pick could not resolve the tracked plane at its screen position (GL backend picking)');
|
||
skip('H1: click on tracked plane does not pull the camera out',
|
||
'scene.pick could not resolve the tracked plane at its screen position (GL backend picking)');
|
||
} else {
|
||
const h1HeightBefore = await evalPage(() => window.__godsEyeView.viewer.camera.positionCartographic.height);
|
||
// Trusted mouse click through the browser event pipeline → the layer's
|
||
// ScreenSpaceEventHandler LEFT_CLICK path (the exact H1 code path).
|
||
await page.mouse.click(h1ClickPoint.x, h1ClickPoint.y);
|
||
const h1Heights = await page.evaluate(async (frames) => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const heights = [];
|
||
await new Promise((res) => {
|
||
let n = 0;
|
||
let settled = false;
|
||
const finish = () => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
stop();
|
||
res();
|
||
};
|
||
const stop = v.scene.postRender.addEventListener(() => {
|
||
heights.push(v.camera.positionCartographic.height);
|
||
if (++n >= frames) {
|
||
finish();
|
||
return;
|
||
}
|
||
v.scene.requestRender();
|
||
});
|
||
const timer = setTimeout(finish, Math.max(15000, frames * 1500));
|
||
v.scene.requestRender();
|
||
});
|
||
return heights;
|
||
}, 45);
|
||
const h1StillTracking = await evalPage((icao) => {
|
||
const ti = window.__godsEyeView.dataManager.layers.get('flights').module.getTrackedInfo();
|
||
return !!(ti && ti.icao24 === icao) && !!window.__godsEyeView.viewer.trackedEntity;
|
||
}, h1Icao);
|
||
record('H1: click on tracked plane keeps tracking (no deselect)', h1StillTracking,
|
||
`picked via ${h1ClickPoint.via}; still tracking=${h1StillTracking}`);
|
||
const h1MaxHeight = Math.max(h1HeightBefore, ...h1Heights);
|
||
record('H1: click on tracked plane does not pull the camera out', h1MaxHeight < PULLOUT_CEIL_M,
|
||
`maxHeight=${(h1MaxHeight / 1000).toFixed(1)} km (before=${(h1HeightBefore / 1000).toFixed(1)} km, ceil ${PULLOUT_CEIL_M / 1000} km)`);
|
||
}
|
||
|
||
// ============================================================
|
||
// REGRESSION M3 (rewritten 2026-07-02 — deliberate behavior change):
|
||
// when the TRACKED plane ages out (3 missed polls), tracking must clear
|
||
// and the camera must be RELEASED IN PLACE — viewer.trackedEntity
|
||
// undefined and NO jump (the old contract flew an ~80 km overview; the
|
||
// owner field-ruled that wrong: "it randomly zooms way up and loses my
|
||
// context"). The M3 ordering fix stays: tracking is cleared BEFORE the
|
||
// removal loop deletes the plane's state, so we never sit mid-follow
|
||
// with stale tracking state.
|
||
// ============================================================
|
||
console.log('\nRegression M3 — tracked plane age-out releases the camera in place');
|
||
const m3Icao = SYNTH.flights[0].icao;
|
||
await evalPage((icao) => {
|
||
window.__godsEyeView.dataManager.layers.get('flights').module.trackById(icao);
|
||
}, m3Icao);
|
||
await sleep(1200); // settle in the low follow band
|
||
|
||
const m3 = await evalPage(async (icao) => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const beforeTracking = !!fl.getTrackedInfo();
|
||
// Simulate the age-out via the shim: the plane stops arriving.
|
||
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => f.icao !== icao);
|
||
// MISSING_POLL_LIMIT = 3 consecutive absent polls → removal + clear.
|
||
// Capture the camera pose right BEFORE the removing poll — the follow
|
||
// camera drifts with the coasting plane until the instant of release,
|
||
// so this is the "where the user was looking" reference.
|
||
await fl.update(v);
|
||
await fl.update(v);
|
||
const c0 = v.camera.positionCartographic;
|
||
const atAgeOut = {
|
||
lonDeg: (c0.longitude * 180) / Math.PI,
|
||
latDeg: (c0.latitude * 180) / Math.PI,
|
||
height: c0.height,
|
||
};
|
||
await fl.update(v); // third miss → removal + clear + in-place release
|
||
return {
|
||
beforeTracking,
|
||
atAgeOut,
|
||
stillTrackedAfter: !!fl.getTrackedInfo(),
|
||
viewerStillTracking: !!v.trackedEntity,
|
||
};
|
||
}, m3Icao);
|
||
|
||
record('M3: tracking cleared after the tracked plane ages out (getTrackedInfo null)',
|
||
m3.beforeTracking && !m3.stillTrackedAfter,
|
||
`before=${m3.beforeTracking} trackedAfter=${m3.stillTrackedAfter}`);
|
||
record('M3: camera released after age-out (viewer.trackedEntity undefined)',
|
||
!m3.viewerStillTracking,
|
||
`viewerTracked=${m3.viewerStillTracking}`);
|
||
|
||
// The camera must NOT move after the release: no overview flyTo, no
|
||
// snap — it stays where the follow left it. Sample heights across frames
|
||
// (any transient flyTo would show up here), then read the settled pose.
|
||
const m3Heights = await page.evaluate(async (frames) => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const heights = [];
|
||
await new Promise((res) => {
|
||
let n = 0;
|
||
let settled = false;
|
||
const finish = () => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
stop();
|
||
res();
|
||
};
|
||
const stop = v.scene.postRender.addEventListener(() => {
|
||
heights.push(v.camera.positionCartographic.height);
|
||
if (++n >= frames) {
|
||
finish();
|
||
return;
|
||
}
|
||
v.scene.requestRender();
|
||
});
|
||
const timer = setTimeout(finish, Math.max(15000, frames * 1500));
|
||
v.scene.requestRender();
|
||
});
|
||
return heights;
|
||
}, 90);
|
||
await sleep(800); // any (regressed) 0.6 s flyTo would have finished by now
|
||
const m3Final = await evalPage(() => {
|
||
const c = window.__godsEyeView.viewer.camera.positionCartographic;
|
||
return {
|
||
lonDeg: (c.longitude * 180) / Math.PI,
|
||
latDeg: (c.latitude * 180) / Math.PI,
|
||
height: c.height,
|
||
};
|
||
});
|
||
// Tolerances: the release must hold the camera within ~2 km of where it
|
||
// was at age-out time (the follow band sits a few km up; the old overview
|
||
// flyTo climbed +80 km, orders of magnitude outside this budget).
|
||
const M3_STAY_TOL_M = 2000;
|
||
const m3AltDeltaM = Math.max(
|
||
Math.abs(m3Final.height - m3.atAgeOut.height),
|
||
...m3Heights.map((h) => Math.abs(h - m3.atAgeOut.height)),
|
||
);
|
||
const m3HorizDeltaM = haversineMeters(
|
||
m3.atAgeOut.latDeg, m3.atAgeOut.lonDeg, m3Final.latDeg, m3Final.lonDeg,
|
||
);
|
||
record('M3: camera stayed in place after age-out — altitude Δ < 2 km',
|
||
m3AltDeltaM < M3_STAY_TOL_M,
|
||
`Δalt=${(m3AltDeltaM / 1000).toFixed(2)} km (tol ${M3_STAY_TOL_M / 1000} km; ageOut=${(m3.atAgeOut.height / 1000).toFixed(1)} km, final=${(m3Final.height / 1000).toFixed(1)} km)`);
|
||
record('M3: camera stayed in place after age-out — horizontal Δ < 2 km',
|
||
m3HorizDeltaM < M3_STAY_TOL_M,
|
||
`Δhoriz=${(m3HorizDeltaM / 1000).toFixed(2)} km (tol ${M3_STAY_TOL_M / 1000} km)`);
|
||
|
||
// ============================================================
|
||
// CHANGE 2a (2026-07-02): landing-ghost fast cull. A plane whose last
|
||
// fix is LOW + SLOW (landed — the feed's ground flag lags the actual
|
||
// landing) is removed after ONE missed poll, not the 3-poll grace; a
|
||
// cruise plane that misses a poll keeps the full grace (it absorbs real
|
||
// feed gaps — owner-confirmed important). Both layers.
|
||
// ============================================================
|
||
console.log('\nChange 2a — landing-ghost fast cull (1 missed poll; cruise grace intact)');
|
||
const fastCull = await evalPage(async () => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const dm = window.__godsEyeView.dataManager;
|
||
const fl = dm.layers.get('flights').module;
|
||
const mil = dm.layers.get('military').module;
|
||
const has = (mod, id) => mod.getAllPositions(2000).some((p) => p.id === id);
|
||
|
||
// Landed commercial plane: 120 m baro, 18 m/s (~35 kt) — under both
|
||
// fast-cull gates (LANDED_ALT_MAX_M=150 m, LANDED_SPEED_MAX_MPS=23).
|
||
window.__SYNTH.flights.push({ icao: 'aaa044', callsign: 'SYN044', lon: -97.7420, lat: 30.2660, alt: 120, vel: 18, track: 180 });
|
||
// Landed military plane: 300 ft (~91 m) baro, 20 kt (~10 m/s).
|
||
window.__SYNTH.military.push({ hex: 'bbb103', flight: 'MIL103', lon: -97.7480, lat: 30.2680, altFt: 300, track: 90, gsKt: 20, t: 'C130', r: 'AF-103' });
|
||
await fl.update(v);
|
||
await mil.update(v);
|
||
const ingested = { fl: has(fl, 'aaa044'), mil: has(mil, 'bbb103') };
|
||
|
||
// Both landed planes AND the cruise control (aaa002: 9500 m, 210 m/s)
|
||
// vanish from the feed in the SAME poll.
|
||
const cruise = window.__SYNTH.flights.find((f) => f.icao === 'aaa002');
|
||
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => f.icao !== 'aaa044' && f.icao !== 'aaa002');
|
||
window.__SYNTH.military = window.__SYNTH.military.filter((m) => m.hex !== 'bbb103');
|
||
await fl.update(v); // missed poll #1
|
||
await mil.update(v); // missed poll #1
|
||
const afterOneMiss = {
|
||
landedGone: !has(fl, 'aaa044'),
|
||
milLandedGone: !has(mil, 'bbb103'),
|
||
cruiseStillThere: has(fl, 'aaa002'),
|
||
};
|
||
// Put the cruise plane back so any later polls aren't disturbed.
|
||
if (cruise) window.__SYNTH.flights.push(cruise);
|
||
await fl.update(v);
|
||
return { ingested, afterOneMiss };
|
||
});
|
||
record('fast-cull: landed low+slow synthetics ingested (both layers)',
|
||
fastCull.ingested.fl && fastCull.ingested.mil,
|
||
`flights=${fastCull.ingested.fl} military=${fastCull.ingested.mil}`);
|
||
record('fast-cull: landed plane removed after ONE missed poll (flights)',
|
||
fastCull.afterOneMiss.landedGone,
|
||
`gone=${fastCull.afterOneMiss.landedGone} (grace skipped, was 3 polls)`);
|
||
record('fast-cull: landed plane removed after ONE missed poll (military)',
|
||
fastCull.afterOneMiss.milLandedGone,
|
||
`gone=${fastCull.afterOneMiss.milLandedGone} (grace skipped, was 3 polls)`);
|
||
record('fast-cull: cruise plane keeps the 3-poll grace (present after 1 miss)',
|
||
fastCull.afterOneMiss.cruiseStillThere,
|
||
`present=${fastCull.afterOneMiss.cruiseStillThere}`);
|
||
|
||
// ============================================================
|
||
// CHANGE 2b (2026-07-02): honest readout. While the TRACKED plane is in
|
||
// its missed-poll grace (rendered but absent from the latest poll), the
|
||
// tracked label/readout must carry the "· STALE" cue — and drop it the
|
||
// moment the plane reappears in the feed.
|
||
// ============================================================
|
||
console.log('\nChange 2b — tracked readout STALE cue during missed-poll grace');
|
||
const staleCheck = await evalPage(async () => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const labelText = () => {
|
||
const ent = v.trackedEntity;
|
||
if (!ent) return null;
|
||
// Post overlay-unification the tracked text lives in the host
|
||
// presentation model (entities are asserted native-label-free);
|
||
// the native read stays as a fallback so this probe still fails
|
||
// loudly if BOTH surfaces are ever absent.
|
||
const model = ent.gevLabelModel;
|
||
if (model) return [model.title, ...(model.details || [])].filter(Boolean).join('\n');
|
||
if (!ent.label || !ent.label.text) return null;
|
||
const t = ent.label.text;
|
||
return typeof t === 'string' ? t : (t.getValue ? String(t.getValue(v.clock.currentTime)) : null);
|
||
};
|
||
// Track a cruise plane that IS in the feed (aaa003: 8700 m, 250 m/s —
|
||
// normal grace, so one missed poll leaves it rendered-but-stale).
|
||
fl.trackById('aaa003');
|
||
await fl.update(v); // plane present → fresh label
|
||
const freshLabel = labelText();
|
||
const syn3 = window.__SYNTH.flights.find((f) => f.icao === 'aaa003');
|
||
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => f.icao !== 'aaa003');
|
||
await fl.update(v); // missed poll #1 → grace → label refreshed with cue
|
||
const staleLabel = labelText();
|
||
if (syn3) window.__SYNTH.flights.push(syn3);
|
||
await fl.update(v); // plane back → label rebuilt without the cue
|
||
const recoveredLabel = labelText();
|
||
fl.stopTracking(); // leave nothing tracked
|
||
return { freshLabel, staleLabel, recoveredLabel };
|
||
});
|
||
record('stale-readout: tracked label carries STALE cue during grace',
|
||
!!staleCheck.staleLabel && staleCheck.staleLabel.includes('STALE')
|
||
&& !!staleCheck.freshLabel && !staleCheck.freshLabel.includes('STALE'),
|
||
`fresh="${staleCheck.freshLabel && staleCheck.freshLabel.split('\n')[0]}" stale="${staleCheck.staleLabel && staleCheck.staleLabel.split('\n')[0]}"`);
|
||
record('stale-readout: STALE cue clears when the plane reappears',
|
||
!!staleCheck.recoveredLabel && !staleCheck.recoveredLabel.includes('STALE'),
|
||
`recovered="${staleCheck.recoveredLabel && staleCheck.recoveredLabel.split('\n')[0]}"`);
|
||
|
||
// ============================================================
|
||
// CHANGE 3 (2026-07-03): ground traffic is a FEATURE. Present-but-
|
||
// grounded planes render FULL-STRENGTH in the airborne tint pipeline
|
||
// (white / amber-military — owner verdict, same-day reversal of the
|
||
// day-1 gray 50%-alpha muted style: "just leave them as white … in NYC
|
||
// I can barely see them") at ×0.8 scale; "on the ground" reads from
|
||
// scale + no trail, never from a fade, so the 45%-alpha stale fade
|
||
// stays unambiguous. The on_ground flip restyles the SAME billboard in
|
||
// place (landing/takeoff transition, never a removal); a grounded plane
|
||
// that then VANISHES from the feed still fast-culls after ONE missed
|
||
// poll. Military mirror keys off adsb.lol's alt_baro === "ground".
|
||
// ============================================================
|
||
console.log('\nChange 3 — ground traffic: full-alpha style, in-place transitions, fast cull');
|
||
const ground = await evalPage(async () => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const dm = window.__godsEyeView.dataManager;
|
||
const fl = dm.layers.get('flights').module;
|
||
const mil = dm.layers.get('military').module;
|
||
const findBB = (id) => {
|
||
let found = null;
|
||
const walk = (coll) => {
|
||
const n = coll.length;
|
||
for (let i = 0; i < n; i++) {
|
||
let p;
|
||
try { p = coll.get(i); } catch { continue; }
|
||
if (!p) continue;
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
if (p.image !== undefined && p.alignedAxis !== undefined && p.id === id) found = p;
|
||
}
|
||
};
|
||
walk(v.scene.primitives);
|
||
return found;
|
||
};
|
||
const snap = (id) => {
|
||
const bb = findBB(id);
|
||
if (!bb) return null;
|
||
const c = bb.color;
|
||
return {
|
||
show: bb.show, scale: bb.scale, alpha: c.alpha, red: c.red, green: c.green, blue: c.blue,
|
||
// Infinity doesn't survive puppeteer's JSON transport — encode it.
|
||
ddtd: bb.disableDepthTestDistance === Number.POSITIVE_INFINITY ? 'inf' : bb.disableDepthTestDistance,
|
||
};
|
||
};
|
||
|
||
// 1. On-ground planes ingest + render full-strength (both layers).
|
||
window.__SYNTH.flights.push({ icao: 'aaa066', callsign: 'GND066', lon: -97.7425, lat: 30.2665, alt: 150, vel: 7, track: 45, onGround: true });
|
||
window.__SYNTH.military.push({ hex: 'bbb166', flight: 'MILGND1', lon: -97.7490, lat: 30.2690, altFt: 'ground', track: 120, gsKt: 12, t: 'C130', r: 'AF-166' });
|
||
await fl.update(v);
|
||
await mil.update(v);
|
||
const groundSnap = snap('aaa066');
|
||
const milGroundSnap = snap('bbb166');
|
||
const detectable = fl.getDetectableObjects({ maxCount: 1000 }).some((d) => d.id === 'GND066');
|
||
|
||
// 2. Takeoff: the flag flips false — same billboard, airborne style.
|
||
const gnd = window.__SYNTH.flights.find((f) => f.icao === 'aaa066');
|
||
gnd.onGround = false; gnd.alt = 900; gnd.vel = 75;
|
||
await fl.update(v);
|
||
const airSnap = snap('aaa066');
|
||
|
||
// 3. Landing: the flag flips back true — ground scale again, still in place.
|
||
gnd.onGround = true; gnd.alt = 150; gnd.vel = 9;
|
||
await fl.update(v);
|
||
const groundAgainSnap = snap('aaa066');
|
||
|
||
// 4. Feed-dropped grounded planes (re-pinned round 7, owner field data
|
||
// "parked planes churn/never heal"): the fast cull now requires
|
||
// AIRBORNE history this session. aaa066 flew (step 2) then landed
|
||
// then dropped → the landing-ghost fast-cull still fires after ONE
|
||
// missed poll. bbb166 was BORN grounded (parked class) → it rides
|
||
// the normal MISSING_POLL_LIMIT grace and must SURVIVE the first
|
||
// missed poll, keeping its identity/floor state across feed flaps.
|
||
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => f.icao !== 'aaa066');
|
||
window.__SYNTH.military = window.__SYNTH.military.filter((m) => m.hex !== 'bbb166');
|
||
await fl.update(v);
|
||
await mil.update(v);
|
||
return {
|
||
groundSnap, milGroundSnap, detectable, airSnap, groundAgainSnap,
|
||
droppedGone: !snap('aaa066'),
|
||
milDroppedGone: !snap('bbb166'),
|
||
};
|
||
});
|
||
const fmtSnap = (s) => (s ? `show=${s.show} scale=${s.scale.toFixed(3)} rgba=(${s.red.toFixed(2)},${s.green.toFixed(2)},${s.blue.toFixed(2)},${s.alpha.toFixed(2)})` : 'missing');
|
||
// Ground style (owner verdict 2026-07-03): FULL-ALPHA airborne tint —
|
||
// white in the flights layer, amber (#FFB800) in the military layer —
|
||
// never the 45%-alpha stale fade, never the retired gray mute. The
|
||
// ground cue is the ×0.8 scale (klass default ⇒ base 1.0).
|
||
const isFullWhite = (s) => !!s && s.show && s.alpha === 1 && s.red === 1 && s.green === 1 && s.blue === 1;
|
||
const isFullAmber = (s) => !!s && s.show && s.alpha === 1 && s.red === 1 && s.blue === 0 && Math.abs(s.green - 0xB8 / 255) < 0.02;
|
||
record('ground: on-ground plane renders full-alpha white at ground scale + detectable (flights)',
|
||
isFullWhite(ground.groundSnap) && Math.abs(ground.groundSnap.scale - 0.8) < 1e-6 && ground.detectable,
|
||
`${fmtSnap(ground.groundSnap)} detectable=${ground.detectable}`);
|
||
record('ground: military mirror — alt_baro "ground" renders full-alpha amber',
|
||
isFullAmber(ground.milGroundSnap),
|
||
fmtSnap(ground.milGroundSnap));
|
||
record('ground: takeoff flip restyles in place (white, full alpha, full scale)',
|
||
isFullWhite(ground.airSnap) && Math.abs(ground.airSnap.scale - 1) < 1e-6,
|
||
fmtSnap(ground.airSnap));
|
||
record('ground: landing flip returns to ground scale in place (no removal, still full alpha)',
|
||
isFullWhite(ground.groundAgainSnap) && Math.abs(ground.groundAgainSnap.scale - 0.8) < 1e-6,
|
||
fmtSnap(ground.groundAgainSnap));
|
||
record('ground: landing ghost fast-culls after ONE missed poll; born-parked contact survives the flap (round 7)',
|
||
ground.droppedGone && !ground.milDroppedGone,
|
||
`flights(landed ghost) gone=${ground.droppedGone} military(born parked) gone=${ground.milDroppedGone} (want true/false)`);
|
||
// Fix 2 (2026-07-03 field test): ground planes VANISHED when zooming into
|
||
// airports — grounded altitudes sit at/below the photoreal tile skin, so the
|
||
// depth test buried the billboard up close (log-depth imprecision let it win
|
||
// from orbit). RE-PINNED for round 5 (owner directive 2026-07-06: "I just
|
||
// want the planes and their lines to ALWAYS be visible... evenly
|
||
// applied"): EVERY billboard — grounded, airborne, before and after a
|
||
// ground flip — renders with disableDepthTestDistance = +Infinity. The
|
||
// old grounded-only rule kept leaving classes of contacts buried (Austin
|
||
// QNH-below-field traffic not yet flagged on_ground). Far-side contacts
|
||
// are removed by the fleet tick's horizon occluder, which never depended
|
||
// on the depth test. A real-machine eyeball should confirm: zooming from
|
||
// orbit to street level, NO aircraft icon ever vanishes into the mesh.
|
||
record('ground: depth test disabled for EVERY contact (always visible), incl. across ground flips',
|
||
ground.groundSnap?.ddtd === 'inf' && ground.milGroundSnap?.ddtd === 'inf'
|
||
&& ground.airSnap?.ddtd === 'inf' && ground.groundAgainSnap?.ddtd === 'inf',
|
||
`flights=${ground.groundSnap?.ddtd} mil=${ground.milGroundSnap?.ddtd} takeoff=${ground.airSnap?.ddtd} landing=${ground.groundAgainSnap?.ddtd}`);
|
||
|
||
// ============================================================
|
||
// GROUND 3D (2026-07-03, owner decision LOCKED): "when I have 3D mode —
|
||
// proximity or all — I want that respected regardless of whether a plane
|
||
// is on the ground or in the air. No distinction."
|
||
// (a) a synthetic on_ground plane is model-ELIGIBLE (not skipped),
|
||
// (b) it gets a model under the existing cap (both layers),
|
||
// (c) the model matrix height = stubbed scene.sampleHeight + the layer's
|
||
// belly offset — the one-shot cached ground snap — NOT the buried/
|
||
// floating feed altitude (flights alt 150 m / military "ground" → 0 m),
|
||
// (d) the TRACKED grounded plane gets the standalone tracked model,
|
||
// (e) the snap is ONE-SHOT cached: sample count stays flat across frames.
|
||
// Height probe: |modelMatrix translation| − |billboard position| — at one
|
||
// lat/lon a geodetic height change moves the point along the ellipsoid
|
||
// normal, whose angle to the radial at Austin's latitude costs < 0.001%.
|
||
//
|
||
// RETARGETED 2026-08-03 (weld). (c) still pins exactly what it always did —
|
||
// the MODEL rides the sampled skin while the BILLBOARD stays at the reported
|
||
// altitude — and that split is by design and unchanged. What changed is the
|
||
// CONSUMER anchor: detection brackets/labels and the tracked card used to
|
||
// follow the billboard, so on a grounded plane they sat ~100 m below the
|
||
// aircraft you can see and rose only as the coarse ground-floor cell warmed
|
||
// ("the buoy"), sometimes never converging. They now follow whatever owns the
|
||
// visual. So do NOT read the non-zero Δ below as "the anchor is the
|
||
// billboard" — it is the model-vs-billboard split, and the weld group that
|
||
// follows pins the anchor itself.
|
||
// ============================================================
|
||
console.log('\nGround 3D — grounded planes are model-eligible + snapped to the sampled skin');
|
||
const GROUND3D_STUB_H = 187.5; // stubbed tile-skin height (m, ellipsoid)
|
||
// Expected belly offsets (m): native origin-above-lowest-vertex × MODEL_SCALE
|
||
// × CLASS_SCALE_3D[klass] — constants match the layers' MODEL_BELLY_OFFSET_NATIVE
|
||
// / MODEL_SCALE pairs, which modelScale.test.mjs locks against the GLBs.
|
||
// Class is computed with the REAL classifier on the synthetic feed fields.
|
||
const g3dFlKlass = classifyAircraft({ typeCode: null, category: null });
|
||
const g3dMilKlass = classifyAircraft({ typeCode: 'C130', category: undefined });
|
||
const g3dFlOffset = 6.719 * 1 * (CLASS_SCALE_3D[g3dFlKlass] || 1); // airplane.glb, flights
|
||
// Hangar fleet (2026-08-16): military C130 → turboprop → the real atr72.glb,
|
||
// whose belly offset is the registry's measured bellyM (meters, scale 1) —
|
||
// NOT the jet.glb 5.631 × class formula. Pinned by modelScale.test.mjs.
|
||
const g3dMilOffset = CLASS_MODEL_REAL[g3dMilKlass]?.bellyM
|
||
?? 5.631 * 1 * (CLASS_SCALE_3D[g3dMilKlass] || 1); // jet.glb fallback classes
|
||
// Height-datum fix (Task 6): the flights-layer billboard for a grounded synthetic
|
||
// (no geo_altitude in this stub feed, no warm terrain-height cache in a headless
|
||
// run) now renders at baroM + geoidHeight(lat, lon) rather than the raw baro
|
||
// value verbatim — pickRenderAltitudeM's documented visual-fallback branch.
|
||
// Height-datum fix (Task 7): militaryFlights.js now runs the pickRenderAltitudeM
|
||
// chain too, but DELIBERATELY passes surfaceM=null for the on-ground billboard
|
||
// (brief item 3, "don't double-correct"): a grounded plane's MODEL already rides
|
||
// groundSnap.js's one-shot tileset sample, and its billboard is depth-test-free,
|
||
// so re-correcting the billboard to the ellipsoidal surface would (a) double-
|
||
// correct the same grounded plane and (b) — because military ground rows carry
|
||
// NO baro ("alt_baro":"ground") — jump the billboard 0 -> ~surface between polls,
|
||
// dragging the model's ground-snap input past groundSnap's 50 m move threshold
|
||
// and forcing a re-sample (breaks the ONE-SHOT invariant asserted below).
|
||
// So bbb177 (on-ground, no alt_geom in the stub feed, no numeric alt_baro,
|
||
// surfaceM forced null) makes pickRenderAltitudeM return its null sentinel and
|
||
// the military layer falls through to its SAME pre-Task-7 default (`altitudeM` —
|
||
// 0 m for an on-ground record with no sticky prior). Expected value unchanged
|
||
// (0 m), but now the documented Task-7 code path rather than "untouched by Task 7".
|
||
await ensureGeoidReady();
|
||
const g3dFlGeoidN = geoidHeight(30.2668, -97.7445); // aaa077's lat/lon (Austin)
|
||
|
||
let g3dSetup = null;
|
||
let g3dPrimaryFailure = null;
|
||
let g3dCleanupFailure = null;
|
||
try {
|
||
g3dSetup = await evalPage(() => {
|
||
const gev = window.__godsEyeView;
|
||
const scene = gev.viewer.scene;
|
||
const dm = gev.dataManager;
|
||
const flights = dm.layers.get('flights').module;
|
||
const military = dm.layers.get('military').module;
|
||
const priorModels3d = {
|
||
flights: flights.getParams().models3d,
|
||
military: military.getParams().models3d,
|
||
};
|
||
const priorSampleHeight = scene.sampleHeight;
|
||
// Keep the previous run-wide no-height seam in the page. Functions cannot
|
||
// cross the Puppeteer serialization boundary, so cleanup restores it from
|
||
// this private slot instead of deleting the scene's own property.
|
||
window.__g3dPriorSampleHeight = priorSampleHeight;
|
||
window.__g3dPriorTilesLoadedDescriptor = gev.tileset
|
||
? Object.getOwnPropertyDescriptor(gev.tileset, 'tilesLoaded')
|
||
: null;
|
||
try {
|
||
// 3D models on (QA param) — the owner decision under test.
|
||
flights.setParams({ models3d: true });
|
||
military.setParams({ models3d: true });
|
||
// Force the ground snap's tiles-ready gate open (b9b pattern): headless the
|
||
// Google tileset never finishes streaming, so tilesLoaded stays false.
|
||
let tilesForced = false;
|
||
try {
|
||
if (gev.tileset) {
|
||
Object.defineProperty(gev.tileset, 'tilesLoaded', { value: true, configurable: true });
|
||
tilesForced = gev.tileset.tilesLoaded === true;
|
||
} else {
|
||
tilesForced = true; // no tileset → groundSnap treats tiles as ready
|
||
}
|
||
} catch { tilesForced = false; }
|
||
// Deterministic sampleHeight stub + call counter (headless has no real skin).
|
||
window.__g3dSampleCalls = 0;
|
||
window.__g3dSampleHits = { flights: 0, military: 0 };
|
||
const fixturePoints = {
|
||
flights: { lat: 30.2668, lon: -97.7445 },
|
||
military: { lat: 30.2685, lon: -97.7470 },
|
||
};
|
||
scene.sampleHeight = function (cartographic) {
|
||
window.__g3dSampleCalls += 1;
|
||
// Attribute each sample to the fixture's distinct ~111 m mesh cell.
|
||
// This prevents one successfully sampled contact from satisfying the
|
||
// two-contact integrity assertion below.
|
||
const lat = Number(cartographic?.latitude) * 180 / Math.PI;
|
||
const lon = Number(cartographic?.longitude) * 180 / Math.PI;
|
||
if (Number.isFinite(lat) && Number.isFinite(lon)) {
|
||
for (const [layer, target] of Object.entries(fixturePoints)) {
|
||
const dLat = lat - target.lat;
|
||
const dLon = (lon - target.lon) * Math.cos(target.lat * Math.PI / 180);
|
||
// 0.0008° encloses the fixture's rounded 0.001° mesh-cell
|
||
// sample but cannot overlap the other fixture ~300 m away.
|
||
if (Math.hypot(dLat, dLon) <= 0.0008) {
|
||
window.__g3dSampleHits[layer] += 1;
|
||
}
|
||
}
|
||
}
|
||
return 187.5;
|
||
};
|
||
return { tilesForced, priorModels3d };
|
||
} catch (error) {
|
||
// Setup is transactional: once the first fixture mutation lands, every
|
||
// later setup failure restores each owned seam independently. Return
|
||
// both outcomes across the page boundary so rollback cannot hide or
|
||
// replace the primary setup error.
|
||
const rollbackFailures = [];
|
||
const attemptRollback = (label, operation) => {
|
||
try { operation(); } catch (rollbackError) {
|
||
rollbackFailures.push(`${label}: ${rollbackError?.message || rollbackError}`);
|
||
}
|
||
};
|
||
attemptRollback('restore sampleHeight seam', () => {
|
||
scene.sampleHeight = priorSampleHeight;
|
||
});
|
||
attemptRollback('restore tilesLoaded seam', () => {
|
||
if (!gev.tileset) return;
|
||
const prior = window.__g3dPriorTilesLoadedDescriptor;
|
||
if (prior) Object.defineProperty(gev.tileset, 'tilesLoaded', prior);
|
||
else delete gev.tileset.tilesLoaded;
|
||
});
|
||
attemptRollback('restore flights models3d', () => {
|
||
flights.setParams({ models3d: priorModels3d.flights });
|
||
});
|
||
attemptRollback('restore military models3d', () => {
|
||
military.setParams({ models3d: priorModels3d.military });
|
||
});
|
||
attemptRollback('delete fixture globals', () => {
|
||
delete window.__g3dPriorSampleHeight;
|
||
delete window.__g3dPriorTilesLoadedDescriptor;
|
||
delete window.__g3dSampleCalls;
|
||
delete window.__g3dSampleHits;
|
||
});
|
||
return {
|
||
setupFailure: error?.message || String(error),
|
||
rollbackFailures,
|
||
priorModels3d,
|
||
};
|
||
}
|
||
});
|
||
if (g3dSetup.setupFailure) {
|
||
g3dPrimaryFailure = new Error(`ground-3d setup failed: ${g3dSetup.setupFailure}`);
|
||
if (g3dSetup.rollbackFailures.length > 0) {
|
||
g3dCleanupFailure = new Error(
|
||
`ground-3d setup rollback failed: ${g3dSetup.rollbackFailures.join(' | ')}`,
|
||
);
|
||
}
|
||
// Setup rollback already attempted every owned seam. Prevent the
|
||
// post-setup cleanup from running against deleted fixture globals.
|
||
g3dSetup = null;
|
||
throw g3dPrimaryFailure;
|
||
}
|
||
record('ground-3d: sampleHeight stub installed + tiles-ready forced', g3dSetup.tilesForced,
|
||
JSON.stringify(g3dSetup));
|
||
|
||
// Ingest one grounded plane per layer, then park the camera 8 km above them
|
||
// (inside the model regime + add radius; on-screen so they win cap slots).
|
||
const g3dIngest = await evalPage(async () => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const dm = window.__godsEyeView.dataManager;
|
||
const fl = dm.layers.get('flights').module;
|
||
const mil = dm.layers.get('military').module;
|
||
window.__SYNTH.flights.push({ icao: 'aaa077', callsign: 'GND077', lon: -97.7445, lat: 30.2668, alt: 150, vel: 0, track: 45, onGround: true });
|
||
window.__SYNTH.military.push({ hex: 'bbb177', flight: 'MILGND7', lon: -97.7470, lat: 30.2685, altFt: 'ground', track: 120, gsKt: 0, t: 'C130', r: 'AF-177' });
|
||
await fl.update(v);
|
||
await mil.update(v);
|
||
const findBB = (id) => {
|
||
let found = null;
|
||
const walk = (coll) => {
|
||
const n = coll.length;
|
||
for (let i = 0; i < n; i++) {
|
||
let p;
|
||
try { p = coll.get(i); } catch { continue; }
|
||
if (!p) continue;
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
if (p.image !== undefined && p.alignedAxis !== undefined && p.id === id) found = p;
|
||
}
|
||
};
|
||
walk(v.scene.primitives);
|
||
return found;
|
||
};
|
||
const flBB = findBB('aaa077');
|
||
const milBB = findBB('bbb177');
|
||
if (!flBB || !milBB) return { error: `ground billboards missing (fl=${!!flBB} mil=${!!milBB})` };
|
||
const radius = (pos) => Math.hypot(pos.x, pos.y, pos.z);
|
||
// Park the camera 8 km radially above the flights plane (both in view nadir-ish).
|
||
const p = flBB.position;
|
||
const r = Math.hypot(p.x, p.y, p.z);
|
||
const k = (r + 8000) / r;
|
||
v.camera.cancelFlight();
|
||
v.camera.setView({
|
||
destination: { x: p.x * k, y: p.y * k, z: p.z * k },
|
||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||
});
|
||
return { flBBRadius: radius(flBB.position), milBBRadius: radius(milBB.position) };
|
||
});
|
||
if (g3dIngest.error) {
|
||
record('ground-3d: grounded synthetics ingested', false, g3dIngest.error);
|
||
} else {
|
||
record('ground-3d: grounded synthetics ingested', true,
|
||
`bb radii fl=${g3dIngest.flBBRadius.toFixed(1)} mil=${g3dIngest.milBBRadius.toFixed(1)}`);
|
||
|
||
// In-page model finder by pick id (fleet AND tracked standalone models carry it).
|
||
await evalPage(() => {
|
||
window.__g3dFindModel = function (id) {
|
||
const v = window.__godsEyeView.viewer;
|
||
let found = null;
|
||
const walk = (coll) => {
|
||
const n = coll.length;
|
||
for (let i = 0; i < n; i++) {
|
||
let p;
|
||
try { p = coll.get(i); } catch { continue; }
|
||
if (!p) continue;
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
if (p.modelMatrix && typeof p.ready !== 'undefined' && p.id === id) found = p;
|
||
}
|
||
};
|
||
walk(v.scene.primitives);
|
||
return found;
|
||
};
|
||
});
|
||
|
||
// (a)+(b): both grounded planes become models under the cap (the old code
|
||
// `continue`d past them in the eligibility pre-pass — no model would EVER
|
||
// appear). Wait for ready+shown so the handoff assert below is meaningful.
|
||
const g3dModelsUp = await page.waitForFunction(() => {
|
||
const fm = window.__g3dFindModel('aaa077');
|
||
const mm = window.__g3dFindModel('bbb177');
|
||
return !!(fm && fm.ready && fm.show && mm && mm.ready && mm.show);
|
||
}, { timeout: 40000, polling: 250 }).then(() => true).catch(() => false);
|
||
if (!glbBackendCapable && !g3dModelsUp) {
|
||
skip('ground-3d: grounded planes are model-eligible and modeled (both layers)',
|
||
`independent GLB control unavailable (${glbControlDetail}); grounded GLB readiness is not observable`);
|
||
} else {
|
||
record('ground-3d: grounded planes are model-eligible and modeled (both layers)', g3dModelsUp,
|
||
g3dModelsUp ? 'fleet models up (ready+shown)' : 'models never became ready+shown');
|
||
}
|
||
|
||
if (g3dModelsUp) {
|
||
// (c) + handoff + (e): heights snapped, billboards handed off, snap one-shot.
|
||
const g3dState = await evalPage(() => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const findBB = (id) => {
|
||
let found = null;
|
||
const walk = (coll) => {
|
||
const n = coll.length;
|
||
for (let i = 0; i < n; i++) {
|
||
let p;
|
||
try { p = coll.get(i); } catch { continue; }
|
||
if (!p) continue;
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
if (p.image !== undefined && p.alignedAxis !== undefined && p.id === id) found = p;
|
||
}
|
||
};
|
||
walk(v.scene.primitives);
|
||
return found;
|
||
};
|
||
const modelRadius = (id) => {
|
||
const m = window.__g3dFindModel(id);
|
||
return m ? Math.hypot(m.modelMatrix[12], m.modelMatrix[13], m.modelMatrix[14]) : null;
|
||
};
|
||
return {
|
||
flModelRadius: modelRadius('aaa077'),
|
||
milModelRadius: modelRadius('bbb177'),
|
||
flBBShown: findBB('aaa077')?.show ?? null,
|
||
milBBShown: findBB('bbb177')?.show ?? null,
|
||
sampleCalls: window.__g3dSampleCalls,
|
||
sampleHits: { ...window.__g3dSampleHits },
|
||
};
|
||
});
|
||
// Expected radial delta = (stub + offset) − billboard's rendered altitude.
|
||
// Flights billboard altitude is baroM + geoidN (height-datum fix, Task 6 —
|
||
// no geo_altitude/warm terrain-cache in this synthetic run, so the
|
||
// baro-fallback branch of pickRenderAltitudeM applies). Military (Task 7)
|
||
// runs pickRenderAltitudeM too but forces surfaceM=null for on-ground
|
||
// billboards (don't double-correct the grounded model's tileset snap — see
|
||
// the block above), and this synthetic has no alt_geom/alt_baro, so its
|
||
// billboard stays at the pre-Task-7 "ground" -> 0 m default.
|
||
const flWantDelta = (GROUND3D_STUB_H + g3dFlOffset) - (150 + g3dFlGeoidN);
|
||
const milWantDelta = (GROUND3D_STUB_H + g3dMilOffset) - 0; // "ground" (no alt_geom/alt_baro, surfaceM forced null) → 0 m default
|
||
const flDelta = g3dState.flModelRadius - g3dIngest.flBBRadius;
|
||
const milDelta = g3dState.milModelRadius - g3dIngest.milBBRadius;
|
||
const HEIGHT_TOL_M = 1.0;
|
||
record(`ground-3d: flights model height = sampled skin + belly offset (${g3dFlKlass})`,
|
||
Math.abs(flDelta - flWantDelta) <= HEIGHT_TOL_M,
|
||
`Δradial=${flDelta.toFixed(2)} m want=${flWantDelta.toFixed(2)} m (stub ${GROUND3D_STUB_H} + offset ${g3dFlOffset.toFixed(2)} − alt ${(150 + g3dFlGeoidN).toFixed(2)} [150 baro + ${g3dFlGeoidN.toFixed(2)} geoidN])`);
|
||
record(`ground-3d: military model height = sampled skin + belly offset (${g3dMilKlass})`,
|
||
Math.abs(milDelta - milWantDelta) <= HEIGHT_TOL_M,
|
||
`Δradial=${milDelta.toFixed(2)} m want=${milWantDelta.toFixed(2)} m (stub ${GROUND3D_STUB_H} + offset ${g3dMilOffset.toFixed(2)} − alt 0)`);
|
||
record('ground-3d: billboard→model handoff holds on the ground (icons hidden once models render)',
|
||
g3dState.flBBShown === false && g3dState.milBBShown === false,
|
||
`fl bb.show=${g3dState.flBBShown} mil bb.show=${g3dState.milBBShown}`);
|
||
const bothGroundContactsSampled = g3dState.sampleHits?.flights > 0
|
||
&& g3dState.sampleHits?.military > 0;
|
||
record('ground-3d: both grounded synthetic contacts reached the sampling seam',
|
||
bothGroundContactsSampled,
|
||
`sample hits near distinct fixture cells: flights=${g3dState.sampleHits?.flights ?? 0}, military=${g3dState.sampleHits?.military ?? 0}`);
|
||
|
||
// ============================================================
|
||
// WELD (2026-08-03): the detection anchor follows the RENDERED aircraft.
|
||
// With a model owning the visual, getDetectableObjects must return the
|
||
// model's actual rendered translation, not the billboard position — the
|
||
// two are ~100 m apart on a grounded plane. Sampled every frame for 20
|
||
// frames because the failure this replaces was TEMPORAL: the bracket used
|
||
// to converge slowly (or never) as the coarse floor cell warmed, so a
|
||
// single-frame probe could pass while the live behaviour still drifted.
|
||
// ============================================================
|
||
const WELD_TOL_M = 2.0;
|
||
// This guard proves the display and visual accessors have not collapsed
|
||
// into the same position. Do not reuse WELD_TOL_M here: the measured
|
||
// split includes wall-clock dead reckoning and can validly be only the
|
||
// 1.51 m airliner belly offset on a faster runtime.
|
||
const ACCESSOR_SEPARATION_EPSILON_M = 0.25;
|
||
const weld = await page.evaluate(async (frames) => {
|
||
const gev = window.__godsEyeView;
|
||
const v = gev.viewer;
|
||
const dm = gev.dataManager;
|
||
const targets = [
|
||
{ layer: 'flights', id: 'aaa077', center: [0, 0, 0] },
|
||
{ layer: 'military', id: 'bbb177', center: [0, 0, 0] },
|
||
];
|
||
const out = {};
|
||
for (const t of targets) out[t.layer] = { maxDelta: 0, samples: 0, missing: 0 };
|
||
await new Promise((res) => {
|
||
let n = 0;
|
||
let settled = false;
|
||
const finish = () => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
stop();
|
||
res();
|
||
};
|
||
const stop = v.scene.postRender.addEventListener(() => {
|
||
for (const t of targets) {
|
||
const rec = out[t.layer];
|
||
const mod = dm.layers.get(t.layer)?.module;
|
||
const model = window.__g3dFindModel(t.id);
|
||
const objects = mod?.getDetectableObjects?.({
|
||
mode: 'DENSE', maxCount: 5000, seed: 0,
|
||
}) || [];
|
||
const object = objects.find((o) => o && o.sourceId === t.id);
|
||
if (!object || !object.position || !model || !model.show) {
|
||
rec.missing++;
|
||
continue;
|
||
}
|
||
const matrix = model.modelMatrix;
|
||
const scale = Number.isFinite(model.computedScale)
|
||
? model.computedScale
|
||
: (Number.isFinite(model.scale) ? model.scale : 1);
|
||
const x = t.center[0] * scale;
|
||
const y = t.center[1] * scale;
|
||
const z = t.center[2] * scale;
|
||
const visual = {
|
||
x: matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
|
||
y: matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
|
||
z: matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14],
|
||
};
|
||
const d = Math.hypot(
|
||
object.position.x - visual.x,
|
||
object.position.y - visual.y,
|
||
object.position.z - visual.z,
|
||
);
|
||
if (d > rec.maxDelta) rec.maxDelta = d;
|
||
rec.samples++;
|
||
}
|
||
if (++n >= frames) {
|
||
finish();
|
||
return;
|
||
}
|
||
v.scene.requestRender();
|
||
});
|
||
const timer = setTimeout(finish, Math.max(15000, frames * 1500));
|
||
v.scene.requestRender();
|
||
});
|
||
return out;
|
||
}, 20);
|
||
for (const layer of ['flights', 'military']) {
|
||
const w = weld[layer] || { samples: 0, missing: -1, maxDelta: Infinity };
|
||
record(`weld: ${layer} grounded detection anchor === rendered model visual centre (20 frames)`,
|
||
w.samples >= 15 && w.missing === 0 && w.maxDelta <= WELD_TOL_M,
|
||
`samples=${w.samples} missing=${w.missing} maxΔ=${w.maxDelta.toFixed(3)} m (tol ${WELD_TOL_M} m)`);
|
||
}
|
||
|
||
// (e) one-shot: drive 60 verified render frames. Correct placement above
|
||
// proves the deterministic sample landed; the exact initial call total is
|
||
// NOT a contract. A successful ground snap publishes the validated height
|
||
// into the shared mesh-floor cell, so the later poll-time sampler may
|
||
// legitimately skip that cell. The load-bearing invariant is bounded
|
||
// growth across frames, with an explicit guard against a timed-out driver
|
||
// falsely looking flat.
|
||
const callsBefore = g3dState.sampleCalls;
|
||
const fleetSampleWindow = await sampleFrames(
|
||
page, 60, () => window.__g3dSampleCalls,
|
||
);
|
||
const callsAfter = await evalPage(() => window.__g3dSampleCalls);
|
||
const fleetFramesComplete = !fleetSampleWindow.timedOut
|
||
&& fleetSampleWindow.values.length === 60;
|
||
record('ground-3d: fleet sampling window completed all 60 requested frames',
|
||
fleetFramesComplete,
|
||
`frames=${fleetSampleWindow.values.length}/60 timedOut=${fleetSampleWindow.timedOut}`);
|
||
record('ground-3d: fleet ground sampling is one-shot/per-poll bounded (no per-frame sampling)',
|
||
fleetFramesComplete && bothGroundContactsSampled && (callsAfter - callsBefore) <= 8,
|
||
`sampleHeight calls: before=${callsBefore}, growth over 60 verified frames=${callsAfter - callsBefore} (per-frame would be ~60+)`);
|
||
|
||
// (d) TRACKED grounded plane → the standalone tracked model (the owner's
|
||
// "tracked SWA143 at 0 kts stayed a 2D cyan billboard" case).
|
||
// Start the counter BEFORE ownership changes. The previous guard began
|
||
// only after ready+shown and could miss a regression that sampled every
|
||
// render frame while the standalone model was loading.
|
||
const trackedTransitionCallsBefore = callsAfter;
|
||
await evalPage(() => {
|
||
window.__godsEyeView.dataManager.layers.get('flights').module.trackById('aaa077');
|
||
});
|
||
const trackedTransitionWindow = await sampleFrames(
|
||
page, 30, () => window.__g3dSampleCalls,
|
||
);
|
||
const trackedTransitionCallsAfter = await evalPage(() => window.__g3dSampleCalls);
|
||
const trackedTransitionFramesComplete = !trackedTransitionWindow.timedOut
|
||
&& trackedTransitionWindow.values.length === 30;
|
||
record('ground-3d: tracked loading/ownership sampling window completed all 30 requested frames',
|
||
trackedTransitionFramesComplete,
|
||
`frames=${trackedTransitionWindow.values.length}/30 timedOut=${trackedTransitionWindow.timedOut}`);
|
||
record('ground-3d: tracked loading/ownership ground sampling is bounded (no per-frame sampling)',
|
||
trackedTransitionFramesComplete
|
||
&& (trackedTransitionCallsAfter - trackedTransitionCallsBefore) <= 8,
|
||
`sampleHeight growth from before trackById across 30 verified frames=${trackedTransitionCallsAfter - trackedTransitionCallsBefore} (per-frame would be ~30+)`);
|
||
const g3dTrackedUp = await page.waitForFunction(() => {
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const ti = fl.getTrackedInfo();
|
||
if (!ti || ti.icao24 !== 'aaa077') return false;
|
||
const m = window.__g3dFindModel('aaa077');
|
||
return !!(m && m.ready && m.show);
|
||
}, { timeout: 25000, polling: 250 }).then(() => true).catch(() => false);
|
||
let trackedDetail = 'tracked model never became ready+shown';
|
||
let trackedHeightOk = false;
|
||
if (g3dTrackedUp) {
|
||
const trackedRadius = await evalPage(() => {
|
||
const m = window.__g3dFindModel('aaa077');
|
||
return m ? Math.hypot(m.modelMatrix[12], m.modelMatrix[13], m.modelMatrix[14]) : null;
|
||
});
|
||
const tDelta = trackedRadius - g3dIngest.flBBRadius;
|
||
trackedHeightOk = Math.abs(tDelta - flWantDelta) <= HEIGHT_TOL_M;
|
||
trackedDetail = `tracked model up; Δradial=${tDelta.toFixed(2)} m want=${flWantDelta.toFixed(2)} m`;
|
||
}
|
||
record('ground-3d: TRACKED grounded plane gets the standalone tracked model, ground-snapped',
|
||
g3dTrackedUp && trackedHeightOk, trackedDetail);
|
||
|
||
if (g3dTrackedUp) {
|
||
const trackedReadyCallsBefore = await evalPage(() => window.__g3dSampleCalls);
|
||
const trackedReadyWindow = await sampleFrames(
|
||
page, 30, () => window.__g3dSampleCalls,
|
||
);
|
||
const trackedReadyCallsAfter = await evalPage(() => window.__g3dSampleCalls);
|
||
const trackedReadyFramesComplete = !trackedReadyWindow.timedOut
|
||
&& trackedReadyWindow.values.length === 30;
|
||
record('ground-3d: tracked ready-state sampling window completed all 30 requested frames',
|
||
trackedReadyFramesComplete,
|
||
`frames=${trackedReadyWindow.values.length}/30 timedOut=${trackedReadyWindow.timedOut}`);
|
||
record('ground-3d: tracked ready-state ground sampling is bounded (no per-frame sampling)',
|
||
trackedReadyFramesComplete
|
||
&& (trackedReadyCallsAfter - trackedReadyCallsBefore) <= 8,
|
||
`sampleHeight growth over 30 verified ready-state frames=${trackedReadyCallsAfter - trackedReadyCallsBefore} (per-frame would be ~30+)`);
|
||
} else {
|
||
record('ground-3d: tracked ready-state sampling window completed all 30 requested frames',
|
||
false, 'tracked model never became ready+shown');
|
||
record('ground-3d: tracked ready-state ground sampling is bounded (no per-frame sampling)',
|
||
false, 'tracked model never became ready+shown');
|
||
}
|
||
|
||
// WELD (tracked): the tracked CARD anchors to the model you can see.
|
||
// `gevVisualPosition` is a SEPARATE accessor from `gevDisplayPosition` on
|
||
// purpose — the latter carries the follow-camera anti-jitter contract and
|
||
// must keep returning the cached dead-reckoned value the camera settled
|
||
// on. So this asserts both halves: the visual accessor is welded to the
|
||
// rendered model, AND the display accessor still reports the (different)
|
||
// dead-reckoned position rather than having been quietly repurposed.
|
||
if (g3dTrackedUp) {
|
||
const trackedWeld = await evalPage(() => {
|
||
const ent = window.__godsEyeView.viewer.trackedEntity;
|
||
const m = window.__g3dFindModel('aaa077');
|
||
if (!m) return { error: 'no tracked model' };
|
||
const matrix = m.modelMatrix;
|
||
const scale = Number.isFinite(m.computedScale)
|
||
? m.computedScale
|
||
: (Number.isFinite(m.scale) ? m.scale : 1);
|
||
// airplane.glb is now bounding-box centred with transforms applied.
|
||
const x = 0 * scale;
|
||
const y = 0 * scale;
|
||
const z = 0 * scale;
|
||
const anchor = {
|
||
x: matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
|
||
y: matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
|
||
z: matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14],
|
||
};
|
||
const at = (p) => (p ? Math.hypot(
|
||
p.x - anchor.x, p.y - anchor.y, p.z - anchor.z,
|
||
) : null);
|
||
const visual = typeof ent?.gevVisualPosition === 'function' ? ent.gevVisualPosition() : null;
|
||
const display = typeof ent?.gevDisplayPosition === 'function' ? ent.gevDisplayPosition() : null;
|
||
return {
|
||
hasVisualAccessor: typeof ent?.gevVisualPosition === 'function',
|
||
visualDelta: at(visual),
|
||
displayDelta: at(display),
|
||
};
|
||
});
|
||
const visualOk = trackedWeld.hasVisualAccessor
|
||
&& Number.isFinite(trackedWeld.visualDelta)
|
||
&& trackedWeld.visualDelta <= WELD_TOL_M;
|
||
record('weld: TRACKED card anchor === rendered tracked model visual centre',
|
||
visualOk,
|
||
trackedWeld.error
|
||
|| `visualΔ=${Number(trackedWeld.visualDelta).toFixed(3)} m (tol ${WELD_TOL_M} m), accessor=${trackedWeld.hasVisualAccessor}`);
|
||
// Separation guard: if displayDelta ever collapses to visualDelta, the
|
||
// anti-jitter accessor has been repurposed and the follow camera is at
|
||
// risk. Skipped (not failed) when the DR cache is momentarily invalid.
|
||
const displayDelta = trackedWeld.displayDelta;
|
||
record('weld: gevDisplayPosition still reports the dead-reckoned position (anti-jitter intact)',
|
||
!Number.isFinite(displayDelta) || displayDelta > ACCESSOR_SEPARATION_EPSILON_M,
|
||
Number.isFinite(displayDelta)
|
||
? `displayΔ=${displayDelta.toFixed(3)} m (must stay > ${ACCESSOR_SEPARATION_EPSILON_M} m — the ground-snap split)`
|
||
: 'display accessor returned null this frame (DR cache invalid) — separation not evaluated');
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// Contain the scenario so a primary fixture failure survives cleanup and
|
||
// later groups plus the run-wide report still execute.
|
||
g3dPrimaryFailure = error;
|
||
} finally {
|
||
// This group temporarily owns the sampling seam, model toggle, synthetics,
|
||
// tracking state, and page globals. Release every one on error/timeout as
|
||
// well as on the happy path so later height-datum scenarios cannot inherit
|
||
// a deterministic skin or a tracked model from this fixture.
|
||
if (g3dSetup) {
|
||
try {
|
||
const cleanupFailures = await evalPage(async (priorModels3d) => {
|
||
const gev = window.__godsEyeView;
|
||
const v = gev.viewer;
|
||
const dm = gev.dataManager;
|
||
const fl = dm.layers.get('flights').module;
|
||
const mil = dm.layers.get('military').module;
|
||
const failures = [];
|
||
const attempt = async (label, operation) => {
|
||
try { await operation(); } catch (error) {
|
||
failures.push(`${label}: ${error?.message || error}`);
|
||
}
|
||
};
|
||
await attempt('stop tracking', () => fl.stopTracking());
|
||
await attempt('remove flights synthetic', () => {
|
||
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => f.icao !== 'aaa077');
|
||
});
|
||
await attempt('remove military synthetic', () => {
|
||
window.__SYNTH.military = window.__SYNTH.military.filter((m) => m.hex !== 'bbb177');
|
||
});
|
||
await attempt('refresh flights', () => fl.update(v));
|
||
await attempt('refresh military', () => mil.update(v));
|
||
await attempt('restore sampleHeight seam', () => {
|
||
v.scene.sampleHeight = window.__g3dPriorSampleHeight;
|
||
});
|
||
await attempt('restore tilesLoaded seam', () => {
|
||
if (!gev.tileset) return;
|
||
const prior = window.__g3dPriorTilesLoadedDescriptor;
|
||
if (prior) Object.defineProperty(gev.tileset, 'tilesLoaded', prior);
|
||
else delete gev.tileset.tilesLoaded;
|
||
});
|
||
await attempt('restore flights models3d', () => {
|
||
fl.setParams({ models3d: priorModels3d.flights });
|
||
});
|
||
await attempt('restore military models3d', () => {
|
||
mil.setParams({ models3d: priorModels3d.military });
|
||
});
|
||
await attempt('delete fixture globals', () => {
|
||
delete window.__g3dPriorSampleHeight;
|
||
delete window.__g3dPriorTilesLoadedDescriptor;
|
||
delete window.__g3dFindModel;
|
||
delete window.__g3dSampleCalls;
|
||
delete window.__g3dSampleHits;
|
||
});
|
||
return failures;
|
||
}, g3dSetup.priorModels3d);
|
||
if (cleanupFailures.length > 0) {
|
||
g3dCleanupFailure = new Error(`ground-3d cleanup failed: ${cleanupFailures.join(' | ')}`);
|
||
}
|
||
} catch (error) {
|
||
// A page-evaluation failure is itself cleanup evidence, but it must
|
||
// not replace the primary error or abort the remaining harness.
|
||
g3dCleanupFailure = error;
|
||
}
|
||
}
|
||
}
|
||
record('ground-3d: scenario completed without an unhandled fixture error',
|
||
g3dPrimaryFailure === null,
|
||
g3dPrimaryFailure
|
||
? `primary failure: ${g3dPrimaryFailure?.message || g3dPrimaryFailure}`
|
||
: 'primary path completed');
|
||
record('ground-3d: fixture cleanup completed without errors',
|
||
g3dCleanupFailure === null,
|
||
g3dCleanupFailure
|
||
? `cleanup failure: ${g3dCleanupFailure?.message || g3dCleanupFailure}`
|
||
: (g3dSetup ? 'all owned fixture state released' : 'post-setup cleanup not required'));
|
||
|
||
// ============================================================
|
||
// CHANGE 4 (2026-07-03): arrival rotation freshness. Field test: "planes
|
||
// look weird when you first come to them — rotations are weird". Two
|
||
// repaired holes, both layers:
|
||
// (a) SETTLE PASS — camera.moveEnd forces a full rotation pass on the
|
||
// next frame (the pose-signature gate can eat a fly-to's settle:
|
||
// the final easing frames land inside one 10 m/0.06° quantization
|
||
// bucket, leaving mid-flight noses for up to ROTATION_REFRESH_MS).
|
||
// (b) REVEAL PASS — a billboard flipping INTO view while the camera
|
||
// idles gets its rotation refreshed the same tick (it used to
|
||
// reappear wearing its stale/creation-north nose for up to 1 s).
|
||
// Staged deterministically: park the camera nadir over the synthetic
|
||
// cluster, let rotations settle, TAMPER a billboard's rotation, then
|
||
// trigger each path with the camera idle. The idle-drift catch-up
|
||
// (ROTATION_REFRESH_MS = 1 s) can coincidentally mask a regression in a
|
||
// ~300 ms SwiftShader frame window, so a regression shows as an
|
||
// intermittent (not guaranteed) failure — never a false failure.
|
||
// ============================================================
|
||
console.log('\nChange 4 — arrival rotation: moveEnd settle pass + horizon-reveal pass');
|
||
const arrival = await evalPage(async () => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const dm = window.__godsEyeView.dataManager;
|
||
const { screenProjectedRotation } = await import('/src/data/iconOrientation.js');
|
||
// 3D models OFF for this phase: a model-handed-off billboard is hidden
|
||
// and skips rotation updates entirely — the probes need live billboards
|
||
// (this is also the app's default state the field report came from).
|
||
dm.layers.get('flights').module.setParams({ models3d: false });
|
||
dm.layers.get('military').module.setParams({ models3d: false });
|
||
const findBB = (id) => {
|
||
let found = null;
|
||
const walk = (coll) => {
|
||
const n = coll.length;
|
||
for (let i = 0; i < n; i++) {
|
||
let p;
|
||
try { p = coll.get(i); } catch { continue; }
|
||
if (!p) continue;
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
if (p.image !== undefined && p.alignedAxis !== undefined && p.id === id) found = p;
|
||
}
|
||
};
|
||
walk(v.scene.primitives);
|
||
return found;
|
||
};
|
||
const nextFrames = (n) => new Promise((res) => {
|
||
let count = 0;
|
||
let settled = false;
|
||
const finish = () => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
rm();
|
||
res();
|
||
};
|
||
const rm = v.scene.postRender.addEventListener(() => {
|
||
if (++count >= n) {
|
||
finish();
|
||
return;
|
||
}
|
||
v.scene.requestRender();
|
||
});
|
||
const timer = setTimeout(finish, Math.max(10000, n * 1500));
|
||
v.scene.requestRender();
|
||
});
|
||
// Park the camera straight above the flights cluster (lift a live
|
||
// billboard's position radially — no Cesium global needed in-page) so
|
||
// every probe plane projects on-screen, then let rotations settle at
|
||
// the new pose (the setView pose change forces a pass on the next tick).
|
||
// aaa002 — aaa001 was evicted for good by the M3 age-out phase.
|
||
const anchor = findBB('aaa002');
|
||
if (!anchor) return { error: 'anchor aaa002 missing' };
|
||
const p = anchor.position;
|
||
const r = Math.hypot(p.x, p.y, p.z);
|
||
const k = (r + 25000) / r;
|
||
v.camera.cancelFlight();
|
||
v.camera.setView({
|
||
destination: { x: p.x * k, y: p.y * k, z: p.z * k },
|
||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||
});
|
||
await nextFrames(3);
|
||
const angDiff = (a, b) => Math.abs(Math.atan2(Math.sin(a - b), Math.cos(a - b)));
|
||
const probe = async (id, course) => {
|
||
const bb = findBB(id);
|
||
if (!bb || !bb.show) return { error: `${id} missing/hidden` };
|
||
const r0 = bb.rotation; // settled tamper origin; correctness uses a fresh projection below
|
||
// (a) settle pass: tamper, then raise moveEnd with the camera IDLE —
|
||
// the pose signature is unchanged, so only the moveEnd hook can fix
|
||
// this before the 1 s catch-up.
|
||
bb.rotation = r0 + 1.5;
|
||
v.camera.moveEnd.raiseEvent();
|
||
await nextFrames(2);
|
||
const afterMoveEnd = bb.rotation;
|
||
// A real-GPU fleet tick can land near the edge of the frame budget and
|
||
// advance the contact before the probe reads it. Compare with the
|
||
// production projection at the CURRENT position, not the now-stale r0.
|
||
const expectedMoveEnd = screenProjectedRotation(v.scene, bb.position, course, null);
|
||
// (b) reveal pass: tamper + hide — the next fleet tick must flip it
|
||
// visible AND correct the nose in that same tick (camera still idle,
|
||
// no moveEnd raised). Diagnostic fields (round 5): record WHICH frame
|
||
// the flip landed on (flipFrame; -1 = never within the extended
|
||
// window) and whether a 3D model owns this id — distinguishes "tick
|
||
// slow under load" from "occluder says hidden" from "model handoff".
|
||
bb.rotation = r0 + 1.5;
|
||
bb.show = false;
|
||
let flipFrame = -1;
|
||
for (let f = 1; f <= 6; f++) {
|
||
await nextFrames(1);
|
||
if (bb.show && flipFrame === -1) flipFrame = f;
|
||
if (f >= 2 && flipFrame !== -1) break;
|
||
}
|
||
const expectedReveal = screenProjectedRotation(v.scene, bb.position, course, null);
|
||
const hasModel = !!(window.__g3dFindModel && window.__g3dFindModel(id));
|
||
return {
|
||
r0,
|
||
dMoveEnd: angDiff(afterMoveEnd, expectedMoveEnd),
|
||
dReveal: angDiff(bb.rotation, expectedReveal),
|
||
shown: bb.show, flipFrame, hasModel,
|
||
};
|
||
};
|
||
const flightCourse = window.__SYNTH.flights.find((f) => f.icao === 'aaa002')?.track ?? 0;
|
||
const militaryCourse = window.__SYNTH.military.find((m) => m.hex === 'bbb101')?.track ?? 0;
|
||
return {
|
||
flights: await probe('aaa002', flightCourse),
|
||
military: await probe('bbb101', militaryCourse),
|
||
};
|
||
});
|
||
const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? a.error
|
||
: `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° shown=${a.shown} flipFrame=${a.flipFrame} hasModel=${a.hasModel}`);
|
||
const TAMPER_DEG = 1.5 * 180 / Math.PI; // ≈ 86° — an uncorrected nose stays this far off
|
||
// Reveal budget re-pinned 2 → 4 frames (round 5): the diagnostic probe
|
||
// shows the military flip consistently lands on frame 2 — the exact edge
|
||
// of the old budget — so any load hiccup flaked it (flights lands frame
|
||
// 1). The regression this protects against is a reveal that waits for
|
||
// the 1 s idle catch-up (~30-60 frames) or never re-aims; 4 frames still
|
||
// catches that with a wide margin, with zero-headroom flakes gone.
|
||
const arrOk = (a) => !!a && !a.error && a.shown
|
||
&& (a.flipFrame === -1 ? false : a.flipFrame <= 4)
|
||
&& a.dMoveEnd * 180 / Math.PI < 10 && a.dReveal * 180 / Math.PI < 10;
|
||
record(`arrival: moveEnd + reveal both re-aim within 4 frames, flights (tamper was ${TAMPER_DEG.toFixed(0)}°)`,
|
||
arrOk(arrival.flights), fmtArr(arrival.flights));
|
||
record(`arrival: moveEnd + reveal both re-aim within 4 frames, military (tamper was ${TAMPER_DEG.toFixed(0)}°)`,
|
||
arrOk(arrival.military), fmtArr(arrival.military));
|
||
|
||
// ============================================================
|
||
// DISPLAY FLOOR (2026-08-19): a grounded contact must never render below
|
||
// the floor of the cell it is DISPLAYED over.
|
||
//
|
||
// `renderAltitudeM` is picked once per poll from the floor of the FIX's
|
||
// coarse cell, but what renders is the dead-reckoned position — which
|
||
// drifts across cells for the whole 30 s segment and, while a ground
|
||
// contact coasts on a stale feed, for hundreds of metres more. On a graded
|
||
// apron (KAUS spans ~119–140 m ellipsoidal) the sprite ends up under the
|
||
// mesh it taxied over; qa-floor-verify measured −15.5 m and reported
|
||
// VERDICT: FAIL on main.
|
||
//
|
||
// Staged so ONLY the display path can produce the lift: the contact's own
|
||
// fix cell is left COLD and the seeded floor is planted on the cells the
|
||
// display has drifted onto. A poll-path fix would read the fix cell and
|
||
// find nothing there, so a pass here cannot come from anywhere else.
|
||
// ============================================================
|
||
console.log('\nDisplay floor — grounded contacts never render below the cell they drifted onto');
|
||
const DISPLAY_FLOOR_LIFT_M = 1.5; // GROUND_FLOOR_LIFT_M
|
||
const DF_LAT = 30.2600, DF_LON = -97.7500;
|
||
const DF_SEED_M = 400; // identity-probe floor for the fix cell
|
||
|
||
const dfSetup = await evalPage(async () => {
|
||
const Cesium = await import('/node_modules/cesium/Build/Cesium/index.js');
|
||
const gev = window.__godsEyeView;
|
||
const v = gev.viewer;
|
||
const fl = gev.dataManager.layers.get('flights').module;
|
||
// Hermetic: the ground-3d group's cleanup restored the REAL sampleHeight,
|
||
// which would let the mesh sampler latch whatever headless GL streams.
|
||
//
|
||
// `__dfSkinM` is the switch, and it is load-bearing rather than tidy. A
|
||
// permanently-missing stub means groundSnap can never resolve, and a
|
||
// grounded MODEL cannot be placed without a ground height — so every
|
||
// grounded-model assertion in this group becomes unreachable and reports
|
||
// as a skip about GLB backends instead of as the contract it is. null is
|
||
// the group default (the honest "no tiles here" the earlier scenarios
|
||
// need, and the COLD case in its own right); a number opens a
|
||
// DETERMINISTIC skin for the scenarios that need a model to draw.
|
||
window.__dfSkinM = null;
|
||
window.__dfSkinSamples = [];
|
||
window.__dfPriorTilesLoadedDescriptor = gev.tileset
|
||
? Object.getOwnPropertyDescriptor(gev.tileset, 'tilesLoaded')
|
||
: null;
|
||
if (gev.tileset) {
|
||
Object.defineProperty(gev.tileset, 'tilesLoaded', { value: true, configurable: true });
|
||
}
|
||
v.scene.sampleHeight = (cartographic) => {
|
||
if (window.__dfSkinM == null) return undefined;
|
||
const lat = Cesium.Math.toDegrees(Number(cartographic?.latitude));
|
||
const lon = Cesium.Math.toDegrees(Number(cartographic?.longitude));
|
||
if (Number.isFinite(lat) && Number.isFinite(lon)) {
|
||
window.__dfSkinSamples.push({ lat, lon, h: window.__dfSkinM });
|
||
if (window.__dfSkinSamples.length > 512) window.__dfSkinSamples.shift();
|
||
}
|
||
return window.__dfSkinM;
|
||
};
|
||
fl.setParams({ models3d: false }); // billboards own the visual (T7 gate open)
|
||
window.__dfFindBB = (id) => {
|
||
let found = null;
|
||
const walk = (coll) => {
|
||
const n = coll.length;
|
||
for (let i = 0; i < n; i++) {
|
||
let p; try { p = coll.get(i); } catch { continue; }
|
||
if (!p) continue;
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
if (p.image !== undefined && p.alignedAxis !== undefined && p.id === id) found = p;
|
||
}
|
||
};
|
||
walk(v.scene.primitives);
|
||
return found;
|
||
};
|
||
window.__dfCarto = (pos) => {
|
||
const c = Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84);
|
||
return { lat: Cesium.Math.toDegrees(c.latitude), lon: Cesium.Math.toDegrees(c.longitude), h: c.height };
|
||
};
|
||
// The fleet pass is rate-limited, so counting raw frames under-waits.
|
||
window.__dfSettle = (ms) => new Promise((res) => {
|
||
let done = false;
|
||
const tick = () => { if (done) return; v.scene.requestRender(); setTimeout(tick, 30); };
|
||
tick();
|
||
setTimeout(() => { done = true; res(); }, ms);
|
||
});
|
||
// Cesium Models in the scene, split by visibility (duck-typed the same
|
||
// way the harness excludes them from height probes).
|
||
//
|
||
// Pass an `icao` to count ONLY that contact's model. Both the fleet loader
|
||
// and the standalone tracked loader stamp `model.id` with the icao (the
|
||
// pick identity click-to-track depends on), and the fleet pass never
|
||
// models the tracked contact — so an id match for the tracked icao is
|
||
// unambiguously ITS model. Counting every model in the scene instead let
|
||
// an unrelated fleet, military, or control model satisfy "the model owns
|
||
// the visual" while the contact under test never rendered at all.
|
||
// `rendering` is the pair the production handoff consults (`ready &&
|
||
// show`): a model registered with Cesium's default `show === true` the
|
||
// instant its glTF resolves is NOT yet the visual.
|
||
window.__dfCountModels = (icao) => {
|
||
let shown = 0;
|
||
let hidden = 0;
|
||
let rendering = 0;
|
||
const walk = (coll) => {
|
||
const n = coll.length;
|
||
for (let i = 0; i < n; i++) {
|
||
let p; try { p = coll.get(i); } catch { continue; }
|
||
if (!p) continue;
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
if (p.activeAnimations !== undefined && p.minimumPixelSize !== undefined) {
|
||
if (icao !== undefined && p.id !== icao) continue;
|
||
if (p.show) {
|
||
shown += 1;
|
||
if (p.ready === true) rendering += 1;
|
||
} else hidden += 1;
|
||
}
|
||
}
|
||
};
|
||
walk(v.scene.primitives);
|
||
return { shown, hidden, rendering };
|
||
};
|
||
// Ellipsoid height of the point a contact's MODEL is actually drawn at.
|
||
// The counters above answer "is a model the visual"; this answers "and is
|
||
// it standing on the ground", which is the half a rendering count cannot
|
||
// see — a model placed at the raw feed altitude renders exactly as
|
||
// enthusiastically as one placed on the tile skin, ~240 m underground.
|
||
window.__dfModelHeight = (icao) => {
|
||
let h = null;
|
||
const walk = (coll) => {
|
||
const n = coll.length;
|
||
for (let i = 0; i < n; i++) {
|
||
let p; try { p = coll.get(i); } catch { continue; }
|
||
if (!p) continue;
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
if (p.activeAnimations !== undefined && p.minimumPixelSize !== undefined
|
||
&& p.id === icao && p.modelMatrix) {
|
||
const m = p.modelMatrix;
|
||
// Cesium's default is the IDENTITY matrix — translation (0,0,0),
|
||
// the Earth's centre, which `Cartographic.fromCartesian` refuses.
|
||
// An admitted-but-never-placed model reads as "no height", which
|
||
// is the truth about it.
|
||
if (m[12] || m[13] || m[14]) {
|
||
const c = Cesium.Cartographic.fromCartesian(
|
||
new Cesium.Cartesian3(m[12], m[13], m[14]), Cesium.Ellipsoid.WGS84,
|
||
);
|
||
h = c ? c.height : null;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
walk(v.scene.primitives);
|
||
return h;
|
||
};
|
||
// Tracked-contact model regime (2026-08-20). The tracked contact's model
|
||
// is NOT governed by the DISPLAY-rail 3D toggle — that toggle owns the
|
||
// FLEET's draw-call budget. The tracked model is default-on and driven by
|
||
// camera distance, entering below TRACKED_MODEL_ENTER_ALT_M and handing
|
||
// back to the billboard only above TRACKED_MODEL_EXIT_ALT_M (the
|
||
// anti-flap band). The CAMERA is therefore what decides whether a model
|
||
// or a billboard is the visual for a tracked contact, which is exactly
|
||
// what the display floor stands aside for — so these pins drive the
|
||
// camera explicitly instead of the toggle.
|
||
//
|
||
// Read the thresholds from the app's own policy module so a later retune
|
||
// of the swap distance moves these pins with it rather than stranding
|
||
// them on stale numbers.
|
||
const reg = await import('/src/data/trackedModelRegime.js');
|
||
window.__dfRegime = {
|
||
enter: reg.TRACKED_MODEL_ENTER_ALT_M,
|
||
exit: reg.TRACKED_MODEL_EXIT_ALT_M,
|
||
};
|
||
// Zooming while following is safe: the tracked-camera framer positions
|
||
// the camera ONCE per selection and is deliberately read-only on normal
|
||
// frames (EntityView is the sole continuous writer, and the only
|
||
// correction is a minimum-range clamp that guards zooming IN, not out),
|
||
// so a programmatic zoom persists instead of being fought every frame.
|
||
window.__dfCamH = () => v.camera.positionCartographic.height;
|
||
// Both walkers bail on a STALLED step as well as on reaching the target:
|
||
// the controller enforces a minimum range when following, so a zoom-in
|
||
// that has bottomed out would otherwise spin the full iteration budget.
|
||
window.__dfZoomAbove = async (targetH) => {
|
||
let last = window.__dfCamH();
|
||
for (let i = 0; i < 80 && window.__dfCamH() <= targetH; i++) {
|
||
v.camera.zoomOut(Math.max(15000, (targetH - window.__dfCamH()) * 0.9));
|
||
await window.__dfSettle(100);
|
||
const now = window.__dfCamH();
|
||
if (i > 0 && Math.abs(now - last) < 1) break;
|
||
last = now;
|
||
}
|
||
return window.__dfCamH();
|
||
};
|
||
window.__dfZoomBelow = async (targetH) => {
|
||
let last = window.__dfCamH();
|
||
for (let i = 0; i < 80 && window.__dfCamH() >= targetH; i++) {
|
||
v.camera.zoomIn(Math.max(250, (window.__dfCamH() - targetH) * 0.5));
|
||
await window.__dfSettle(100);
|
||
const now = window.__dfCamH();
|
||
if (i > 0 && Math.abs(now - last) < 1) break;
|
||
last = now;
|
||
}
|
||
return window.__dfCamH();
|
||
};
|
||
// Wait for THIS contact's tracked model to actually RENDER (loaded AND
|
||
// shown) — ownership means drawing, not existing, and it means the model
|
||
// that carries this icao's pick id, not whichever model happened to be up.
|
||
window.__dfAwaitTrackedModel = async (icao, ms) => {
|
||
const deadline = Date.now() + ms;
|
||
while (Date.now() < deadline && window.__dfCountModels(icao).rendering === 0) {
|
||
await window.__dfSettle(500);
|
||
}
|
||
return window.__dfCountModels(icao);
|
||
};
|
||
// Vite serves an edited source file as `…/groundFloor.js?t=<hmr stamp>`;
|
||
// importing the PLAIN path then hands back a second, unrelated module
|
||
// instance whose cells the app never reads. Offer the URL the app itself
|
||
// loaded first, then the plain path (clean, never-hot-reloaded server).
|
||
const seen = performance.getEntriesByType('resource')
|
||
.map((e) => e.name)
|
||
.filter((n) => /\/src\/data\/groundFloor\.js(\?|$)/.test(n));
|
||
window.__dfCandidates = [...new Set([...seen.reverse(), '/src/data/groundFloor.js'])];
|
||
return { candidates: window.__dfCandidates.length };
|
||
});
|
||
record('display-floor: groundFloor module URL candidates found', dfSetup.candidates > 0,
|
||
JSON.stringify(dfSetup));
|
||
|
||
// Identity probe. Seed a contact's FIX cell BEFORE its first fix arrives,
|
||
// then poll: the grounded surfaceM chain reads cachedGroundFloor, so the
|
||
// sprite renders at the seeded floor if and only if the app shares this
|
||
// module instance. (It must be the FIRST fix — the display runs `now − 30 s`,
|
||
// which for a new contact extrapolates history[0].) This exercises the POLL
|
||
// path only; the clamp under test reads the DISPLAY cell, which stays cold
|
||
// here, so a broken fix cannot fake a pass.
|
||
const dfIdentity = await evalPage(async (lat, lon, seedM) => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const tried = [];
|
||
for (let i = 0; i < window.__dfCandidates.length; i++) {
|
||
const url = window.__dfCandidates[i];
|
||
let gf;
|
||
try { gf = await import(/* @vite-ignore */ url); } catch { tried.push({ url, h: null }); continue; }
|
||
if (typeof gf.reportMeshFloorCell !== 'function') { tried.push({ url, h: null }); continue; }
|
||
gf.setMeshFloorPreferred(true);
|
||
gf._clearMeshFloorCellsForTest();
|
||
gf.reportMeshFloorCell(lat, lon, seedM);
|
||
// A fresh icao per attempt: a contact that already exists keeps the
|
||
// history[0] the warm-up dead-reckon renders, so it could never show a
|
||
// floor seeded afterwards.
|
||
const icao = `aaa09${i}`;
|
||
window.__SYNTH.flights.push({ icao, callsign: `TAXI9${i}`, lon, lat, alt: 150, vel: 12, track: 90, onGround: true });
|
||
await fl.update(v);
|
||
await window.__dfSettle(400);
|
||
const bb = window.__dfFindBB(icao);
|
||
const h = bb ? window.__dfCarto(bb.position).h : null;
|
||
tried.push({ url, h });
|
||
gf._clearMeshFloorCellsForTest();
|
||
if (Number.isFinite(h) && Math.abs(h - seedM) < 5) {
|
||
window.__dfGf = gf; window.__dfIcao = icao;
|
||
return { picked: { url, h }, tried };
|
||
}
|
||
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => f.icao !== icao);
|
||
}
|
||
return { picked: null, tried };
|
||
}, DF_LAT, DF_LON, DF_SEED_M);
|
||
const identityOk = !!dfIdentity.picked;
|
||
record('display-floor: harness reached the app\'s own groundFloor module instance',
|
||
identityOk,
|
||
identityOk
|
||
? `a ${DF_SEED_M} m fix-cell seed put the sprite at ${Number(dfIdentity.picked.h).toFixed(1)} m (${dfIdentity.picked.url})`
|
||
: `no candidate module was the app's: ${JSON.stringify(dfIdentity.tried)}`);
|
||
|
||
if (!identityOk) {
|
||
skip('display-floor: drift clamp (harness could not seed the shared floor cache)',
|
||
'the identity probe above failed — assertions would be meaningless');
|
||
} else {
|
||
const dfDrift = await evalPage(async (lat, lon) => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const gf = window.__dfGf;
|
||
const icao = window.__dfIcao;
|
||
const cell = (x) => Number(x.toFixed(3));
|
||
// Fix cell COLD from here on. The contact keeps the seeded render
|
||
// altitude in its history, and its DISPLAYED position sits one render
|
||
// delay of taxi behind the fix — several cells away.
|
||
gf._clearMeshFloorCellsForTest();
|
||
// Airborne control at the same coordinates, ingested while every cell
|
||
// is cold so its own fix-time clamp finds nothing: it drifts over the
|
||
// same cells and must NOT be lifted.
|
||
const airIcao = 'aaa095';
|
||
window.__SYNTH.flights.push({ icao: airIcao, callsign: 'AIR95', lon, lat, alt: 150, vel: 12, track: 90 });
|
||
await new Promise((r) => setTimeout(r, 1200)); // fix epochs are second-resolution
|
||
await fl.update(v);
|
||
await window.__dfSettle(400);
|
||
|
||
const bb = window.__dfFindBB(icao);
|
||
if (!bb) return { error: `${icao} billboard missing` };
|
||
const d0 = window.__dfCarto(bb.position);
|
||
const driftedCells = cell(d0.lat) !== cell(lat) || cell(d0.lon) !== cell(lon);
|
||
const driftM = Math.hypot((d0.lat - lat) * 111320,
|
||
(d0.lon - lon) * 111320 * Math.cos(lat * Math.PI / 180));
|
||
// Plant a floor 30 m above where the sprite is DISPLAYED, across the
|
||
// 3×3 cell block it can move within during the assertion window.
|
||
const seeded = d0.h + 30;
|
||
for (let dy = -1; dy <= 1; dy++) {
|
||
for (let dx = -1; dx <= 1; dx++) {
|
||
gf.reportMeshFloorCell(cell(d0.lat) + dy * 0.001, cell(d0.lon) + dx * 0.001, seeded);
|
||
}
|
||
}
|
||
// The fleet clamp runs on its throttled render pass. Under a loaded QA
|
||
// machine a fixed 1.2 s sleep can sample just before that pass, so wait
|
||
// for the observable clamp with a hard deadline instead of weakening
|
||
// the assertion or assuming one scheduler cadence.
|
||
const clampDeadline = Date.now() + 5000;
|
||
let bb2 = null;
|
||
let d1 = null;
|
||
do {
|
||
await window.__dfSettle(250);
|
||
bb2 = window.__dfFindBB(icao);
|
||
d1 = bb2 ? window.__dfCarto(bb2.position) : null;
|
||
} while (Date.now() < clampDeadline && (!d1 || d1.h < seeded + 1));
|
||
const air = window.__dfFindBB(airIcao);
|
||
const a1 = air ? window.__dfCarto(air.position) : null;
|
||
return {
|
||
driftedCells, driftM, seeded,
|
||
beforeH: d0.h,
|
||
afterH: d1 ? d1.h : null,
|
||
floorUnderSprite: d1 ? gf.cachedGroundFloor(d1.lat, d1.lon) : null,
|
||
fixCellMesh: gf.cachedMeshFloor(lat, lon),
|
||
fixCellFloor: gf.cachedGroundFloor(lat, lon),
|
||
airborneH: a1 ? a1.h : null,
|
||
airborneFound: !!air,
|
||
};
|
||
}, DF_LAT, DF_LON);
|
||
|
||
record('display-floor: the displayed position really has drifted off the fix cell',
|
||
dfDrift.driftedCells === true,
|
||
dfDrift.error || `displayed ${Number(dfDrift.driftM).toFixed(0)} m from the fix (render delay × taxi speed)`);
|
||
|
||
const clampOk = Number.isFinite(dfDrift.afterH) && Number.isFinite(dfDrift.floorUnderSprite)
|
||
&& dfDrift.afterH >= dfDrift.floorUnderSprite + DISPLAY_FLOOR_LIFT_M - 0.5;
|
||
record('display-floor: a grounded sprite never renders below the cell it drifted onto',
|
||
clampOk,
|
||
dfDrift.error || `${Number(dfDrift.beforeH).toFixed(1)} m → ${Number(dfDrift.afterH).toFixed(1)} m against a ${Number(dfDrift.floorUnderSprite).toFixed(1)} m floor (needs >= floor + ${DISPLAY_FLOOR_LIFT_M} m)`);
|
||
|
||
// Discriminator: the poll path had nothing to read, so the lift can only
|
||
// have come from the display path.
|
||
const fixCellCold = dfDrift.fixCellMesh == null
|
||
&& (dfDrift.fixCellFloor == null || dfDrift.fixCellFloor < dfDrift.seeded - 10);
|
||
record('display-floor: the lift came from the display path (fix cell still cold)',
|
||
fixCellCold,
|
||
`fix cell mesh=${dfDrift.fixCellMesh} floor=${dfDrift.fixCellFloor} vs seeded ${Number(dfDrift.seeded).toFixed(1)} m`);
|
||
|
||
// Grounded-only: an AIRBORNE contact drifting over the same seeded cells
|
||
// keeps its reported altitude — the fix-time clamp owns airborne heights.
|
||
record('display-floor: an AIRBORNE contact over the same cells is not lifted',
|
||
dfDrift.airborneFound === true && Number.isFinite(dfDrift.airborneH)
|
||
&& dfDrift.airborneH < dfDrift.seeded - 10,
|
||
`airborne renders at ${Number(dfDrift.airborneH).toFixed(1)} m (seeded floor ${Number(dfDrift.seeded).toFixed(1)} m)`);
|
||
|
||
// ---- Corridor wiring, WITHOUT pre-seeding ----------------------------
|
||
// The scenario above plants the floor by hand, so it still passes if the
|
||
// corridor collection is deleted, aimed at the wrong end, or starved.
|
||
// This one seeds NOTHING: the contact's DISPLAY cell can only get a floor
|
||
// if the poll actually collects the ground it is rendering over and warms
|
||
// it through the real DEM resolve. A control cell the same distance away,
|
||
// which no corridor covers, must stay cold.
|
||
const dfCorridor = await evalPage(async (lat, lon) => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const gf = window.__dfGf;
|
||
const cell = (x) => Number(x.toFixed(3));
|
||
gf._clearMeshFloorCellsForTest(); // DEM only — sampleHeight is stubbed off
|
||
const key = (c) => `${c.lat},${c.lon}`;
|
||
// Control: the same distance from the fix, perpendicular to the track,
|
||
// so it is equally "near the airport" but on no contact's path.
|
||
const controlCell = { lat: cell(lat + 0.02), lon: cell(lon) };
|
||
// ~250 m PAST the fix along the track (090). The display sits behind the
|
||
// fix and is extrapolating toward it, so this ground is only reachable
|
||
// if the corridor follows the direction of travel instead of stopping
|
||
// at the fix — the coasting-away defect, observable without waiting for
|
||
// a real coast.
|
||
const aheadCell = {
|
||
lat: cell(lat),
|
||
lon: cell(lon + 250 / (111320 * Math.cos(lat * Math.PI / 180))),
|
||
};
|
||
// Snapshot the whole neighbourhood BEFORE this contact exists. The
|
||
// display cell's identity is only knowable after a second fix has aged
|
||
// the display head off the fix — and by then the corridor has already
|
||
// run and (correctly) warmed it, so a read at that point can never show
|
||
// it cold. `_clearMeshFloorCellsForTest()` does not help: the DEM half
|
||
// of `cachedGroundFloor` lives in terrainHeights' own cache. So the
|
||
// cold state is captured up front and looked up afterwards. A display
|
||
// cell outside this grid reports `out-of-snapshot`, which fails the
|
||
// assertion rather than passing it quietly.
|
||
const coldBefore = new Map();
|
||
for (let dy = -3; dy <= 3; dy++) {
|
||
for (let dx = -8; dx <= 8; dx++) {
|
||
const c = { lat: cell(lat + dy * 0.001), lon: cell(lon + dx * 0.001) };
|
||
coldBefore.set(key(c), gf.cachedGroundFloor(c.lat, c.lon));
|
||
}
|
||
}
|
||
const startCold = {
|
||
control: gf.cachedGroundFloor(controlCell.lat, controlCell.lon),
|
||
};
|
||
// Baro 0 puts it at the geoid (~-27 m here), ~150 m under the real
|
||
// Austin surface, so any lift is unambiguous.
|
||
window.__SYNTH.flights.push({ icao: 'aaa097', callsign: 'COAST97', lon, lat, alt: 0, vel: 12, track: 90, onGround: true });
|
||
await fl.update(v);
|
||
await window.__dfSettle(600);
|
||
// AGE A SECOND FIX before sampling. With a single fix the display has
|
||
// no path to run and renders AT the fix, which would make the whole
|
||
// scenario vacuous — ordinary fix-cell warming could then satisfy the
|
||
// display-cell assertion below. The second fix (epochs are
|
||
// second-resolution) gives the `now − 30 s` display head a segment to
|
||
// extrapolate back along, putting it ~360 m of taxi behind the fix.
|
||
await new Promise((r) => setTimeout(r, 1200));
|
||
await fl.update(v);
|
||
await window.__dfSettle(600);
|
||
const bb0 = window.__dfFindBB('aaa097');
|
||
if (!bb0) return { error: 'aaa097 billboard missing' };
|
||
const d0 = window.__dfCarto(bb0.position);
|
||
const displayCell = { lat: cell(d0.lat), lon: cell(d0.lon) };
|
||
const fixCell = { lat: cell(lat), lon: cell(lon) };
|
||
startCold.display = coldBefore.has(key(displayCell))
|
||
? coldBefore.get(key(displayCell))
|
||
: 'out-of-snapshot';
|
||
// Give the fire-and-forget DEM resolve time to land, then let a couple
|
||
// more polls run the corridor again.
|
||
for (let i = 0; i < 3; i++) {
|
||
await new Promise((r) => setTimeout(r, 1400));
|
||
await fl.update(v);
|
||
await window.__dfSettle(600);
|
||
}
|
||
const bb1 = window.__dfFindBB('aaa097');
|
||
const d1 = bb1 ? window.__dfCarto(bb1.position) : null;
|
||
return {
|
||
startCold,
|
||
displayCell,
|
||
fixCell,
|
||
sameCellAsFix: displayCell.lat === fixCell.lat && displayCell.lon === fixCell.lon,
|
||
displayFloor: gf.cachedGroundFloor(displayCell.lat, displayCell.lon),
|
||
controlFloor: gf.cachedGroundFloor(controlCell.lat, controlCell.lon),
|
||
aheadCell,
|
||
aheadFloor: gf.cachedGroundFloor(aheadCell.lat, aheadCell.lon),
|
||
spriteH: d1 ? d1.h : null,
|
||
spriteFloor: d1 ? gf.cachedGroundFloor(d1.lat, d1.lon) : null,
|
||
beforeH: d0.h,
|
||
};
|
||
}, 30.3000, -97.8000);
|
||
|
||
// The whole scenario is only meaningful if the cell being warmed is one
|
||
// the ordinary fix-cell batch could NOT have produced. Two conditions
|
||
// make that true, and both are asserted here rather than assumed: the
|
||
// display head really left the fix cell (the second fix aged it there),
|
||
// and that cell was COLD when the measurement started.
|
||
record('display-floor/corridor: the display cell is not the fix cell, and started cold',
|
||
dfCorridor.sameCellAsFix === false && dfCorridor.startCold?.display == null,
|
||
dfCorridor.error
|
||
|| `display ${JSON.stringify(dfCorridor.displayCell)} vs fix ${JSON.stringify(dfCorridor.fixCell)}, started at ${dfCorridor.startCold?.display} — the fix-cell collection cannot reach the display cell`);
|
||
|
||
record('display-floor/corridor: polling warms the ground the contact is DISPLAYED over',
|
||
Number.isFinite(dfCorridor.displayFloor),
|
||
dfCorridor.error
|
||
|| `display-cell floor = ${dfCorridor.displayFloor} (nothing was pre-seeded — only corridor collection can produce this)`);
|
||
|
||
// The beyond-fix probe carries its own arithmetic precondition: a cell
|
||
// that collapsed onto the fix cell would be warmed by the fix batch and
|
||
// prove nothing about the direction of travel.
|
||
const distinctAheadCell = dfCorridor.aheadCell?.lat !== dfCorridor.fixCell?.lat
|
||
|| dfCorridor.aheadCell?.lon !== dfCorridor.fixCell?.lon;
|
||
record('display-floor/corridor: the corridor reaches PAST the fix, along the direction of travel',
|
||
distinctAheadCell && Number.isFinite(dfCorridor.aheadFloor),
|
||
dfCorridor.error
|
||
|| `cell ~250 m beyond the fix (${JSON.stringify(dfCorridor.aheadCell)}, distinct from fix ${JSON.stringify(dfCorridor.fixCell)}: ${distinctAheadCell}) floor = ${dfCorridor.aheadFloor} — a fix-aimed corridor never offers it`);
|
||
|
||
record('display-floor/corridor: an off-path control cell stays cold (not ambient warming)',
|
||
dfCorridor.controlFloor == null,
|
||
`control cell floor = ${dfCorridor.controlFloor}`);
|
||
|
||
record('display-floor/corridor: the sprite rides the corridor-warmed floor',
|
||
Number.isFinite(dfCorridor.spriteH) && Number.isFinite(dfCorridor.spriteFloor)
|
||
&& dfCorridor.spriteH >= dfCorridor.spriteFloor + DISPLAY_FLOOR_LIFT_M - 0.5,
|
||
dfCorridor.error
|
||
|| `${Number(dfCorridor.beforeH).toFixed(1)} m → ${Number(dfCorridor.spriteH).toFixed(1)} m on a ${Number(dfCorridor.spriteFloor).toFixed(1)} m floor`);
|
||
|
||
// ---- Tracked contact gets the same floor ----------------------------
|
||
// The fleet pass skips the tracked icao and the tracked entity draws from
|
||
// _trackedDisplayPosition, so selecting a correctly floored grounded
|
||
// billboard used to swap it for an unfloored cyan target under the mesh.
|
||
const dfTracked = await evalPage(async () => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const Cesium = await import('/node_modules/cesium/Build/Cesium/index.js');
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const gf = window.__dfGf;
|
||
const cell = (x) => Number(x.toFixed(3));
|
||
const bbBefore = window.__dfFindBB('aaa097');
|
||
if (!bbBefore) return { error: 'aaa097 billboard missing' };
|
||
const d0 = window.__dfCarto(bbBefore.position);
|
||
// Plant a floor well ABOVE where it currently renders, across the block
|
||
// it can move within, so an UNFLOORED tracked entity is unmistakable.
|
||
const seeded = d0.h + 40;
|
||
for (let dy = -1; dy <= 1; dy++) {
|
||
for (let dx = -1; dx <= 1; dx++) {
|
||
gf.reportMeshFloorCell(cell(d0.lat) + dy * 0.001, cell(d0.lon) + dx * 0.001, seeded);
|
||
}
|
||
}
|
||
const fleetClampDeadline = Date.now() + 5000;
|
||
let bbLifted = null;
|
||
let fleetH = null;
|
||
do {
|
||
await window.__dfSettle(250);
|
||
bbLifted = window.__dfFindBB('aaa097');
|
||
fleetH = bbLifted ? window.__dfCarto(bbLifted.position).h : null;
|
||
} while (Date.now() < fleetClampDeadline
|
||
&& (!Number.isFinite(fleetH) || fleetH < seeded + 1));
|
||
if (!fl.trackById('aaa097')) return { error: 'trackById(aaa097) failed' };
|
||
await window.__dfSettle(1500);
|
||
// Read the tracked entity's rendered height plus the floor under it and
|
||
// the sensor value the data APIs report, at whatever regime the camera
|
||
// is currently in.
|
||
const readTracked = () => {
|
||
const pos = v.trackedEntity?.position?.getValue(Cesium.JulianDate.now());
|
||
if (!pos) return null;
|
||
const c = window.__dfCarto(pos);
|
||
const info = fl.getTrackedInfo();
|
||
const subject = fl.getTrackedSubject();
|
||
return {
|
||
h: c.h,
|
||
floor: gf.cachedGroundFloor(c.lat, c.lon),
|
||
reportedAltM: info?.altitudeM,
|
||
subjectH: subject?.position ? window.__dfCarto(subject.position).h : null,
|
||
camH: window.__dfCamH(),
|
||
// THIS contact's model, not any model in the scene: the ownership
|
||
// side of the assertion has to be about aaa097 or it proves nothing
|
||
// about who drew aaa097.
|
||
models: window.__dfCountModels('aaa097'),
|
||
};
|
||
};
|
||
|
||
// --- Regime IN, COLD ground: the model is WITHHELD ------------------
|
||
// trackById re-frames the camera to the close follow range, so the
|
||
// tracked model regime is already active. `__dfSkinM` is still null, so
|
||
// groundSnap has never resolved for this contact: there is no evidence
|
||
// of where the ground is, and a depth-tested glTF placed at the feed
|
||
// altitude (baro 0 → ~-27 m at Austin) renders ~240 m INSIDE the mesh.
|
||
// The only honest answer is not to draw it — which also means the
|
||
// billboard is still the visual and must still be FLOORED.
|
||
await window.__dfSettle(2500);
|
||
const coldModels = window.__dfCountModels('aaa097');
|
||
// `shown` as well as `rendering`: a model parked at scale 0 with
|
||
// show=true would satisfy "nothing renders" while still being a shown
|
||
// primitive claiming the visual, which is the shape this rejects.
|
||
|
||
const coldTracked = readTracked();
|
||
const coldModelH = window.__dfModelHeight('aaa097');
|
||
|
||
// --- Regime IN, WARM ground: the model takes over, ON the floor ------
|
||
// Open a deterministic tile skin AT the planted floor. groundSnap
|
||
// resolves, the model is placed on it (plus the class belly offset) and
|
||
// takes the visual; the display clamp must then STAND ASIDE (T7),
|
||
// leaving the ENTITY at its raw dead-reckon while the MODEL carries the
|
||
// ground truth. Both halves are asserted — a rendering count alone
|
||
// cannot tell a model standing on the apron from one buried under it.
|
||
//
|
||
// The open skin is HANDED to the retained-model scenario below, which
|
||
// closes it once its own model is up. Every other way out of this block
|
||
// — an early error return, a throw — has to close it here instead: a
|
||
// skin left open lets a live sample latch into the floor cells the
|
||
// planted seeds below are about to own, and the damage would then
|
||
// surface as a failure in an unrelated scenario.
|
||
window.__dfSkinM = seeded;
|
||
let handOffSkin = false;
|
||
try {
|
||
const modelsIn = await window.__dfAwaitTrackedModel('aaa097', 20000);
|
||
const warmModelH = window.__dfModelHeight('aaa097');
|
||
// Scene-wide count, used only to tell two very different states apart:
|
||
// NO model rendered at all (this browser has no usable GLB backend — a
|
||
// legitimate skip) versus models rendered but none of them carried
|
||
// aaa097's pick id (a real defect, and the case the old scene-wide
|
||
// probe reported as a pass).
|
||
const anyModelsIn = window.__dfCountModels();
|
||
await window.__dfSettle(600);
|
||
const zoomedIn = readTracked();
|
||
if (!zoomedIn) return { error: 'tracked entity has no position (zoomed in)' };
|
||
|
||
// --- Regime OUT: camera above the exit ceiling ----------------------
|
||
// Climb past the anti-flap band and the model hands the visual back to
|
||
// the billboard — which MUST be floored, or a tracked grounded contact
|
||
// renders buried at any distance the operator pulls out to. This is the
|
||
// coverage the toggle used to reach; the zoom regime reaches it now.
|
||
const achievedOut = await window.__dfZoomAbove(window.__dfRegime.exit + 5000);
|
||
await window.__dfSettle(1500);
|
||
const zoomedOut = readTracked();
|
||
if (!zoomedOut) return { error: 'tracked entity has no position (zoomed out)' };
|
||
|
||
const out = {
|
||
rawH: d0.h,
|
||
seeded,
|
||
fleetH,
|
||
enter: window.__dfRegime.enter,
|
||
exit: window.__dfRegime.exit,
|
||
modelsRenderingIn: modelsIn.rendering,
|
||
anyModelsRenderingIn: anyModelsIn.rendering,
|
||
achievedOut,
|
||
coldModelsRendering: coldModels.rendering,
|
||
coldModelsShown: coldModels.shown,
|
||
coldModelH,
|
||
coldEntityH: coldTracked ? coldTracked.h : null,
|
||
coldFloor: coldTracked ? coldTracked.floor : null,
|
||
warmModelH,
|
||
zoomedIn,
|
||
zoomedOut,
|
||
};
|
||
fl.stopTracking();
|
||
await window.__dfSettle(400);
|
||
handOffSkin = true;
|
||
return out;
|
||
} finally {
|
||
if (!handOffSkin) window.__dfSkinM = null;
|
||
}
|
||
});
|
||
|
||
record('display-floor: the fleet billboard lifts onto the planted floor (control for the tracked check)',
|
||
!dfTracked.error && Number.isFinite(dfTracked.fleetH)
|
||
&& dfTracked.fleetH >= dfTracked.seeded + DISPLAY_FLOOR_LIFT_M - 0.5,
|
||
dfTracked.error
|
||
|| `billboard ${Number(dfTracked.rawH).toFixed(1)} m → ${Number(dfTracked.fleetH).toFixed(1)} m (floor ${Number(dfTracked.seeded).toFixed(1)} m)`);
|
||
|
||
// The tracked contact is floored exactly when the BILLBOARD is what the
|
||
// operator sees, and stands aside exactly when the MODEL is. "Exactly
|
||
// when" needs BOTH halves, so both are asserted: floored on the way out,
|
||
// NOT floored on the way in. Since the tracked model became zoom-driven
|
||
// (2026-08-20), the camera is what picks between the two, so this pin
|
||
// drives both sides of the hysteresis band in one scenario.
|
||
//
|
||
// The negative half is what a height read alone cannot establish —
|
||
// groundSnap can land at the same number as the clamp — so it is paired
|
||
// with an OWNERSHIP probe rather than inferred from height. The probe is
|
||
// aaa097's OWN model (exact pick id) in the `ready && show` state
|
||
// `_modelOwnsVisual` consults: rendering while zoomed in, not rendering
|
||
// while zoomed out. That distinguishes the two sources by WHO DREW IT,
|
||
// which is the actual contract:
|
||
// - IN: aaa097's model renders ⇒ `_floorGroundedDisplayPosition` returns
|
||
// early, so the tracked ENTITY sits at its raw dead-reckon (baro 0,
|
||
// ~150 m under the planted floor) while groundSnap positions the model.
|
||
// - OUT: no model of aaa097 renders ⇒ the billboard is the visual and
|
||
// MUST be lifted, or a tracked grounded contact renders buried at
|
||
// whatever distance the operator has pulled out to.
|
||
const dfIn = dfTracked.zoomedIn || {};
|
||
const dfOut = dfTracked.zoomedOut || {};
|
||
const dfOutFloored = Number.isFinite(dfOut.h) && Number.isFinite(dfOut.floor)
|
||
&& dfOut.h >= dfOut.floor + DISPLAY_FLOOR_LIFT_M - 0.5;
|
||
const dfInStandsAside = Number.isFinite(dfIn.h) && Number.isFinite(dfIn.floor)
|
||
&& dfIn.h < dfIn.floor + DISPLAY_FLOOR_LIFT_M - 0.5;
|
||
const dfInModelOwns = dfIn.models?.rendering > 0;
|
||
const dfOutBillboardOwns = dfOut.models?.rendering === 0;
|
||
const dfRegimeName = 'display-floor/regime: the TRACKED grounded contact is floored exactly when the billboard is the visual';
|
||
|
||
// COLD ground, camera IN: the gate. A grounded model with no resolved
|
||
// ground is not drawn at all, and the billboard it did not replace is
|
||
// still floored. Before this pin the same run reported "1 aaa097 model(s)
|
||
// rendering, entity left at -26.8 m under a 215.1 m floor" as a PASS —
|
||
// the model was rendering 242 m under the apron and nothing said so.
|
||
const dfColdFloored = Number.isFinite(dfTracked.coldEntityH)
|
||
&& Number.isFinite(dfTracked.coldFloor)
|
||
&& dfTracked.coldEntityH >= dfTracked.coldFloor + DISPLAY_FLOOR_LIFT_M - 0.5;
|
||
record('display-floor/gate: with NO resolved ground the grounded model is withheld and the billboard is floored',
|
||
!dfTracked.error && dfTracked.coldModelsRendering === 0
|
||
&& dfTracked.coldModelsShown === 0 && dfColdFloored,
|
||
dfTracked.error
|
||
|| `cold skin: ${dfTracked.coldModelsRendering} aaa097 model(s) rendering / ${dfTracked.coldModelsShown} shown (placed height ${dfTracked.coldModelH == null ? 'none — never placed' : Number(dfTracked.coldModelH).toFixed(1) + ' m'}), billboard at ${Number(dfTracked.coldEntityH).toFixed(1)} m on a ${Number(dfTracked.coldFloor).toFixed(1)} m floor`);
|
||
|
||
// WARM ground, camera IN: the placement. The model is not merely visible,
|
||
// it is standing ON the floor — within one class belly offset above it,
|
||
// never under it. MAX_BELLY_M clears the largest offset any class adds
|
||
// (widebody b789, 7.81 m in CLASS_MODEL_REAL); the floor itself is the
|
||
// hard lower bound, and a model placed at the raw feed altitude is ~240 m
|
||
// under it.
|
||
const MAX_BELLY_M = 12;
|
||
const dfWarmOnFloor = Number.isFinite(dfTracked.warmModelH)
|
||
&& dfTracked.warmModelH >= dfTracked.seeded
|
||
&& dfTracked.warmModelH <= dfTracked.seeded + MAX_BELLY_M;
|
||
record('display-floor/placement: a resolved grounded model stands ON the floor, not under it and not hidden',
|
||
!dfTracked.error && dfTracked.modelsRenderingIn > 0 && dfWarmOnFloor,
|
||
dfTracked.error
|
||
|| `warm skin: ${dfTracked.modelsRenderingIn} aaa097 model(s) rendering at ${Number(dfTracked.warmModelH).toFixed(1)} m on a ${Number(dfTracked.seeded).toFixed(1)} m floor (belly headroom ${MAX_BELLY_M} m; raw feed altitude was ${Number(dfTracked.rawH).toFixed(1)} m)`);
|
||
|
||
// Skip only when NO model rendered anywhere — that is a browser without a
|
||
// usable GLB backend. Models rendering while none of them is aaa097's is
|
||
// a defect, and falls through to the record below so it reports RED.
|
||
if (!dfTracked.error && !dfTracked.modelsRenderingIn && !dfTracked.anyModelsRenderingIn) {
|
||
skip(dfRegimeName,
|
||
'no tracked model rendered in this browser — the zoomed-in half has no model to own the visual');
|
||
} else {
|
||
record(dfRegimeName,
|
||
!dfTracked.error
|
||
&& dfInModelOwns && dfInStandsAside
|
||
&& dfOutBillboardOwns && dfOutFloored
|
||
&& Number.isFinite(dfIn.camH) && dfIn.camH < dfTracked.enter
|
||
&& Number.isFinite(dfOut.camH) && dfOut.camH > dfTracked.exit,
|
||
dfTracked.error
|
||
|| `IN @ ${(dfIn.camH / 1000).toFixed(0)} km (< ${(dfTracked.enter / 1000).toFixed(0)} km enter): ${dfIn.models?.rendering} aaa097 model(s) rendering, entity left at ${Number(dfIn.h).toFixed(1)} m under a ${Number(dfIn.floor).toFixed(1)} m floor (groundSnap owns it); `
|
||
+ `OUT @ ${(dfOut.camH / 1000).toFixed(0)} km (> ${(dfTracked.exit / 1000).toFixed(1)} km exit): ${dfOut.models?.rendering} aaa097 model(s) rendering, billboard owns it again, ${Number(dfIn.h).toFixed(1)} m → ${Number(dfOut.h).toFixed(1)} m on a ${Number(dfOut.floor).toFixed(1)} m floor`);
|
||
}
|
||
|
||
// The visual/data split is INTENTIONAL: pixels are floored, measurements
|
||
// are not. Pinned so a later "floor everything" pass has to argue with it.
|
||
// Asserted in BOTH regimes — who owns the visual must never change what a
|
||
// query, the cockpit altimeter, or a proximity count reports.
|
||
record('display-floor/regime: data APIs report sensor truth in BOTH regimes, floored or not',
|
||
!dfTracked.error
|
||
&& Number.isFinite(dfIn.reportedAltM) && Math.abs(dfIn.reportedAltM) < 1
|
||
&& Number.isFinite(dfOut.reportedAltM) && Math.abs(dfOut.reportedAltM) < 1
|
||
&& Math.abs(dfOut.reportedAltM - dfOut.h) > 100,
|
||
dfTracked.error
|
||
|| `getTrackedInfo().altitudeM = ${dfIn.reportedAltM} zoomed in / ${dfOut.reportedAltM} zoomed out (feed baro), while the floored visual reads ${Number(dfOut.h).toFixed(1)} m — the split is deliberate`);
|
||
|
||
// ---- F4: a RETAINED-but-hidden tracked model must not suppress the floor
|
||
// Climbing past the tracked model's EXIT ceiling leaves `_trackedModel`
|
||
// alive with show=false (the driver hides it, it is not destroyed), and
|
||
// the tracked billboard becomes the visual again. Gating the clamp on
|
||
// model EXISTENCE put the burial straight back in exactly that state.
|
||
//
|
||
// This scenario also pins the toggle contract itself. The DISPLAY-rail
|
||
// "3D" toggle stays OFF for the whole block on purpose: it governs the
|
||
// FLEET's draw-call budget only, and the tracked contact must still take
|
||
// its model on zoom without the operator arming anything.
|
||
const dfRetained = await evalPage(async () => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const Cesium = await import('/node_modules/cesium/Build/Cesium/index.js');
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const gf = window.__dfGf;
|
||
const cell = (x) => Number(x.toFixed(3));
|
||
// This scenario INHERITS the open deterministic skin from the block
|
||
// above and closes it partway through, once its own model is up. The
|
||
// close is repeated in a finally because the skip return between here
|
||
// and there would otherwise hand an open skin to the seeded scenarios
|
||
// that follow; closing an already-closed skin is inert.
|
||
try {
|
||
// Toggle OFF throughout — the tracked model must not need it.
|
||
fl.setParams({ models3d: false });
|
||
const toggleOff = fl.getParams().models3d === false;
|
||
if (!fl.trackById('aaa097')) return { error: 'trackById(aaa097) failed' };
|
||
await window.__dfSettle(600);
|
||
// Below the ENTER ceiling (trackById re-frames to the close follow
|
||
// range): the tracked model arrives on zoom alone, toggle still off.
|
||
const belowH = await window.__dfZoomBelow(window.__dfRegime.enter - 20000);
|
||
const withModel = await window.__dfAwaitTrackedModel('aaa097', 20000);
|
||
// Same split as the regime scenario: a browser with no usable GLB
|
||
// backend skips; models rendering under someone else's pick id must
|
||
// reach the assertions and fail there.
|
||
if (!withModel.rendering && !window.__dfCountModels().rendering) {
|
||
fl.stopTracking();
|
||
return { skipped: 'no tracked model rendered in this browser' };
|
||
}
|
||
// While the model IS the visual the clamp must stand aside (T7).
|
||
const entWith = v.trackedEntity?.position?.getValue(Cesium.JulianDate.now());
|
||
const withModelH = entWith ? window.__dfCarto(entWith).h : null;
|
||
// Close the deterministic skin again for the rest of the group. The
|
||
// model is up on a snap that is already cached, so it stays up until the
|
||
// regime hides it; what this prevents is a live sample latching into the
|
||
// floor cells that the planted seeds below are about to own.
|
||
window.__dfSkinM = null;
|
||
// Now climb past the EXIT ceiling: the model object is RETAINED but
|
||
// hidden, and the billboard is the visual again.
|
||
const aboveH = await window.__dfZoomAbove(window.__dfRegime.exit + 5000);
|
||
await window.__dfSettle(1500);
|
||
// aaa097's own model only: "retained but hidden" is a claim about THIS
|
||
// contact's primitive, and an unrelated model in the scene must not be
|
||
// able to answer it either way.
|
||
const afterOut = window.__dfCountModels('aaa097');
|
||
const ent = v.trackedEntity?.position?.getValue(Cesium.JulianDate.now());
|
||
const c = ent ? window.__dfCarto(ent) : null;
|
||
// Plant a floor above wherever it now renders so "floored" is decisive.
|
||
// Mesh cells latch ONE-SHOT, so the earlier scenario's seed would win
|
||
// this block silently — clear first, then plant.
|
||
gf._clearMeshFloorCellsForTest();
|
||
const seeded = (c ? c.h : 0) + 35;
|
||
if (c) {
|
||
for (let dy = -1; dy <= 1; dy++) {
|
||
for (let dx = -1; dx <= 1; dx++) {
|
||
gf.reportMeshFloorCell(cell(c.lat) + dy * 0.001, cell(c.lon) + dx * 0.001, seeded);
|
||
}
|
||
}
|
||
}
|
||
// The viewer is request-render driven. Directly seeding the test seam
|
||
// does not itself schedule a frame, so explicitly request one before
|
||
// reading the CallbackProperty again. Production floor warming already
|
||
// runs inside the app's render/update lifecycle; this keeps the harness
|
||
// from mistaking a cached same-frame position for a failed clamp.
|
||
v.scene.requestRender();
|
||
await window.__dfSettle(900);
|
||
const ent2 = v.trackedEntity?.position?.getValue(Cesium.JulianDate.now());
|
||
const c2 = ent2 ? window.__dfCarto(ent2) : null;
|
||
const out = {
|
||
toggleOff,
|
||
enter: window.__dfRegime.enter,
|
||
exit: window.__dfRegime.exit,
|
||
belowH,
|
||
aboveH,
|
||
modelsRenderingBelow: withModel.rendering,
|
||
withModelH,
|
||
hiddenModelsAbove: afterOut.hidden,
|
||
shownModelsAbove: afterOut.shown,
|
||
beforeSeedH: c ? c.h : null,
|
||
seeded,
|
||
afterSeedH: c2 ? c2.h : null,
|
||
};
|
||
// Hand the camera back to the CLOSE follow range before releasing. The
|
||
// LOADING scenario below needs BOTH an active tracked regime and a
|
||
// fleet model for this contact, and the fleet only admits models within
|
||
// MODEL_PROX_ADD_M of the camera — parking just inside the enter
|
||
// ceiling satisfies the regime but sits outside that add radius, which
|
||
// silently starved the fleet half into a skip.
|
||
await window.__dfZoomBelow(belowH + 500);
|
||
fl.stopTracking();
|
||
await window.__dfSettle(300);
|
||
return out;
|
||
} finally {
|
||
window.__dfSkinM = null;
|
||
}
|
||
});
|
||
|
||
if (dfRetained.skipped) {
|
||
skip('display-floor: retained-but-hidden tracked model still floors the billboard',
|
||
dfRetained.skipped);
|
||
} else {
|
||
record('display-floor/T7: while the tracked MODEL is the visual, the clamp stands aside',
|
||
!dfRetained.error && dfRetained.modelsRenderingBelow > 0,
|
||
dfRetained.error
|
||
|| `${dfRetained.modelsRenderingBelow} aaa097 model(s) rendering at ${(dfRetained.belowH / 1000).toFixed(0)} km with the 3D toggle OFF; entity at ${Number(dfRetained.withModelH).toFixed(1)} m (ground-snap owns it)`);
|
||
|
||
// The toggle contract after auto-3D (2026-08-20): the DISPLAY-rail "3D"
|
||
// switch owns the FLEET only. The tracked contact's model is default-on
|
||
// and decided purely by camera distance, so with the toggle OFF it is
|
||
// SHOWN below the enter ceiling and handed back above the exit ceiling —
|
||
// and handed back by HIDING, not destroying, which is the retained state
|
||
// the floor pin below depends on.
|
||
record('display-floor/regime: the 3D toggle governs the FLEET only — the tracked model follows the zoom',
|
||
!dfRetained.error && dfRetained.toggleOff === true
|
||
&& dfRetained.modelsRenderingBelow > 0
|
||
&& dfRetained.belowH < dfRetained.enter
|
||
&& dfRetained.shownModelsAbove === 0
|
||
&& dfRetained.hiddenModelsAbove > 0
|
||
&& dfRetained.aboveH > dfRetained.exit,
|
||
dfRetained.error
|
||
|| `toggle OFF throughout: at ${(dfRetained.belowH / 1000).toFixed(0)} km (< ${(dfRetained.enter / 1000).toFixed(0)} km enter) ${dfRetained.modelsRenderingBelow} aaa097 model(s) rendering; at ${(dfRetained.aboveH / 1000).toFixed(0)} km (> ${(dfRetained.exit / 1000).toFixed(1)} km exit) ${dfRetained.shownModelsAbove} shown / ${dfRetained.hiddenModelsAbove} retained-hidden`);
|
||
|
||
record('display-floor: a retained-but-hidden tracked model does NOT suppress the floor',
|
||
Number.isFinite(dfRetained.afterSeedH)
|
||
&& dfRetained.afterSeedH >= dfRetained.seeded + DISPLAY_FLOOR_LIFT_M - 0.5,
|
||
`${Number(dfRetained.beforeSeedH).toFixed(1)} m → ${Number(dfRetained.afterSeedH).toFixed(1)} m on a ${Number(dfRetained.seeded).toFixed(1)} m floor (${dfRetained.hiddenModelsAbove} model object(s) still retained, hidden by the zoom regime)`);
|
||
}
|
||
|
||
// ---- F3: the LOADING window is billboard-owned, not model-owned ------
|
||
// A fleet model is registered with Cesium's default show=true the instant
|
||
// its glTF resolves, but the handoff waits for `ready`; and a tracked
|
||
// model is null for the whole load even though the regime is already
|
||
// active. Both windows leave the BILLBOARD as the visual, so both must
|
||
// stay floored — ownership means actually rendering.
|
||
const dfLoading = await evalPage(async () => {
|
||
const v = window.__godsEyeView.viewer;
|
||
const Cesium = await import('/node_modules/cesium/Build/Cesium/index.js');
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
const gf = window.__dfGf;
|
||
const cell = (x) => Number(x.toFixed(3));
|
||
const floorAround = (c, seeded) => {
|
||
for (let dy = -1; dy <= 1; dy++) {
|
||
for (let dx = -1; dx <= 1; dx++) {
|
||
gf.reportMeshFloorCell(cell(c.lat) + dy * 0.001, cell(c.lon) + dx * 0.001, seeded);
|
||
}
|
||
}
|
||
};
|
||
const out = {};
|
||
|
||
// (a) FLEET: reach the real admitted-but-not-ready window. A fleet model
|
||
// is registered in `_models` with Cesium's default show=true the moment
|
||
// its glTF resolves, while the billboard handoff waits for `ready` — so
|
||
// for a tick the model claims the visual it is not drawing. That window
|
||
// is ~one fleet tick wide, so hold the state open by shadowing the two
|
||
// flags on the instance (the same defineProperty trick the ground-3d
|
||
// group uses on `tilesLoaded`).
|
||
const findFleetModel = (id) => {
|
||
let found = null;
|
||
const walk = (coll) => {
|
||
const len = coll.length;
|
||
for (let i = 0; i < len; i++) {
|
||
let p; try { p = coll.get(i); } catch { continue; }
|
||
if (!p) continue;
|
||
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
|
||
if (p.activeAnimations !== undefined && p.minimumPixelSize !== undefined && p.id === id) found = p;
|
||
}
|
||
};
|
||
walk(v.scene.primitives);
|
||
return found;
|
||
};
|
||
fl.setParams({ models3d: true });
|
||
const fleetDeadline = Date.now() + 20000;
|
||
let fleetModel = null;
|
||
while (Date.now() < fleetDeadline && !(fleetModel = findFleetModel('aaa097'))) {
|
||
await window.__dfSettle(500);
|
||
}
|
||
if (!fleetModel) {
|
||
fl.setParams({ models3d: false });
|
||
return { skipped: 'no fleet model for aaa097 in this browser' };
|
||
}
|
||
// Accessors with SWALLOWING setters, not data properties: the fleet tick
|
||
// assigns `model.show = false` for a not-ready model, and assigning to a
|
||
// non-writable own property throws in strict mode — which lands inside
|
||
// Cesium's render loop and stops rendering for the rest of the run.
|
||
Object.defineProperty(fleetModel, 'ready', { get: () => false, set: () => {}, configurable: true });
|
||
Object.defineProperty(fleetModel, 'show', { get: () => true, set: () => {}, configurable: true });
|
||
const bb = window.__dfFindBB('aaa097');
|
||
if (!bb) return { error: 'aaa097 billboard missing' };
|
||
bb.show = true; // the handoff would not have hidden it: the model is not ready
|
||
const d0 = window.__dfCarto(bb.position);
|
||
gf._clearMeshFloorCellsForTest();
|
||
// Earlier display-floor scenarios deliberately leave this contact's
|
||
// sticky-cell/rebuild cache populated. Reset that test-owned cache
|
||
// before planting a different artificial floor in the same area, so
|
||
// this case measures loading ownership instead of prior-scenario
|
||
// hysteresis.
|
||
fl._clearDisplayFloorStateForTest();
|
||
const seededFleet = d0.h + 45;
|
||
floorAround(d0, seededFleet);
|
||
// reportMeshFloorCell is a direct test seam and does not schedule a
|
||
// Cesium frame. Force the CallbackProperty to re-evaluate before the
|
||
// assertion, matching the request-render fix in the retained-model
|
||
// scenario above.
|
||
v.scene.requestRender();
|
||
await window.__dfSettle(900);
|
||
const bbAfter = window.__dfFindBB('aaa097');
|
||
out.fleetModelReady = fleetModel.ready;
|
||
out.fleetModelShow = fleetModel.show;
|
||
out.fleetBillboardVisible = !!bbAfter?.show;
|
||
out.fleetSeeded = seededFleet;
|
||
out.fleetH = bbAfter ? window.__dfCarto(bbAfter.position).h : null;
|
||
delete fleetModel.ready;
|
||
delete fleetModel.show;
|
||
fl.setParams({ models3d: false });
|
||
await window.__dfSettle(400);
|
||
|
||
// (b) TRACKED, regime ACTIVE but no model yet. Turning 3D on while
|
||
// tracking makes `_trackedModelRegimeActive()` true immediately; the
|
||
// GLB load takes frames, and the tracked billboard is the visual until
|
||
// it finishes. Sample within that window.
|
||
if (!fl.trackById('aaa097')) return { ...out, error: 'trackById(aaa097) failed' };
|
||
await window.__dfSettle(500);
|
||
const entBefore = v.trackedEntity?.position?.getValue(Cesium.JulianDate.now());
|
||
const cBefore = entBefore ? window.__dfCarto(entBefore) : null;
|
||
gf._clearMeshFloorCellsForTest();
|
||
fl._clearDisplayFloorStateForTest();
|
||
const seededTracked = (cBefore ? cBefore.h : 0) + 45;
|
||
if (cBefore) floorAround(cBefore, seededTracked);
|
||
// Release the tracked model so the next regime activation must load
|
||
// from scratch, then flip the regime on and sample IMMEDIATELY.
|
||
fl.setParams({ models3d: false });
|
||
await window.__dfSettle(400);
|
||
fl.stopTracking();
|
||
await window.__dfSettle(300);
|
||
fl.trackById('aaa097');
|
||
fl.setParams({ models3d: true });
|
||
v.scene.requestRender();
|
||
await new Promise((r) => setTimeout(r, 120)); // inside the load window
|
||
// Count only the contact under test. A headful run can legitimately
|
||
// render unrelated live fleet or military models at the same time;
|
||
// those say nothing about whether aaa097's loading handoff is still
|
||
// billboard-owned. The shared helper keys models by their pick id and
|
||
// uses the production ownership pair (`show && ready`).
|
||
out.renderingModelsDuringLoad = window.__dfCountModels('aaa097').rendering;
|
||
const entLoad = v.trackedEntity?.position?.getValue(Cesium.JulianDate.now());
|
||
out.trackedSeeded = seededTracked;
|
||
out.trackedLoadH = entLoad ? window.__dfCarto(entLoad).h : null;
|
||
fl.setParams({ models3d: false });
|
||
fl.stopTracking();
|
||
await window.__dfSettle(300);
|
||
return out;
|
||
});
|
||
|
||
if (dfLoading.skipped) {
|
||
skip('display-floor/loading: fleet model loading window', dfLoading.skipped);
|
||
} else {
|
||
record('display-floor/loading: a fleet model that is shown-but-not-ready does not own the visual',
|
||
!dfLoading.error && dfLoading.fleetModelShow === true && dfLoading.fleetModelReady === false
|
||
&& dfLoading.fleetBillboardVisible === true
|
||
&& Number.isFinite(dfLoading.fleetH)
|
||
&& dfLoading.fleetH >= dfLoading.fleetSeeded + DISPLAY_FLOOR_LIFT_M - 0.5,
|
||
dfLoading.error
|
||
|| `model show=${dfLoading.fleetModelShow} ready=${dfLoading.fleetModelReady}, billboard visible=${dfLoading.fleetBillboardVisible} at ${Number(dfLoading.fleetH).toFixed(1)} m on a ${Number(dfLoading.fleetSeeded).toFixed(1)} m floor`);
|
||
}
|
||
|
||
record('display-floor/loading: tracked regime active with no rendering model stays floored',
|
||
!dfLoading.error && dfLoading.renderingModelsDuringLoad === 0
|
||
&& Number.isFinite(dfLoading.trackedLoadH)
|
||
&& dfLoading.trackedLoadH >= dfLoading.trackedSeeded + DISPLAY_FLOOR_LIFT_M - 0.5,
|
||
dfLoading.error
|
||
|| `rendering models during load=${dfLoading.renderingModelsDuringLoad}, entity at ${Number(dfLoading.trackedLoadH).toFixed(1)} m on a ${Number(dfLoading.trackedSeeded).toFixed(1)} m floor`);
|
||
|
||
// ---- F5: a taxi-invalidated snap HOLDS, bounded by drift -------------
|
||
// The gate above ("no evidence ⇒ no model") is right for a contact that
|
||
// has NEVER resolved. Applied to one that HAS, it produces a new defect:
|
||
// taxiing past groundSnap's 50 m resample threshold drops the cache, and
|
||
// if the resample misses — tiles streaming, OSM fallback — the backoff is
|
||
// 2–30 s, so the aircraft pops from 3D back to 2D and back again while it
|
||
// rolls. The last MEASUREMENT is held through that instead, bounded by how
|
||
// far the contact can have moved from where it was taken. Same answer the
|
||
// billboard chain gives (`_heldDisplayFloorM`), a quarter of the distance,
|
||
// because a held floor only ever RAISES a sprite while a held snap IS the
|
||
// model's placement.
|
||
//
|
||
// Driven through the app's OWN handoff and its OWN groundSnap instance —
|
||
// real Cesium primitive, real cache, real backoff. Only the position is
|
||
// supplied by the harness rather than by the poll, which is exactly what a
|
||
// taxi is; waiting out the 30 s render delay for the feed to move the
|
||
// display would measure the same thing half a minute later.
|
||
const dfHold = await evalPage(async () => {
|
||
const Cesium = await import('/node_modules/cesium/Build/Cesium/index.js');
|
||
const v = window.__godsEyeView.viewer;
|
||
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
|
||
// `.module` is the layer OBJECT (the default export), not the module
|
||
// namespace, so the handoff seam is not on it. Reach the namespace the
|
||
// same way this group reaches groundFloor's: offer the URL the app
|
||
// itself loaded (Vite serves an edited file as `…?t=<hmr stamp>`, and
|
||
// the plain path would hand back a second, unrelated instance whose
|
||
// module state the app never touches), then prove identity by requiring
|
||
// its default export to BE the live layer object.
|
||
let ns = null;
|
||
const urls = [...new Set([
|
||
...performance.getEntriesByType('resource').map((e) => e.name)
|
||
.filter((n) => /\/src\/data\/flights\.js(\?|$)/.test(n)).reverse(),
|
||
'/src/data/flights.js',
|
||
])];
|
||
for (const url of urls) {
|
||
let mod; try { mod = await import(/* @vite-ignore */ url); } catch { continue; }
|
||
if (mod?.default === fl && typeof mod._driveFleetModelHandoffForTest === 'function') {
|
||
ns = mod;
|
||
break;
|
||
}
|
||
}
|
||
if (!ns) return { skipped: `the app's own flights module was not reachable (tried ${urls.length})` };
|
||
// Use a scenario-owned contact so its groundSnap entry is provably cold;
|
||
// earlier display-floor cases intentionally exercise aaa097's cache.
|
||
const holdIcao = 'aaa098';
|
||
const sourceBb = window.__dfFindBB('aaa097');
|
||
if (!sourceBb) return { error: 'aaa097 source billboard missing' };
|
||
const source = window.__dfCarto(sourceBb.position);
|
||
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => f.icao !== holdIcao);
|
||
window.__SYNTH.flights.push({
|
||
icao: holdIcao,
|
||
callsign: 'HOLD98',
|
||
lon: source.lon,
|
||
lat: source.lat,
|
||
alt: 0,
|
||
vel: 0,
|
||
track: 90,
|
||
onGround: true,
|
||
});
|
||
fl.setParams({ models3d: false });
|
||
await fl.update(v);
|
||
await window.__dfSettle(600);
|
||
const bb = window.__dfFindBB(holdIcao);
|
||
if (!bb) return { error: `${holdIcao} billboard missing` };
|
||
const base = window.__dfCarto(bb.position);
|
||
const basePos = Cesium.Cartesian3.fromDegrees(base.lon, base.lat, base.h);
|
||
// Offered skin: unmistakably not the feed altitude, for the case where
|
||
// this contact's cache is cold and a real sample has to fire.
|
||
const skin = base.h + 60;
|
||
|
||
// 1. Open a deterministic skin and let the REAL fleet tick admit, place
|
||
// and show the model — the arrival path this scenario then interrupts.
|
||
const baseSampleCount = () => window.__dfSkinSamples.filter((sample) => {
|
||
const dLat = sample.lat - base.lat;
|
||
const dLon = (sample.lon - base.lon) * Math.cos(base.lat * Math.PI / 180);
|
||
return Math.hypot(dLat, dLon) <= 0.0002;
|
||
}).length;
|
||
const baseSamplesBefore = baseSampleCount();
|
||
window.__dfSkinM = skin;
|
||
fl.setParams({ models3d: true });
|
||
const up = await window.__dfAwaitTrackedModel(holdIcao, 20000);
|
||
if (!up.rendering) {
|
||
window.__dfSkinM = null;
|
||
fl.setParams({ models3d: false });
|
||
return { skipped: `no fleet model rendered for ${holdIcao} in this browser` };
|
||
}
|
||
const freshOwns = ns._driveFleetModelHandoffForTest({
|
||
icao24: holdIcao, position: basePos, course: 90,
|
||
});
|
||
const baseSampleHits = baseSampleCount() - baseSamplesBefore;
|
||
const freshH = window.__dfModelHeight(holdIcao);
|
||
|
||
// 2. The tiles go away and the contact taxis ~96 m — past the 50 m
|
||
// resample threshold, inside the hold bound. Every resample from here
|
||
// misses, so this is the whole backoff window in one call.
|
||
window.__dfSkinM = null;
|
||
const taxiPos = Cesium.Cartesian3.fromDegrees(base.lon + 0.001, base.lat, base.h);
|
||
// What independent evidence says about each spot. A held snap now loses
|
||
// to a MEASURED floor at the contact's current cell that disagrees with
|
||
// it (groundSnap.HELD_SNAP_CONTRADICTION_M), so if this group has
|
||
// planted a floor at the taxi cell, that is what decides the outcome —
|
||
// and the numbers have to be visible or the failure is unreadable.
|
||
const gfns = window.__dfGf;
|
||
const baseCarto = window.__dfCarto(basePos);
|
||
const taxiCarto = window.__dfCarto(taxiPos);
|
||
const baseMeshM = gfns?.cachedMeshFloor?.(baseCarto.lat, baseCarto.lon) ?? null;
|
||
const taxiMeshM = gfns?.cachedMeshFloor?.(taxiCarto.lat, taxiCarto.lon) ?? null;
|
||
const heldOwns = ns._driveFleetModelHandoffForTest({ icao24: holdIcao, position: taxiPos, course: 90 });
|
||
const heldH = window.__dfModelHeight(holdIcao);
|
||
const heldBb = !!window.__dfFindBB(holdIcao)?.show;
|
||
|
||
// 3. ~385 m out the memory stops describing anywhere this contact has
|
||
// been. It is released rather than stretched, and the gate takes over.
|
||
const farPos = Cesium.Cartesian3.fromDegrees(base.lon + 0.004, base.lat, base.h);
|
||
const releasedOwns = ns._driveFleetModelHandoffForTest({ icao24: holdIcao, position: farPos, course: 90 });
|
||
const releasedBb = !!window.__dfFindBB(holdIcao)?.show;
|
||
|
||
fl.setParams({ models3d: false });
|
||
await window.__dfSettle(300);
|
||
return {
|
||
freshOwns,
|
||
freshH,
|
||
baseSampleHits,
|
||
heldOwns,
|
||
heldH,
|
||
heldBb,
|
||
baseMeshM,
|
||
taxiMeshM,
|
||
releasedOwns,
|
||
releasedBb,
|
||
taxiM: Cesium.Cartesian3.distance(
|
||
Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface(basePos, new Cesium.Cartesian3()),
|
||
Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface(taxiPos, new Cesium.Cartesian3()),
|
||
),
|
||
farM: Cesium.Cartesian3.distance(
|
||
Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface(basePos, new Cesium.Cartesian3()),
|
||
Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface(farPos, new Cesium.Cartesian3()),
|
||
),
|
||
};
|
||
});
|
||
|
||
if (dfHold.skipped) {
|
||
skip('display-floor/hold: a taxi-invalidated ground snap holds the model through the resample backoff',
|
||
dfHold.skipped);
|
||
} else {
|
||
// The taxi has to actually cross the threshold, or the rest proves
|
||
// nothing: > 50 m invalidates, < 250 m is still inside the bound.
|
||
record('display-floor/hold: the taxi really does invalidate, and the far step really does exceed the bound',
|
||
!dfHold.error && dfHold.taxiM > 50 && dfHold.taxiM < 250 && dfHold.farM > 250,
|
||
dfHold.error || `taxi ${Number(dfHold.taxiM).toFixed(1)} m (> 50 m invalidate, < 250 m bound), far step ${Number(dfHold.farM).toFixed(1)} m (> 250 m bound)`);
|
||
|
||
record('display-floor/hold: a taxi-invalidated ground snap holds the model through the resample backoff',
|
||
!dfHold.error && dfHold.baseSampleHits > 0
|
||
&& dfHold.freshOwns === true && dfHold.heldOwns === true
|
||
&& dfHold.heldBb === false
|
||
&& Number.isFinite(dfHold.heldH) && Number.isFinite(dfHold.freshH)
|
||
&& Math.abs(dfHold.heldH - dfHold.freshH) < 0.5,
|
||
dfHold.error || `fresh owns=${dfHold.freshOwns} after ${dfHold.baseSampleHits} positive base sample(s), on a MEASURED floor at ${Number(dfHold.freshH).toFixed(1)} m; tiles gone + ${Number(dfHold.taxiM).toFixed(1)} m taxi → owns=${dfHold.heldOwns} at ${Number(dfHold.heldH).toFixed(1)} m, billboard shown=${dfHold.heldBb} (want owns=true, bb=false: no 3D→2D pop). Independent mesh floor: base=${dfHold.baseMeshM == null ? 'cold' : Number(dfHold.baseMeshM).toFixed(1)} m, taxi=${dfHold.taxiMeshM == null ? 'cold' : Number(dfHold.taxiMeshM).toFixed(1)} m`);
|
||
|
||
record('display-floor/hold: past the drift bound the hold is released and the model is withheld',
|
||
!dfHold.error && dfHold.releasedOwns === false && dfHold.releasedBb === true,
|
||
dfHold.error || `${Number(dfHold.farM).toFixed(1)} m from the sample: model owns=${dfHold.releasedOwns}, billboard back at shown=${dfHold.releasedBb}`);
|
||
}
|
||
|
||
|
||
}
|
||
|
||
// Cleanup: drop the synthetics and the seeded cells so nothing leaks into
|
||
// the run-wide console/HTTP checks below.
|
||
await evalPage(async () => {
|
||
const gev = window.__godsEyeView;
|
||
const v = gev.viewer;
|
||
const fl = gev.dataManager.layers.get('flights').module;
|
||
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => !/^aaa09/.test(f.icao));
|
||
fl.stopTracking();
|
||
window.__dfGf?._clearMeshFloorCellsForTest();
|
||
delete v.scene.sampleHeight;
|
||
if (gev.tileset) {
|
||
const prior = window.__dfPriorTilesLoadedDescriptor;
|
||
if (prior) Object.defineProperty(gev.tileset, 'tilesLoaded', prior);
|
||
else delete gev.tileset.tilesLoaded;
|
||
}
|
||
delete window.__dfPriorTilesLoadedDescriptor;
|
||
delete window.__dfSkinSamples;
|
||
delete window.__dfSkinM;
|
||
await fl.update(v);
|
||
});
|
||
|
||
|
||
|
||
// Harness hardening (2026-07-06): the two earlier console-error checks run
|
||
// BEFORE the ground/ground-3d/arrival groups — an exception thrown in
|
||
// those later scenarios (e.g. inside a fleet tick) was invisible and
|
||
// surfaced only as inexplicable downstream assertion failures. Catch-all.
|
||
record('no console errors across the FULL run (late groups included)', consoleErrors.length === 0,
|
||
consoleErrors.length
|
||
? `${consoleErrors.length}: ${consoleErrors.slice(-3).join(' | ')}`
|
||
: 'clean');
|
||
record('no HTTP 5xx responses across the FULL run', failedResponses.length === 0,
|
||
failedResponses.length
|
||
? `${failedResponses.length}: ${failedResponses.slice(-3).join(' | ')}`
|
||
: 'clean');
|
||
|
||
finishAndExit();
|
||
} finally {
|
||
if (!KEEP_OPEN) {
|
||
await browser.close();
|
||
} else {
|
||
console.log('\n--keep-open set; leaving browser running. Ctrl-C to exit.');
|
||
}
|
||
}
|
||
|
||
function finishAndExit() {
|
||
const failed = results.filter((r) => r.ok === false);
|
||
const passed = results.filter((r) => r.ok === true);
|
||
const skipped = results.filter((r) => r.ok === null);
|
||
console.log(`\n${'─'.repeat(60)}`);
|
||
console.log(` RESULT: ${passed.length} passed, ${failed.length} failed, ${skipped.length} skipped`);
|
||
console.log(`${'─'.repeat(60)}\n`);
|
||
process.exitCode = failed.length > 0 ? 1 : 0;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Frame sampler: run `sampleFn` once per render frame for `count` frames,
|
||
// in the page, collecting its numeric return (or null).
|
||
// ---------------------------------------------------------------------------
|
||
async function sampleFrames(page, count, sampleFn, ...args) {
|
||
const { values, timedOut } = await page.evaluate(async (fnStr, n, extra) => {
|
||
const fn = new Function('return (' + fnStr + ')')();
|
||
const v = window.__godsEyeView.viewer;
|
||
const out = [];
|
||
let timedOut = false;
|
||
await new Promise((resolve) => {
|
||
let i = 0;
|
||
let settled = false;
|
||
const finish = () => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
remove();
|
||
resolve();
|
||
};
|
||
const remove = v.scene.postRender.addEventListener(() => {
|
||
try { out.push(fn(...extra)); } catch (e) { out.push(null); }
|
||
if (++i >= n) {
|
||
finish();
|
||
return;
|
||
}
|
||
// Cesium runs with requestRenderMode enabled, so one request produces
|
||
// one postRender callback. Drive every sample explicitly instead of
|
||
// waiting forever after the first frame.
|
||
v.scene.requestRender();
|
||
});
|
||
const timer = setTimeout(() => {
|
||
timedOut = true;
|
||
finish();
|
||
}, Math.max(15000, n * 1500));
|
||
v.scene.requestRender();
|
||
});
|
||
return { values: out, timedOut };
|
||
}, sampleFn.toString(), count, args);
|
||
return { values, timedOut };
|
||
}
|
||
|
||
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
||
|
||
/** Great-circle surface distance (m) between two lat/lon points (small-angle safe). */
|
||
function haversineMeters(lat1, lon1, lat2, lon2) {
|
||
const R = 6371000;
|
||
const toRad = (d) => (d * Math.PI) / 180;
|
||
const dLat = toRad(lat2 - lat1);
|
||
const dLon = toRad(lon2 - lon1);
|
||
const a = Math.sin(dLat / 2) ** 2
|
||
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
||
return 2 * R * Math.asin(Math.sqrt(a));
|
||
}
|
||
|
||
// Poll a node-side predicate.
|
||
function waitFor(pred, timeoutMs) {
|
||
return new Promise((resolve, reject) => {
|
||
const start = Date.now();
|
||
const tick = () => {
|
||
if (pred()) return resolve();
|
||
if (Date.now() - start > timeoutMs) return reject(new Error('waitFor timeout'));
|
||
setTimeout(tick, 100);
|
||
};
|
||
tick();
|
||
});
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error('\n\x1b[31mHarness error:\x1b[0m', err && err.stack ? err.stack : err);
|
||
process.exit(2);
|
||
});
|