diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e288e8ed..f7bf0a65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,15 @@ ## [Unreleased] +- Preserve full GNSS upstream retry deadlines and concurrent cooldowns; keep + published Street Level token `0` and assign GNSS token `3`. Live QA requires + a successful dated snapshot even in empty airspace. +- Civil Flights fallback retains ADS-B-only emergency/priority status and clears + it on later polls. Analyst/voice results label the retained general aircraft + contact time as `lastContactAtMs`, not the time of the current status. + GNSS Integrity and aircraft emergency highlighting contributed by + [Leo Pechnicki](https://github.com/leopechnicki) (#855, #856). + - Flights and Military Flights highlight aircraft broadcasting a transponder emergency. Squawk 7500 (unlawful interference), 7600 (radio failure) and 7700 (general emergency) — or adsb.lol's ADS-B `emergency` status, which diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 4681f8984..9afeec60b 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -79,7 +79,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **OpenSky Network.** Its license is **non-commercial**, and operational use of the REST API in a live product can require a prior written agreement with OpenSky — even for non-profit/government use. If you deploy this commercially, contact OpenSky for your own terms. The flights layer is a toggle and runs anonymously by default. - **adsb.lol flight fallback.** When OpenSky is unavailable and no last-good OpenSky response exists, the server requests a cached, capped 250 nm adsb.lol point snapshot around the current camera subpoint. This is regional observed context, not worldwide completeness; provenance is exposed in the Flights stats/context row. Military ICAOs remain reconciled through the existing dedicated military registry rather than duplicated. - **GNSS integrity (adsb.lol integrity fields).** The GNSS Integrity layer (id `gnss-interference`) reads the same keyless 250 nm point snapshot around the camera (anchor rounded to a whole degree, 60 s server cache, cooldown on upstream 429). The server returns only hex, position, NIC, NACp and readsb's GPS-loss flag for airborne ADS-B v1/v2 transponder positions seen in the last minute; callsigns and registrations are dropped. The method has three separately sourced parts, exposed as machine-readable `provenance` on the layer and its stats: (1) a GEV-defined per-aircraft classifier (`gev-nic-nacp-v1`): an aircraft has low navigation accuracy when it reports NIC < 7 or NACp < 8 or readsb flags GPS loss. The numbers are borrowed from the 14 CFR 91.227(c) ADS-B Out performance minima, a US regulation, not a published interference classifier; gpsjam.org does not publish its per-aircraft threshold ([FAQ](https://gpsjam.org/faq)), and this one is not yet validated against known events. (2) gpsjam.org's published cell formula and bands (`100 × (bad − 1) / total`; low < 2 %, medium 2–10 %, high > 10 %). (3) A GEV-defined window: at least 3 aircraft per 0.5° cell over a rolling 30 minutes around the views the user visits. Medium and high cells are navigation-integrity anomalies. Suspected jamming or spoofing is a secondary interpretation, not a confirmed source or location: avionics faults also report low values. Verified 2026-09-29. -- **Transponder emergencies.** Emergency highlighting reads fields the flight feeds already return: OpenSky state vectors' `squawk` and adsb.lol/readsb's `squawk` and `emergency`. It makes no extra request. A broadcast code states what the transponder sends, not a verified incident; OpenSky has no ADS-B emergency field, so civil highlighting there relies on squawk 7500/7600/7700 alone. +- **Transponder emergencies.** Emergency highlighting reads fields the flight feeds already return: OpenSky state vectors' `squawk` and adsb.lol/readsb's `squawk` and `emergency`. It makes no extra request. A broadcast code states what the transponder sends, not a verified incident; OpenSky has no ADS-B emergency field, so genuine OpenSky highlighting relies on squawk 7500/7600/7700 alone. The civil adsb.lol fallback preserves its additional ADS-B status beside the OpenSky-compatible vectors, without another request. - **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session, with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so fresh entries survive server restarts. That gitignored cache remains local to the deployment and is not bundled with this repo. adsbdb publishes no general service license; its README says the route data may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh. Deployers are responsible for obtaining any permission their use requires. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it. - **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state. - **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index a186b9c28..c894d49de 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -834,9 +834,13 @@ else `ADS-B EMERGENCY · …` or `ADS-B PRIORITY · …`, in red. Analyst record the voice query fields expose `squawk`, `emergency` (flag), `emergencyKind` and `emergencySource` (`squawk` when the Mode A code names the status, `ads-b` for the readsb field), plus `emergencyBroadcast` (`broadcasting squawk 7700 (general -emergency)` vs `ADS-B reports general emergency`) and `observedAtMs`, the epoch -ms of the last transponder message. Voice results carry `observedAtMs` only for -aircraft with a status. +emergency)` vs `ADS-B reports general emergency`) and `lastContactAtMs`, the latest known general aircraft-message contact in epoch +ms. Missing contact times retain the preceding known contact, or stay null if +none is known. This does not timestamp the current emergency broadcast. Civil +adsb.lol fallback preserves the readsb status in `emergencyByIcao` alongside +unchanged OpenSky-compatible vectors, including clearing on subsequent polls. +Compact voice results retain the emergency flag, source, phrase and named +last-contact time; they do not label that time as the status receipt. `scripts/qa-emergency-squawk.mjs` proves the tint, HUD line and clearing. ## Browser live-source observations @@ -5207,3 +5211,9 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye Military fragments stitch by source identity. Different mapped parcels merge only when their footprints touch or contain one another, never across a seam tolerance gap. + +GNSS integration reserves share token `3`, retaining published Street Level `0`. +It uses the catalog source contract and voice layer manifest. Upstream 429 +Retry-After seconds/HTTP dates retain their full deadlines; overlapping requests +keep the longest cooldown. Live GNSS QA requires a successful dated response +even when there are no qualifying cells. diff --git a/scripts/qa-emergency-squawk.mjs b/scripts/qa-emergency-squawk.mjs index a1625ef6e..88f1adf37 100644 --- a/scripts/qa-emergency-squawk.mjs +++ b/scripts/qa-emergency-squawk.mjs @@ -202,8 +202,6 @@ async function main() { ? { executablePath } : {}), args: [ - '--no-sandbox', - '--disable-setuid-sandbox', '--use-gl=angle', '--use-angle=swiftshader', '--disable-dev-shm-usage', diff --git a/scripts/qa-gnss.mjs b/scripts/qa-gnss.mjs index e673ac224..12cab5ebd 100644 --- a/scripts/qa-gnss.mjs +++ b/scripts/qa-gnss.mjs @@ -329,8 +329,6 @@ async function main() { ? { executablePath } : {}), args: [ - '--no-sandbox', - '--disable-setuid-sandbox', '--use-gl=angle', '--use-angle=swiftshader', '--disable-dev-shm-usage', @@ -353,7 +351,7 @@ async function main() { const liveBodies = []; live.on('response', (res) => { if (res.url().includes('/api/gnss-integrity') && res.ok()) - liveBodies.push(res.json().then((body) => body.rows ?? [])); + liveBodies.push(res.json()); }); await boot(live, { lon: 19.5, lat: 52.5, height: 1_100_000 }); const l = await enableAndSettle(live); @@ -364,10 +362,29 @@ async function main() { const liveSnapshots = (await Promise.allSettled(liveBodies)) .filter((r) => r.status === 'fulfilled') .map((r) => r.value); - const liveRows = liveSnapshots.reduce((sum, rows) => sum + rows.length, 0); + record( + 'LIVE: a successful, dated integrity response was received', + liveSnapshots.length > 0 && + liveSnapshots.every( + (body) => + Array.isArray(body.rows) && + Number.isFinite(body.ageMs) && + body.ageMs >= 0 && + body.classifier === 'gev-nic-nacp-v1', + ), + `successful responses=${liveSnapshots.length}`, + ); + const liveRows = liveSnapshots.reduce( + (sum, body) => sum + (body.rows?.length || 0), + 0, + ); const expectedStore = new Map(); - for (const rows of liveSnapshots) - accumulateGnssObservations(expectedStore, rows, Date.now()); + for (const body of liveSnapshots) { + const at = Date.now(); + accumulateGnssObservations(expectedStore, body.rows, at, { + observedAt: at - body.ageMs, + }); + } const expectedCells = binGnssCells(expectedStore).length; record( 'LIVE: no error, one entity per cell', diff --git a/server/providers/gnssIntegrity.js b/server/providers/gnssIntegrity.js index 277fc119f..4b27bff4f 100644 --- a/server/providers/gnssIntegrity.js +++ b/server/providers/gnssIntegrity.js @@ -18,9 +18,8 @@ const CACHE_MS = 60_000; const CACHE_MAX = 64; /** Cooldown after a 429 or 5xx when upstream sends no usable Retry-After. */ const COOLDOWN_MS = 60_000; -/** Bounds for an upstream-supplied Retry-After. */ +/** Minimum backoff; a valid longer upstream deadline is never shortened. */ const COOLDOWN_MIN_MS = 5_000; -const COOLDOWN_MAX_MS = 120_000; const MAX_RESPONSE_BYTES = 8 * 1024 * 1024; const TIMEOUT_MS = 10_000; @@ -36,13 +35,14 @@ export function gnssIntegrityAnchor(url) { /** Cooldown (ms) an upstream 429 earns, honouring Retry-After when sane. */ export function gnssRetryCooldownMs(raw, nowMs) { - const clamp = (ms) => - Math.min(COOLDOWN_MAX_MS, Math.max(COOLDOWN_MIN_MS, ms)); if (raw) { const seconds = Number(raw); - if (Number.isFinite(seconds) && seconds > 0) return clamp(seconds * 1000); + const ms = seconds * 1000; + if (Number.isFinite(ms) && seconds > 0) + return Math.max(COOLDOWN_MIN_MS, ms); const at = Date.parse(raw); - if (Number.isFinite(at) && at > nowMs) return clamp(at - nowMs); + if (Number.isFinite(at) && at > nowMs) + return Math.max(COOLDOWN_MIN_MS, at - nowMs); } return COOLDOWN_MS; } @@ -75,9 +75,11 @@ export function gnssIntegrityProxy({ if (!response.ok) { await response.body?.cancel(); if (response.status === 429) { - cooldownUntil = - now() + - gnssRetryCooldownMs(response.headers.get('retry-after'), now()); + const at = now(); + cooldownUntil = Math.max( + cooldownUntil, + at + gnssRetryCooldownMs(response.headers.get('retry-after'), at), + ); throw Object.assign(new Error('upstream_rate_limited'), { status: 429, }); diff --git a/server/providers/openai/toolDescriptions.js b/server/providers/openai/toolDescriptions.js index 450c9da81..3504be9a1 100644 --- a/server/providers/openai/toolDescriptions.js +++ b/server/providers/openai/toolDescriptions.js @@ -543,7 +543,7 @@ export const ACTION_DESCRIPTIONS = { }, analyst_query: { description: - 'Counts, lists, superlatives and attribute filters over records loaded by ENABLED layers — "how many flights over Texas", "biggest fire near LA", "ships headed to Oakland", "next rocket launch". A requested list or ranking requires this tool even after set_context_mode returned a Contacts count; that count alone has no requested items. complete:false means the count is a floor ("at least") and rankings cover the records examined; partial names unanswered layers. Items carry lat/lon for a follow-up fly_to_location. ok:false codes: LAYER_OFF (offer to enable), NOT_READY (loading), FOLLOW_UP_MISMATCH (ask again without followUp), UNKNOWN_FIELD/UNKNOWN_SCOPE/BAD_VALUE (retry with the allowed values).', + 'Counts, lists, superlatives and attribute filters over records loaded by ENABLED layers — "how many flights over Texas", "biggest fire near LA", "ships headed to Oakland", "next rocket launch". A requested list or ranking requires this tool even after set_context_mode returned a Contacts count; that count alone has no requested items. complete:false means the count is a floor ("at least") and rankings cover the records examined; partial names unanswered layers. Items carry lat/lon for a follow-up fly_to_location. ok:false codes: LAYER_OFF (offer to enable), NOT_READY (loading), FOLLOW_UP_MISMATCH (ask again without followUp), UNKNOWN_FIELD/UNKNOWN_SCOPE/BAD_VALUE (retry with the allowed values). Aircraft emergency fields describe broadcasts, not confirmed incidents: narrate emergencyBroadcast with emergencySource. lastContactAtMs is retained general aircraft-message contact and never a status-specific receipt timestamp.', $position: 1, parameters: { properties: { diff --git a/src/app/constructCatalog.test.mjs b/src/app/constructCatalog.test.mjs index f1e4c8c4b..d5a1fff63 100644 --- a/src/app/constructCatalog.test.mjs +++ b/src/app/constructCatalog.test.mjs @@ -40,7 +40,7 @@ test('catalogs construct distinct layers and classification from their supplied signal: b.signal, surface: fixtureSurface(b.signal), }); - assert.equal(first.layers.length, 30); + assert.equal(first.layers.length, 31); assert.ok(first.get('local-adsb'), 'Local ADS-B is registered'); assert.deepEqual( first.metadata.find(({ id }) => id === 'local-adsb'), @@ -154,7 +154,7 @@ test('Mapillary can be omitted or replaced without changing catalog membership', surface: fixtureSurface(lifetime.signal), }; const catalog = createApplicationCatalog(options); - assert.equal(catalog.layers.length, 30); + assert.equal(catalog.layers.length, 31); assert.deepEqual(catalog.get('street-level').providerIds, []); assert.equal( catalogControlServices(catalog).streetLevelLayer, diff --git a/src/app/sourceComposition.js b/src/app/sourceComposition.js index 14c864ac5..826b90b1f 100644 --- a/src/app/sourceComposition.js +++ b/src/app/sourceComposition.js @@ -46,6 +46,7 @@ export const CATALOG_SOURCE_CONTRACTS = Object.freeze({ cyclones: contract(['weather-cyclones'], ['getSnapshot']), earthquakes: contract(['earthquakes'], ['getSnapshot']), 'fire-perimeters': contract(['fire-perimeters'], ['getSnapshot']), + 'gnss-interference': contract(['gnss-interference'], ['getSnapshot']), cables: contract(['telegeography-submarine-cables'], ['fetch']), transit: contract(['transit'], ['requestSnapshot', 'getHistory']), }); diff --git a/src/app/sourceComposition.test.mjs b/src/app/sourceComposition.test.mjs index 44a7c8fa0..cf8646e25 100644 --- a/src/app/sourceComposition.test.mjs +++ b/src/app/sourceComposition.test.mjs @@ -37,7 +37,7 @@ test('every acquisition source can be omitted without losing catalog membership' test('all acquisition sources may be absent, and enable fails before initializing or fetching', async (t) => { const actual = catalog({}, t); - assert.equal(actual.layers.length, 30); + assert.equal(actual.layers.length, 31); const manager = new LayerLifecycle( {}, { getSourceAvailability: actual.getSourceAvailability }, diff --git a/src/data/adsbLolFallback.js b/src/data/adsbLolFallback.js index 7cd887dc6..2ae17fbe6 100644 --- a/src/data/adsbLolFallback.js +++ b/src/data/adsbLolFallback.js @@ -94,8 +94,25 @@ export function normalizeAdsbLolPointResponse(payload) { : Math.floor( responseNow > 10_000_000_000 ? responseNow / 1000 : responseNow, ); - const states = (Array.isArray(payload?.ac) ? payload.ac : []) + const aircraft = Array.isArray(payload?.ac) ? payload.ac : []; + const states = aircraft .map((aircraft) => normalizeAdsbLolAircraftState(aircraft, nowSeconds)) .filter(Boolean); - return { time: nowSeconds, states }; + // Keep the OpenSky vectors unchanged. The regional fallback's extra readsb + // status travels alongside them, keyed only to admitted aircraft. + const admitted = new Set(states.map((row) => row[0])); + const emergencyByIcao = Object.fromEntries( + aircraft.flatMap((row) => { + const id = String(row?.hex || '') + .trim() + .toLowerCase(); + if (!admitted.has(id) || typeof row?.emergency !== 'string') return []; + return [[id, row.emergency.trim().toLowerCase()]]; + }), + ); + return { + time: nowSeconds, + states, + ...(Object.keys(emergencyByIcao).length ? { emergencyByIcao } : {}), + }; } diff --git a/src/data/aircraftEmergencyAnalyst.test.mjs b/src/data/aircraftEmergencyAnalyst.test.mjs index 8df428162..2bb9ede1b 100644 --- a/src/data/aircraftEmergencyAnalyst.test.mjs +++ b/src/data/aircraftEmergencyAnalyst.test.mjs @@ -11,9 +11,157 @@ import { MilitaryFlightRecords } from '../layers/military/records.js'; import { mapAnalystRecord as mapFlight } from './flights.js'; import { mapAnalystRecord as mapMilitary } from './militaryFlights.js'; import { createAnalystEngine } from './analystEngine.js'; +import { normalizeAdsbLolPointResponse } from './adsbLolFallback.js'; +import { createGevActionRunner } from '../voice/gevActions.js'; const NOW_MS = 1800000000000; +function civilStore() { + return new FlightRecords({ + geoidHeight: () => 0, + cachedGroundFloor: () => null, + floorAltitudeM: (value) => value, + approxDistanceKm: () => 0, + }); +} +const receiveContext = { + viewerLatDeg: null, + viewerLonDeg: null, + trackedId: null, + floorWarmPoints: [], +}; + +test('real civil fallback preserves ADS-B emergency and priority, then clears', () => { + const store = civilStore(); + const aircraft = { + hex: 'a1b2c3', + flight: 'TEST1', + lat: 30, + lon: -97, + alt_baro: 10000, + gs: 150, + track: 90, + squawk: '1200', + seen: 3, + seen_pos: 1, + }; + for (const [status, expectedKind, flagged] of [ + ['general', 'general', true], + ['minfuel', 'minfuel', false], + ['none', null, false], + ]) { + const payload = normalizeAdsbLolPointResponse({ + now: NOW_MS, + ac: [{ ...aircraft, emergency: status }], + }); + assert.equal( + payload.states[0].length, + 18, + 'fallback vectors retain the OpenSky shape', + ); + const [observation] = openSkySnapshot(payload, { now: NOW_MS }).records; + const result = mapFlight( + aircraft.hex, + store.receive(observation, receiveContext).meta, + ); + assert.equal(result.emergency, flagged); + assert.equal(result.emergencyKind, expectedKind); + assert.equal(result.emergencySource, expectedKind ? 'ads-b' : null); + assert.equal(result.lastContactAtMs, NOW_MS - 3000); + } +}); + +test('changed status without contact time retains general contact, including voice projection', async () => { + const store = civilStore(); + const [observation] = readsbSnapshot( + { + ac: [ + { + hex: 'a1b2c3', + flight: 'TEST1', + lat: 30, + lon: -97, + alt_baro: 10000, + gs: 150, + track: 90, + squawk: '1200', + emergency: 'general', + seen: 3, + seen_pos: 1, + }, + ], + }, + { observedAtMs: NOW_MS, now: NOW_MS }, + ).records; + store.receive(observation, receiveContext); + const meta = store.receive( + { ...observation, contactTimeMs: null, squawk: '7600', emergency: 'none' }, + receiveContext, + ).meta; + const record = mapFlight(observation.id, meta); + assert.equal(record.emergencyKind, 'nordo'); + assert.equal(record.lastContactAtMs, NOW_MS - 3000); + assert.equal(Object.hasOwn(record, 'observedAtMs'), false); + globalThis.window = globalThis.window || { + clearTimeout, + setTimeout, + requestIdleCallback: null, + }; + const flights = { + id: 'flights', + source: 'fixture', + getStats: () => ({ count: 1 }), + getAnalystRecords: () => [record], + }; + const runner = createGevActionRunner({ + viewer: { + clock: { onTick: { addEventListener: () => () => {} } }, + scene: { canvas: { addEventListener() {}, removeEventListener() {} } }, + camera: { + moveEnd: { addEventListener() {} }, + positionCartographic: { + height: 300000, + latitude: 0.52, + longitude: -1.71, + }, + }, + }, + styleManager: { + getContextModeState: () => ({ mode: null, active: false }), + getCockpitState: () => ({ active: false }), + }, + dataManager: { + layers: new Map([['flights', { module: flights }]]), + isEnabled: (id) => id === 'flights', + getAll: () => [ + { id: 'flights', name: 'Flights', enabled: true, stats: { count: 1 } }, + ], + }, + }); + try { + const result = await runner('analyst_query', { + layers: ['flights'], + scope: { kind: 'anywhere' }, + filters: [{ field: 'emergency', op: 'eq', value: true }], + }); + assert.equal(result.ok, true); + assert.equal(result.items[0].emergency, true); + assert.equal(result.items[0].lastContactAtMs, NOW_MS - 3000); + assert.equal(result.items[0].emergencySource, 'squawk'); + assert.equal( + result.items[0].emergencyBroadcast, + 'broadcasting squawk 7600 (radio failure)', + ); + assert.equal( + Object.hasOwn(result.items[0], 'time'), + false, + 'general contact is not a generic incident timestamp', + ); + } finally { + runner.dispose?.(); + } +}); + // OpenSky /states/all: [icao24, callsign, origin_country, time_position, // last_contact, lon, lat, baro_alt, on_ground, velocity, true_track, // vertical_rate, sensors, geo_alt, squawk, spi, position_source, category]. @@ -110,14 +258,14 @@ test('OpenSky 7700 and adsb.lol emergency=general differ only in provenance', () civil.emergencyBroadcast, 'broadcasting squawk 7700 (general emergency)', ); - assert.equal(civil.observedAtMs, NOW_MS - 3000, 'OpenSky last_contact'); + assert.equal(civil.lastContactAtMs, NOW_MS - 3000, 'OpenSky last_contact'); assert.equal(military.emergency, true); assert.equal(military.emergencyKind, 'general'); assert.equal(military.squawk, '1200', 'the code is not an emergency code'); assert.equal(military.emergencySource, 'ads-b'); assert.equal(military.emergencyBroadcast, 'ADS-B reports general emergency'); - assert.equal(military.observedAtMs, NOW_MS - 1500, 'snapshot time - seen'); + assert.equal(military.lastContactAtMs, NOW_MS - 1500, 'snapshot time - seen'); }); test('the analyst can filter emergencies by source', async () => { @@ -150,3 +298,40 @@ test('the analyst can filter emergencies by source', async () => { [['ae01ce', 'ADS-B reports general emergency']], ); }); + +test('military status changes retain only the latest known general contact', () => { + const store = new MilitaryFlightRecords({ + geoidHeight: () => 0, + cachedGroundFloor: () => null, + floorAltitudeM: (v) => v, + }); + const [observation] = readsbSnapshot(ADSB_LOL, { + observedAtMs: NOW_MS, + now: NOW_MS, + }).records; + const context = { + observedAtMs: NOW_MS, + floorWarmPoints: [], + modelOwnsVisual: false, + }; + const unknown = mapMilitary( + observation.id, + store.receive({ ...observation, contactTimeMs: null }, context).meta, + ); + assert.equal(unknown.lastContactAtMs, null); + store.receive(observation, context); + const changed = mapMilitary( + observation.id, + store.receive( + { + ...observation, + contactTimeMs: null, + squawk: '7600', + emergency: 'none', + }, + context, + ).meta, + ); + assert.equal(changed.emergencyKind, 'nordo'); + assert.equal(changed.lastContactAtMs, NOW_MS - 1500); +}); diff --git a/src/data/analystEngine.test.mjs b/src/data/analystEngine.test.mjs index 3bb0dd06e..8da85a4d6 100644 --- a/src/data/analystEngine.test.mjs +++ b/src/data/analystEngine.test.mjs @@ -7,7 +7,7 @@ const TEXLAND = { name: 'Texland', ring: [[-100, 28], [-94, 28], [-94, 33], [-10 const FLIGHTS = [ { id: 'SWA1', lat: 30.2, lon: -97.7, altitudeM: 11000, speedMps: 240, military: false, onGround: false, routeOrigin: 'AUS', routeDestination: 'LAX' }, { id: 'RCH01', lat: 31.0, lon: -97.0, altitudeM: 13500, speedMps: 250, military: true, onGround: false, routeOrigin: null, routeDestination: null }, - { id: 'N123', lat: 45.0, lon: -122.0, altitudeM: 2000, speedMps: 80, military: false, onGround: false, squawk: '7700', emergency: true, emergencyKind: 'general', routeOrigin: null, routeDestination: null }, + { id: 'N123', lat: 45.0, lon: -122.0, altitudeM: 2000, speedMps: 80, military: false, onGround: false, routeOrigin: null, routeDestination: null }, { id: 'GND1', lat: 30.19, lon: -97.66, altitudeM: 150, speedMps: 5, military: false, onGround: true, routeOrigin: null, routeDestination: null }, ]; const SHIPS = [ @@ -325,19 +325,6 @@ test('analyst: route fields queryable from cached enrichment only', async () => assert.deepEqual(r.items.map((i) => i.id), ['SWA1'], 'null route fields never match'); }); -test('analyst: aircraft broadcasting an emergency are filterable by flag and code', async () => { - const flagged = await makeEngine().query({ - layers: ['flights'], scope: { kind: 'anywhere' }, - filters: [{ field: 'emergency', op: 'eq', value: true }], - }); - assert.deepEqual(flagged.items.map((i) => i.id), ['N123']); - const coded = await makeEngine().query({ - layers: ['flights'], scope: { kind: 'anywhere' }, - filters: [{ field: 'squawk', op: 'eq', value: '7700' }], - }); - assert.deepEqual(coded.items.map((i) => i.id), ['N123']); -}); - test('analyst: satellites and local infrastructure are queryable layers', async () => { const SATS = [ { id: 'ISS (ZARYA)', noradId: '25544', name: 'ISS (ZARYA)', lat: 30.3, lon: -97.7, altitudeM: 410000, satelliteClass: 'STATION · ISS', group: 'stations' }, @@ -519,7 +506,6 @@ test('time fields filter by ISO and derive ageHours; the feed window is exposed' assert.equal(age.coverage.layersQueried[0].window, 'last 24 h'); }); - test('follow-up provenance stays attached to old rows after a feed recovers', async () => { let state = 'stale'; const engine = createAnalystEngine({ diff --git a/src/data/flights.test.mjs b/src/data/flights.test.mjs index 6c6a586d6..c325ea744 100644 --- a/src/data/flights.test.mjs +++ b/src/data/flights.test.mjs @@ -18,8 +18,10 @@ import flightsLayer, { } from './flights.js'; import { setMilitaryLayerActive } from './militaryRegistry.js'; import { - GROUND_FLOOR_LIFT_M, reportMeshFloorCell, - setMeshFloorPreferred, _clearMeshFloorCellsForTest, + GROUND_FLOOR_LIFT_M, + reportMeshFloorCell, + setMeshFloorPreferred, + _clearMeshFloorCellsForTest, } from './groundFloor.js'; import { _setTrackedOverlayHostForTest, @@ -33,7 +35,10 @@ test('share-Follow absence requires an accepted OpenSky snapshot', async () => { (await flightsLayer.resolveTrackingRestoreTarget('abc123')).status, 'source-unavailable', ); - _setFlightTrackingRefreshOutcomeForTest({ status: 'accepted', ids: ['different'] }); + _setFlightTrackingRefreshOutcomeForTest({ + status: 'accepted', + ids: ['different'], + }); const missing = await flightsLayer.resolveTrackingRestoreTarget('abc123'); assert.equal(missing.status, 'missing'); assert.equal(missing.reason, 'target-absent-from-snapshot'); @@ -55,7 +60,10 @@ const FULL_INFO = { }; test('flights analyst record: full record maps every contract field', () => { - const r = mapAnalystRecord('a1b2c3', FULL_INFO, { military: false, routeOk: true }); + const r = mapAnalystRecord('a1b2c3', FULL_INFO, { + military: false, + routeOk: true, + }); assert.deepEqual(r, { id: 'SWA696', icao24: 'a1b2c3', @@ -76,7 +84,7 @@ test('flights analyst record: full record maps every contract field', () => { emergencyKind: null, emergencySource: null, emergencyBroadcast: null, - observedAtMs: null, + lastContactAtMs: null, routeOrigin: 'AUS', routeDestination: 'LAX', }); @@ -96,7 +104,8 @@ test('flights analyst record: empty info yields nulls, never NaN/undefined', () assert.equal(r.military, false); for (const [key, value] of Object.entries(r)) { assert.notEqual(value, undefined, `${key} must not be undefined`); - if (typeof value === 'number') assert.ok(Number.isFinite(value), `${key} must not be NaN`); + if (typeof value === 'number') + assert.ok(Number.isFinite(value), `${key} must not be NaN`); } }); @@ -104,14 +113,31 @@ test('flights analyst record: no callsign falls back to registration, then icao2 const r = mapAnalystRecord('abc123', { ...FULL_INFO, callsign: ' ' }); assert.equal(r.id, 'abc123'); // FULL_INFO carries no registration assert.equal(r.callsign, null); - assert.equal(mapAnalystRecord('abc123', { ...FULL_INFO, callsign: '', registration: 'N123AB' }).id, 'N123AB'); - assert.equal(mapAnalystRecord('abc123', { callsign: '', registration: ' ' }).id, 'abc123'); + assert.equal( + mapAnalystRecord('abc123', { + ...FULL_INFO, + callsign: '', + registration: 'N123AB', + }).id, + 'N123AB', + ); + assert.equal( + mapAnalystRecord('abc123', { callsign: '', registration: ' ' }).id, + 'abc123', + ); }); test('flights analyst record: NaN kinematics become null, military flag passes through', () => { - const r = mapAnalystRecord('ae01ce', { - ...FULL_INFO, velocity: NaN, true_track: undefined, verticalRate: null, - }, { military: true }); + const r = mapAnalystRecord( + 'ae01ce', + { + ...FULL_INFO, + velocity: NaN, + true_track: undefined, + verticalRate: null, + }, + { military: true }, + ); assert.equal(r.speedMps, null); assert.equal(r.heading, null); assert.equal(r.verticalRateMps, null); @@ -126,17 +152,25 @@ test('flights analyst record: output is JSON-safe (no Cesium types)', () => { test('flights first update forwards caller cancellation into the feed request', async () => { const realFetch = globalThis.fetch; let observedSignal = null; - globalThis.fetch = (_url, options = {}) => new Promise((_resolve, reject) => { - observedSignal = options.signal; - options.signal?.addEventListener('abort', () => { - reject(new DOMException('aborted', 'AbortError')); - }, { once: true }); - }); + globalThis.fetch = (_url, options = {}) => + new Promise((_resolve, reject) => { + observedSignal = options.signal; + options.signal?.addEventListener( + 'abort', + () => { + reject(new DOMException('aborted', 'AbortError')); + }, + { once: true }, + ); + }); try { const controller = new AbortController(); - const work = flightsLayer.update({ camera: { positionCartographic: null }, scene: {} }, { - signal: controller.signal, - }); + const work = flightsLayer.update( + { camera: { positionCartographic: null }, scene: {} }, + { + signal: controller.signal, + }, + ); await Promise.resolve(); assert.ok(observedSignal); controller.abort(); @@ -161,14 +195,27 @@ test('nonempty OpenSky payload with zero usable rows cannot prove share target a ok: true, status: 200, headers: { get: () => null }, - json: async () => ({ states: [null, {}, ['abc123', null, null, null, null, null, null]] }), + json: async () => ({ + states: [null, {}, ['abc123', null, null, null, null, null, null]], + }), }); try { - await flightsLayer.update({ camera: { positionCartographic: null }, scene: {} }); - const resolution = await flightsLayer.resolveTrackingRestoreTarget('abc123'); + await flightsLayer.update({ + camera: { positionCartographic: null }, + scene: {}, + }); + const resolution = + await flightsLayer.resolveTrackingRestoreTarget('abc123'); assert.equal(resolution.status, 'source-unavailable'); - assert.match(flightsLayer.getStats().error, /Malformed OpenSky aircraft rows/); - assert.equal(flightsLayer.getAnalystRecords().length, 1, 'warm aircraft data is preserved'); + assert.match( + flightsLayer.getStats().error, + /Malformed OpenSky aircraft rows/, + ); + assert.equal( + flightsLayer.getAnalystRecords().length, + 1, + 'warm aircraft data is preserved', + ); } finally { globalThis.fetch = realFetch; } @@ -306,13 +353,31 @@ test('flights poll refreshes tracked callsign/FL/kts and marks a missed poll STA if (!String(url).startsWith('/api/flights')) { return { ok: true, status: 200, json: async () => ({ ac: [] }) }; } - const states = openskyPoll++ === 0 - ? [[ - icao24, 'DAL123 ', 'United States', nowSec, nowSec, - -97.6, 30.3, 10_668, false, 250, 95, 5, null, 10_700, - null, null, null, 5, - ]] - : []; + const states = + openskyPoll++ === 0 + ? [ + [ + icao24, + 'DAL123 ', + 'United States', + nowSec, + nowSec, + -97.6, + 30.3, + 10_668, + false, + 250, + 95, + 5, + null, + 10_700, + null, + null, + null, + 5, + ], + ] + : []; return { ok: true, status: 200, @@ -351,14 +416,18 @@ test('real civil track path creates no native label and publishes every cached h trackedEntityChanged, clock: { currentTime: now }, camera: { - cancelFlight() { cancelledFlights += 1; }, + cancelFlight() { + cancelledFlights += 1; + }, position: new Cesium.Cartesian3(0, -10_000, 7_000), positionWC: new Cesium.Cartesian3(0, -10_000, 7_000), direction: Cesium.Cartesian3.UNIT_Y, transform: Cesium.Matrix4.IDENTITY, viewMatrix: Cesium.Matrix4.IDENTITY, frustum: { projectionMatrix: Cesium.Matrix4.IDENTITY }, - lookAtTransform() { appliedFrames += 1; }, + lookAtTransform() { + appliedFrames += 1; + }, }, scene: { canvas: { clientWidth: 1600, clientHeight: 900 }, @@ -397,9 +466,12 @@ test('real civil track path creates no native label and publishes every cached h const realFetch = globalThis.fetch; globalThis.window = new EventTarget(); const selectionEvents = []; - globalThis.window.addEventListener('gev:awareness-subject-selected', (event) => { - selectionEvents.push(event.detail); - }); + globalThis.window.addEventListener( + 'gev:awareness-subject-selected', + (event) => { + selectionEvents.push(event.detail); + }, + ); globalThis.fetch = async () => ({ ok: false }); _setTrackedOverlayHostForTest(host); try { @@ -411,13 +483,15 @@ test('real civil track path creates no native label and publishes every cached h billboardCollection: { show: true, remove() {} }, viewer, tracked: false, - history: [{ - time: now, - epochMs: Date.now(), - position, - velocity: 250, - track: 95, - }], + history: [ + { + time: now, + epochMs: Date.now(), + position, + velocity: 250, + track: 95, + }, + ], meta: { callsign: 'N12345', altitude: 10_668, @@ -440,11 +514,19 @@ test('real civil track path creates no native label and publishes every cached h }, }); - assert.equal(flightsLayer.trackById(icao24, { origin: 'programmatic' }), true); + assert.equal( + flightsLayer.trackById(icao24, { origin: 'programmatic' }), + true, + ); const entity = viewer.trackedEntity; - assert.ok(entity instanceof Cesium.Entity, 'trackById must create the real Cesium entity'); + assert.ok( + entity instanceof Cesium.Entity, + 'trackById must create the real Cesium entity', + ); assert.equal(entity.label, undefined); - assert.ok(entities.values.every((candidate) => candidate.label === undefined)); + assert.ok( + entities.values.every((candidate) => candidate.label === undefined), + ); assert.deepEqual(entity.gevLabelModel, { title: 'N12345 · FL350 · 486 kts', details: ['TEST AIR · A320', 'AUS → LAX'], @@ -454,17 +536,44 @@ test('real civil track path creates no native label and publishes every cached h const initialAppliedFrames = appliedFrames; const initialCancelledFlights = cancelledFlights; assert.equal(flightsLayer.trackById(icao24, { origin: 'user' }), true); - assert.equal(cancelledFlights, initialCancelledFlights, 'ordinary repeated tracking stays camera-idempotent'); - assert.equal(selectionEvents.at(-1)?.origin, 'user', 'same-target selection upgrades durable authority'); + assert.equal( + cancelledFlights, + initialCancelledFlights, + 'ordinary repeated tracking stays camera-idempotent', + ); + assert.equal( + selectionEvents.at(-1)?.origin, + 'user', + 'same-target selection upgrades durable authority', + ); assert.equal(entity.gevSelectionOrigin, 'user'); assert.equal(flightsLayer.refocusTrackedById('different-flight'), false); - assert.equal(flightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), true); - assert.equal(selectionEvents.at(-1)?.origin, 'voice', 'same-target refocus forwards explicit authority'); + assert.equal( + flightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), + true, + ); + assert.equal( + selectionEvents.at(-1)?.origin, + 'voice', + 'same-target refocus forwards explicit authority', + ); assert.equal(flightsLayer.refocusTrackedById(icao24), true); viewer.scene.preUpdate.raiseEvent(); - assert.equal(viewer.trackedEntity, entity, 'refocus must retain the exact tracked entity'); - assert.equal(appliedFrames, initialAppliedFrames + 1, 'repeated refocus keeps one camera-frame owner'); - assert.equal(cancelledFlights, initialCancelledFlights + 2, 'only explicit refocus requests cancel camera flights'); + assert.equal( + viewer.trackedEntity, + entity, + 'refocus must retain the exact tracked entity', + ); + assert.equal( + appliedFrames, + initialAppliedFrames + 1, + 'repeated refocus keeps one camera-frame owner', + ); + assert.equal( + cancelledFlights, + initialCancelledFlights + 2, + 'only explicit refocus requests cancel camera flights', + ); const publication = publications.at(-1); assert.equal(publication.sourceId, 'tracked'); const entry = publication.entries[0]; @@ -475,7 +584,11 @@ test('real civil track path creates no native label and publishes every cached h const display = entity.position.getValue(now); assert.ok(display, 'tracked position callback must seed its frame cache'); - assert.equal(entry.position(), display, 'host must reuse the exact cached display Cartesian'); + assert.equal( + entry.position(), + display, + 'host must reuse the exact cached display Cartesian', + ); flightsLayer.setParams( { selectedFlightsTrackingId: 'late001' }, @@ -483,10 +596,17 @@ test('real civil track path creates no native label and publishes every cached h ); assert.equal(_pendingFlightTrackingRestoreForTest(), 'late001'); assert.equal(flightsLayer.trackById(icao24, { origin: 'user' }), true); - assert.equal(_pendingFlightTrackingRestoreForTest(), null, 'new user selection cancels stale restore'); + assert.equal( + _pendingFlightTrackingRestoreForTest(), + null, + 'new user selection cancels stale restore', + ); _addFlightTrackingCandidateForTest({ icao24: 'late001', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.5, 30.4, 9_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-97.5, 30.4, 9_000), + }, meta: { ...FULL_INFO, callsign: 'LATE1', rawLat: 30.4, rawLon: -97.5 }, history: [], }); @@ -500,10 +620,17 @@ test('real civil track path creates no native label and publishes every cached h ); assert.equal(_pendingFlightTrackingRestoreForTest(), 'late002'); flightsLayer.stopTracking({ origin: 'user' }); - assert.equal(_pendingFlightTrackingRestoreForTest(), null, 'explicit clear cancels stale restore'); + assert.equal( + _pendingFlightTrackingRestoreForTest(), + null, + 'explicit clear cancels stale restore', + ); _addFlightTrackingCandidateForTest({ icao24: 'late002', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.4, 30.5, 8_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-97.4, 30.5, 8_000), + }, meta: { ...FULL_INFO, callsign: 'LATE2', rawLat: 30.5, rawLon: -97.4 }, history: [], }); @@ -523,7 +650,10 @@ test('real civil track path creates no native label and publishes every cached h ); _addFlightTrackingCandidateForTest({ icao24: 'late003', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000), + }, meta: { ...FULL_INFO, callsign: 'LATE3', rawLat: 30.6, rawLon: -97.3 }, history: [], }); @@ -537,7 +667,10 @@ test('real civil track path creates no native label and publishes every cached h assert.equal(_pendingFlightTrackingRestoreForTest(), 'late004'); _addFlightTrackingCandidateForTest({ icao24: 'late004', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000), + }, meta: { ...FULL_INFO, callsign: 'LATE4', rawLat: 30.7, rawLon: -97.2 }, history: [], }); @@ -548,7 +681,10 @@ test('real civil track path creates no native label and publishes every cached h { selectedFlightsTrackingId: 'late005' }, { origin: 'share-restore' }, ); - assert.equal(flightsLayer.trackById(icao24, { origin: 'programmatic' }), true); + assert.equal( + flightsLayer.trackById(icao24, { origin: 'programmatic' }), + true, + ); assert.equal( _pendingFlightTrackingRestoreForTest(), 'late005', @@ -556,7 +692,10 @@ test('real civil track path creates no native label and publishes every cached h ); _addFlightTrackingCandidateForTest({ icao24: 'late005', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.1, 30.8, 5_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-97.1, 30.8, 5_000), + }, meta: { ...FULL_INFO, callsign: 'LATE5', rawLat: 30.8, rawLon: -97.1 }, history: [], }); @@ -591,7 +730,11 @@ function seedLabelContact({ callsign, registration, tracked = false }) { _setTrackedFlightRefreshStateForTest({ icao24: LABEL_ICAO, entity: null, - billboard: { position: LABEL_POSITION, color: Cesium.Color.WHITE, show: true }, + billboard: { + position: LABEL_POSITION, + color: Cesium.Color.WHITE, + show: true, + }, billboardCollection: { show: true, remove() {} }, viewer: { camera: { positionCartographic: null }, scene: {} }, tracked, @@ -649,7 +792,11 @@ test('civil label chain: a blank callsign falls back to the registration, not th const labels = labelsFor({ callsign: ' ', registration: 'N123AB ' }); for (const [surface, value] of Object.entries(labels)) { if (surface === '_identity') continue; - assert.equal(value, 'N123AB', `${surface} must show the registration, not ${LABEL_ICAO}`); + assert.equal( + value, + 'N123AB', + `${surface} must show the registration, not ${LABEL_ICAO}`, + ); } }); @@ -658,7 +805,11 @@ test('civil label chain: no callsign and no registration still reads as the ICAO const labels = labelsFor({ callsign: null, registration }); for (const [surface, value] of Object.entries(labels)) { if (surface === '_identity') continue; - assert.equal(value, LABEL_ICAO, `${surface} must fall through to the hex`); + assert.equal( + value, + LABEL_ICAO, + `${surface} must fall through to the hex`, + ); } } }); @@ -682,7 +833,11 @@ test('civil label chain: identity stays icao24 while the label moves', () => { ]) { const { _identity } = labelsFor(fixture); for (const [key, value] of Object.entries(_identity)) { - assert.equal(value, LABEL_ICAO, `${key} identity must remain the ICAO hex`); + assert.equal( + value, + LABEL_ICAO, + `${key} identity must remain the ICAO hex`, + ); } } }); @@ -729,7 +884,11 @@ test('civil label chain: the awareness selection EVENT uses the canonical chain' `selection event label for ${JSON.stringify(fixture)}`, ); // Identity must NOT move with the label. - assert.equal(detail.id, LABEL_ICAO, 'selection event identity stays the ICAO hex'); + assert.equal( + detail.id, + LABEL_ICAO, + 'selection event identity stays the ICAO hex', + ); assert.equal(detail.layerId, 'flights'); } }); @@ -740,7 +899,11 @@ test('civil tracked readout: a callsign-less enriched contact reads as its regis _setTrackedFlightRefreshStateForTest({ icao24: LABEL_ICAO, entity, - billboard: { position: LABEL_POSITION, color: Cesium.Color.WHITE, show: false }, + billboard: { + position: LABEL_POSITION, + color: Cesium.Color.WHITE, + show: false, + }, billboardCollection: { show: false, remove() {} }, viewer: { camera: { positionCartographic: null }, scene: {} }, meta: { @@ -761,10 +924,16 @@ test('civil tracked readout: a callsign-less enriched contact reads as its regis const realFetch = globalThis.fetch; globalThis.fetch = async () => ({ - ok: true, status: 200, headers: { get: () => null }, json: async () => ({ time: 0, states: [] }), + ok: true, + status: 200, + headers: { get: () => null }, + json: async () => ({ time: 0, states: [] }), }); try { - await flightsLayer.update({ camera: { positionCartographic: null }, scene: {} }); + await flightsLayer.update({ + camera: { positionCartographic: null }, + scene: {}, + }); assert.match(entity.gevLabelModel.title, /^N123AB\b/); assert.doesNotMatch( [entity.gevLabelModel.title, ...entity.gevLabelModel.details].join(' · '), @@ -844,7 +1013,8 @@ test('military suppression exempts the contact this layer is still restoring', ( // contacts only, never when a 3D model owns the visual (T7 one-shot). // --------------------------------------------------------------------------- -const _floorCarto = (pos) => Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84); +const _floorCarto = (pos) => + Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84); test('a newly seen taxiing aircraft starts on the floor under its delayed position', async () => { // A grounded contact is drawn at its delayed position lifted onto the floor @@ -949,10 +1119,16 @@ test('display floor: a grounded contact that drifted onto a higher cell is lifte // Fix cell floor was 124.7; the display has taxied into a 140.2 m cell. reportMeshFloorCell(30.2004, -97.6604, 140.2); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false); + const out = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + ); const c = _floorCarto(out); - assert.ok(Math.abs(c.height - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, - `height ${c.height} should be 140.2 + lift`); + assert.ok( + Math.abs(c.height - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, + `height ${c.height} should be 140.2 + lift`, + ); // Lifted straight up — the drifted lat/lon is preserved exactly. assert.ok(Math.abs(Cesium.Math.toDegrees(c.latitude) - 30.2004) < 1e-6); assert.ok(Math.abs(Cesium.Math.toDegrees(c.longitude) + 97.6604) < 1e-6); @@ -964,7 +1140,11 @@ test('display floor: an AIRBORNE contact over the same cell is never lifted', () setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140.2); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest({ onGround: false }, pos, false); + const out = _floorGroundedDisplayPositionForTest( + { onGround: false }, + pos, + false, + ); assert.equal(out, pos, 'airborne display positions pass through untouched'); }); @@ -974,8 +1154,16 @@ test('display floor: T7 — a model-owned grounded contact keeps its billboard d setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140.2); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, true); - assert.equal(out, pos, 'moving it would drag groundSnap past its 50 m re-sample threshold'); + const out = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + true, + ); + assert.equal( + out, + pos, + 'moving it would drag groundSnap past its 50 m re-sample threshold', + ); }); test('display floor: a contact already above the floor is passed through unchanged', () => { @@ -984,7 +1172,11 @@ test('display floor: a contact already above the floor is passed through unchang setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 124.7); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 140.2); - const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false); + const out = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + ); assert.equal(out, pos, 'no rebuild when nothing needs lifting'); }); @@ -993,7 +1185,11 @@ test('display floor: a cold display cell leaves the contact where it is', () => _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false); + const out = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + ); assert.equal(out, pos, 'no floor data → no clamp (never invent a surface)'); }); @@ -1004,7 +1200,11 @@ test('display floor: mesh cells are ignored outside the google-3d regime', () => reportMeshFloorCell(30.2004, -97.6604, 140.2); setMeshFloorPreferred(false); // globe stack: the DEM IS the rendered terrain const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false); + const out = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + ); assert.equal(out, pos); setMeshFloorPreferred(true); }); @@ -1020,14 +1220,23 @@ test('display floor: jitter across a cell edge does not alternate the floor', () reportMeshFloorCell(30.2014, -97.6604, 146); const at = (lat) => { const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, lat, 120), false, 'jitter', + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, lat, 120), + false, + 'jitter', ); return _floorCarto(out).height; }; const settled = at(30.2004); // squarely inside the low cell - assert.ok(Math.abs(settled - (140 + GROUND_FLOOR_LIFT_M)) < 0.05, `settled ${settled}`); + assert.ok( + Math.abs(settled - (140 + GROUND_FLOOR_LIFT_M)) < 0.05, + `settled ${settled}`, + ); // 30.2006 belongs to the HIGH cell but is only ~11 m past the boundary. - assert.ok(Math.abs(at(30.2006) - settled) < 0.05, 'held the low cell across the edge'); + assert.ok( + Math.abs(at(30.2006) - settled) < 0.05, + 'held the low cell across the edge', + ); assert.ok(Math.abs(at(30.2004) - settled) < 0.05, 'and back again — no pop'); }); @@ -1037,12 +1246,21 @@ test('display floor: a real crossing does adopt the next cell', () => { setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140); reportMeshFloorCell(30.2014, -97.6604, 146); - const at = (lat) => _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, lat, 120), false, 'crossing', - )).height; + const at = (lat) => + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, lat, 120), + false, + 'crossing', + ), + ).height; at(30.2004); const crossed = at(30.2014); // properly into the next cell - assert.ok(Math.abs(crossed - (146 + GROUND_FLOOR_LIFT_M)) < 0.05, `crossed ${crossed}`); + assert.ok( + Math.abs(crossed - (146 + GROUND_FLOOR_LIFT_M)) < 0.05, + `crossed ${crossed}`, + ); }); // --- F7: no rebuild when nothing changed ----------------------------------- @@ -1057,10 +1275,28 @@ test('display floor: a clamped stationary contact reuses its previous output', ( setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 120); - const first = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'parked'); - const second = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'parked'); - assert.notEqual(first, pos, 'it really is clamped (not the pass-through case)'); - assert.equal(second, first, 'same input + same floor ⇒ the very same object, no rebuild'); + const first = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'parked', + ); + const second = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'parked', + ); + assert.notEqual( + first, + pos, + 'it really is clamped (not the pass-through case)', + ); + assert.equal( + second, + first, + 'same input + same floor ⇒ the very same object, no rebuild', + ); }); test('display floor: the cached output is dropped when the floor changes under it', () => { @@ -1069,14 +1305,31 @@ test('display floor: the cached output is dropped when the floor changes under i setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 120); - const before = _floorCarto(_floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'warming')).height; + const before = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'warming', + ), + ).height; // A late mesh sample raises this cell (a fresh module state stands in for the // one-shot latch, which never overwrites). _clearMeshFloorCellsForTest(); reportMeshFloorCell(30.2004, -97.6604, 175); - const after = _floorCarto(_floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'warming')).height; + const after = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'warming', + ), + ).height; assert.ok(Math.abs(before - 141.5) < 0.05, `before ${before}`); - assert.ok(Math.abs(after - 176.5) < 0.05, `after ${after} — a stale cache would still read 141.5`); + assert.ok( + Math.abs(after - 176.5) < 0.05, + `after ${after} — a stale cache would still read 141.5`, + ); }); // --- F8: hold the last known floor through a floor-data gap ---------------- @@ -1096,18 +1349,28 @@ test('display floor: a cold cell HOLDS the last floor that resolved for this con reportMeshFloorCell(30.2004, -97.6604, 140.2); // On the warm cell: clamped onto the floor as usual. const warm = _floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7), false, 'held', + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7), + false, + 'held', + ); + assert.ok( + Math.abs(_floorCarto(warm).height - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, ); - assert.ok(Math.abs(_floorCarto(warm).height - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05); // Taxis ~550 m onto a cell nothing has resolved, still at the geoid-ish // height the poll path handed it. Pre-fix this passed straight through — // buried. const cold = _floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2054, 124.7), false, 'held', + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, 30.2054, 124.7), + false, + 'held', ); const h = _floorCarto(cold).height; - assert.ok(Math.abs(h - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, - `held ${h} — a cold cell must hold 140.2 + lift, never the 124.7 it arrived with`); + assert.ok( + Math.abs(h - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, + `held ${h} — a cold cell must hold 140.2 + lift, never the 124.7 it arrived with`, + ); }); test('display floor: with NO prior anywhere the contact is left exactly where it is', () => { @@ -1115,8 +1378,17 @@ test('display floor: with NO prior anywhere the contact is left exactly where it _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'fresh'); - assert.equal(out, pos, 'nothing measured anywhere ⇒ nothing invented — same object, no clamp'); + const out = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'fresh', + ); + assert.equal( + out, + pos, + 'nothing measured anywhere ⇒ nothing invented — same object, no clamp', + ); }); test('display floor: a fresh contact on a cold cell adopts a resolved NEIGHBOUR', () => { @@ -1129,11 +1401,16 @@ test('display floor: a fresh contact on a cold cell adopts a resolved NEIGHBOUR' reportMeshFloorCell(30.2014, -97.6604, 141.0); reportMeshFloorCell(30.1994, -97.6604, 152.0); const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7), false, 'neighbour', + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7), + false, + 'neighbour', ); const h = _floorCarto(out).height; - assert.ok(Math.abs(h - (141.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `neighbour hold ${h} — expected the lower of the two resolved neighbours`); + assert.ok( + Math.abs(h - (141.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `neighbour hold ${h} — expected the lower of the two resolved neighbours`, + ); }); // Drives a stationary contact at the production 80 ms fleet cadence and reports @@ -1141,16 +1418,28 @@ test('display floor: a fresh contact on a cold cell adopts a resolved NEIGHBOUR' // `mutate` runs before the tick at that index, so a target can be moved // mid-approach. function _driveEase(id, pos, fromMs, ticks, mutate = () => {}) { - let previous = _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, pos, false, id, fromMs, - )).height; + let previous = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + id, + fromMs, + ), + ).height; const opened = previous; let maxStepM = 0; for (let i = 1; i <= ticks; i += 1) { mutate(i); - const h = _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, pos, false, id, fromMs + i * 80, - )).height; + const h = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + id, + fromMs + i * 80, + ), + ).height; maxStepM = Math.max(maxStepM, previous - h); previous = h; } @@ -1169,10 +1458,18 @@ test('display floor: a hold released onto a LOWER floor eases down, never snaps' if (i === 1) reportMeshFloorCell(30.2004, -97.6604, 160.0); // its own cell resolves lower }); const totalDropM = r.opened - (160.0 + GROUND_FLOOR_LIFT_M); - assert.ok(Math.abs(r.opened - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, `opened at ${r.opened}`); - assert.ok(r.maxStepM <= totalDropM * 0.25, - `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m — a snap is the whole drop at once`); - assert.ok(Math.abs(r.settled - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05, `settled at ${r.settled}`); + assert.ok( + Math.abs(r.opened - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `opened at ${r.opened}`, + ); + assert.ok( + r.maxStepM <= totalDropM * 0.25, + `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m — a snap is the whole drop at once`, + ); + assert.ok( + Math.abs(r.settled - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `settled at ${r.settled}`, + ); }); test('display floor: a hold released onto a HIGHER floor rises immediately', () => { @@ -1180,17 +1477,26 @@ test('display floor: a hold released onto a HIGHER floor rises immediately', () _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const at = (ms) => _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, pos, false, 'rise', ms, - )).height; + const at = (ms) => + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'rise', + ms, + ), + ).height; reportMeshFloorCell(30.2014, -97.6604, 160.0); reportMeshFloorCell(30.1994, -97.6604, 190.0); assert.ok(Math.abs(at(1000) - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05); reportMeshFloorCell(30.2004, -97.6604, 205.0); // No easing UP: an eased rise is time spent under the mesh, which is the // whole failure this path exists to prevent. - assert.ok(Math.abs(at(1001) - (205.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - 'the next tick is already on the higher floor'); + assert.ok( + Math.abs(at(1001) - (205.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + 'the next tick is already on the higher floor', + ); }); // --- F9: the hold is a live state machine, not a one-shot ------------------ @@ -1204,17 +1510,24 @@ test('display floor: a STATIONARY unresolved contact still adopts a floor that a _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const at = (ms) => _floorGroundedDisplayPositionForTest( - { onGround: true }, pos, false, 'parked-cold', ms, - ); + const at = (ms) => + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'parked-cold', + ms, + ); assert.equal(at(1000), pos, 'nothing anywhere yet — passes through'); // The poll's warm batch lands the cells NEXT DOOR (this contact's own cell // is still cold, so only the hold chain can see them). reportMeshFloorCell(30.2014, -97.6604, 138.0); reportMeshFloorCell(30.1994, -97.6604, 160.0); const adopted = _floorCarto(at(1600)).height; // past the probe throttle - assert.ok(Math.abs(adopted - (138.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `stationary contact ${adopted} — a memoized unresolved answer would still read 124.7`); + assert.ok( + Math.abs(adopted - (138.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `stationary contact ${adopted} — a memoized unresolved answer would still read 124.7`, + ); }); test('display floor: a stationary contact FOLLOWS the neighbourhood as it fills in', () => { @@ -1222,9 +1535,16 @@ test('display floor: a stationary contact FOLLOWS the neighbourhood as it fills _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); - const at = (ms) => _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, pos, false, 'upgrade', ms, - )).height; + const at = (ms) => + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'upgrade', + ms, + ), + ).height; reportMeshFloorCell(30.2014, -97.6604, 200.0); reportMeshFloorCell(30.1994, -97.6604, 240.0); assert.ok(Math.abs(at(1000) - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05); @@ -1234,8 +1554,10 @@ test('display floor: a stationary contact FOLLOWS the neighbourhood as it fills reportMeshFloorCell(30.2004, -97.6614, 120.0); let h = 0; for (let ms = 1600; ms <= 6000; ms += 80) h = at(ms); - assert.ok(Math.abs(h - (120.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `followed to ${h} — a borrowed floor must not outlive a better one`); + assert.ok( + Math.abs(h - (120.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `followed to ${h} — a borrowed floor must not outlive a better one`, + ); }); test('display floor: an on_ground FLAP mid-takeoff-roll never dips below the runway', () => { @@ -1249,23 +1571,35 @@ test('display floor: an on_ground FLAP mid-takeoff-roll never dips below the run // the geoid. Deleting the hold on the airborne poll made that switch visible: // the contact came back grounded with no prior, outrunning its own floor // cells at 23 m/s, and sat under the runway (owner sighting, VIR138M). - const GROUND = -28.5, GEOID = -32.5, LON = -73.78; + const GROUND = -28.5, + GEOID = -32.5, + LON = -73.78; reportMeshFloorCell(40.64, LON, GROUND); // only the cell it STARTED on is warm let lat = 40.64; const grounded = []; for (let i = 0; i <= 22; i += 1) { const onGround = i !== 10; // one airborne poll mid-roll const out = _floorGroundedDisplayPositionForTest( - { onGround }, Cesium.Cartesian3.fromDegrees(LON, lat, GEOID), false, 'VIR138M', 1000 + i * 80, + { onGround }, + Cesium.Cartesian3.fromDegrees(LON, lat, GEOID), + false, + 'VIR138M', + 1000 + i * 80, ); if (onGround) grounded.push(_floorCarto(out).height); lat += 0.00021; // ~23 m per tick at 45 kt } const dipped = grounded.filter((h) => h < GROUND - 0.05); - assert.deepEqual(dipped, [], - `${dipped.length} of ${grounded.length} grounded ticks rendered below the runway`); - assert.ok(Math.abs(grounded[grounded.length - 1] - (GROUND + GROUND_FLOOR_LIFT_M)) < 0.05, - 'and it is still standing on the floor it held, not on nothing'); + assert.deepEqual( + dipped, + [], + `${dipped.length} of ${grounded.length} grounded ticks rendered below the runway`, + ); + assert.ok( + Math.abs(grounded[grounded.length - 1] - (GROUND + GROUND_FLOOR_LIFT_M)) < + 0.05, + 'and it is still standing on the floor it held, not on nothing', + ); }); test('display floor: the rehydration seed EXPIRES on its own age, with NO calls in between', () => { @@ -1275,20 +1609,45 @@ test('display floor: the rehydration seed EXPIRES on its own age, with NO calls const field = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); reportMeshFloorCell(30.2014, -97.6604, 200.0); reportMeshFloorCell(30.1994, -97.6604, 240.0); - assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, field, false, 'longhaul', 1000, - )).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, 'holding 200 m before departure'); + assert.ok( + Math.abs( + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + field, + false, + 'longhaul', + 1000, + ), + ).height - + (200.0 + GROUND_FLOOR_LIFT_M), + ) < 0.05, + 'holding 200 m before departure', + ); // ONE airborne tick parks the seed, and then this contact is not seen again // for 198 s: off the poll on a cruise, outside the corridor radius, tab // hidden. Nothing runs in between, so the retire path never gets a second // chance to expire it — the age has to be judged where it comes BACK. // An earlier cut checked expiry only on the retire path and cleared // `retiredMs` before validating it, so this reused the 200 m floor. - _floorGroundedDisplayPositionForTest({ onGround: false }, field, false, 'longhaul', 2000); + _floorGroundedDisplayPositionForTest( + { onGround: false }, + field, + false, + 'longhaul', + 2000, + ); _clearMeshFloorCellsForTest(); // a different airport: nothing warm here const back = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); assert.equal( - _floorGroundedDisplayPositionForTest({ onGround: true }, back, false, 'longhaul', 200000), back, + _floorGroundedDisplayPositionForTest( + { onGround: true }, + back, + false, + 'longhaul', + 200000, + ), + back, 'an expired seed is gone — no floor is invented from a 198-second-old measurement', ); }); @@ -1300,15 +1659,40 @@ test('display floor: the seed expires the same way when the contact keeps report const field = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); reportMeshFloorCell(30.2014, -97.6604, 200.0); reportMeshFloorCell(30.1994, -97.6604, 240.0); - _floorGroundedDisplayPositionForTest({ onGround: true }, field, false, 'cruise', 1000); + _floorGroundedDisplayPositionForTest( + { onGround: true }, + field, + false, + 'cruise', + 1000, + ); // The other half of the same clock: an airborne contact that IS still being // ticked. The window is the window either way. - _floorGroundedDisplayPositionForTest({ onGround: false }, field, false, 'cruise', 2000); - _floorGroundedDisplayPositionForTest({ onGround: false }, field, false, 'cruise', 200000); + _floorGroundedDisplayPositionForTest( + { onGround: false }, + field, + false, + 'cruise', + 2000, + ); + _floorGroundedDisplayPositionForTest( + { onGround: false }, + field, + false, + 'cruise', + 200000, + ); _clearMeshFloorCellsForTest(); const back = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); assert.equal( - _floorGroundedDisplayPositionForTest({ onGround: true }, back, false, 'cruise', 201000), back, + _floorGroundedDisplayPositionForTest( + { onGround: true }, + back, + false, + 'cruise', + 201000, + ), + back, 'no memory of the departure field survives the grace window', ); }); @@ -1324,10 +1708,28 @@ test('display floor: a rehydrated seed ranks BELOW fresh neighbour evidence', () setMeshFloorPreferred(true); const departed = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 50); reportMeshFloorCell(30.2004, -97.6604, 200.0); - assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, departed, false, 'short-hop', 1000, - )).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, 'standing on its own 200 m cell'); - _floorGroundedDisplayPositionForTest({ onGround: false }, departed, false, 'short-hop', 2000); + assert.ok( + Math.abs( + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + departed, + false, + 'short-hop', + 1000, + ), + ).height - + (200.0 + GROUND_FLOOR_LIFT_M), + ) < 0.05, + 'standing on its own 200 m cell', + ); + _floorGroundedDisplayPositionForTest( + { onGround: false }, + departed, + false, + 'short-hop', + 2000, + ); // Re-grounds 0.56 km away — INSIDE HELD_FLOOR_MAX_DRIFT_KM, so the drift // bound does not save us here; the tier order has to. Its own cell is cold, // but two adjacent cells have since resolved, and they are 100 m of ground. @@ -1335,11 +1737,19 @@ test('display floor: a rehydrated seed ranks BELOW fresh neighbour evidence', () reportMeshFloorCell(30.2044, -97.6604, 100.0); reportMeshFloorCell(30.2064, -97.6604, 105.0); const moved = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2054, 50); - const rendered = _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, moved, false, 'short-hop', 3000, - )).height; - assert.ok(Math.abs(rendered - (100.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `rendered at ${rendered} — a seed that outranked the neighbourhood read 201.5, 100 m in the air`); + const rendered = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + moved, + false, + 'short-hop', + 3000, + ), + ).height; + assert.ok( + Math.abs(rendered - (100.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `rendered at ${rendered} — a seed that outranked the neighbourhood read 201.5, 100 m in the air`, + ); }); test('display floor: with nothing fresh to contradict it, the seed still answers', () => { @@ -1351,14 +1761,34 @@ test('display floor: with nothing fresh to contradict it, the seed still answers // warmed either. Demoting the seed must not mean discarding it. const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 50); reportMeshFloorCell(30.2004, -97.6604, 200.0); - _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'flap', 1000); - _floorGroundedDisplayPositionForTest({ onGround: false }, pos, false, 'flap', 2000); + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'flap', + 1000, + ); + _floorGroundedDisplayPositionForTest( + { onGround: false }, + pos, + false, + 'flap', + 2000, + ); _clearMeshFloorCellsForTest(); // the whole neighbourhood goes cold - const held = _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2014, 50), false, 'flap', 3000, - )).height; - assert.ok(Math.abs(held - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `held ${held} — with no measurement anywhere the seed is the best thing available`); + const held = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, 30.2014, 50), + false, + 'flap', + 3000, + ), + ).height; + assert.ok( + Math.abs(held - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `held ${held} — with no measurement anywhere the seed is the best thing available`, + ); }); test('display floor: re-measuring its own cell makes a seeded contact authoritative again', () => { @@ -1367,24 +1797,56 @@ test('display floor: re-measuring its own cell makes a seeded contact authoritat setMeshFloorPreferred(true); const pad = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); reportMeshFloorCell(30.2004, -97.6604, 300.0); - _floorGroundedDisplayPositionForTest({ onGround: true }, pad, false, 'relatch', 1000); - _floorGroundedDisplayPositionForTest({ onGround: false }, pad, false, 'relatch', 2000); + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pad, + false, + 'relatch', + 1000, + ); + _floorGroundedDisplayPositionForTest( + { onGround: false }, + pad, + false, + 'relatch', + 2000, + ); // Re-grounds on its own, still-warm 300 m cell: that is a live reading, so the // demotion is over and the ordinary tier order applies again. - assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, pad, false, 'relatch', 3000, - )).height - (300.0 + GROUND_FLOOR_LIFT_M)) < 0.05, 'back on its own measured cell'); + assert.ok( + Math.abs( + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pad, + false, + 'relatch', + 3000, + ), + ).height - + (300.0 + GROUND_FLOOR_LIFT_M), + ) < 0.05, + 'back on its own measured cell', + ); // It now taxis 220 m onto a cold cell whose two adjacent cells read 100 m. An // own floor it measured while standing here outranks a borrowed one — the // seed flag has to have been cleared by that reading, or the contact drops // 200 m onto someone else's ground. reportMeshFloorCell(30.2014, -97.6604, 100.0); reportMeshFloorCell(30.2034, -97.6604, 100.0); - const taxied = _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2024, 100), false, 'relatch', 4000, - )).height; - assert.ok(Math.abs(taxied - (300.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `taxied at ${taxied} — a still-seeded contact would have taken the 100 m neighbours`); + const taxied = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, 30.2024, 100), + false, + 'relatch', + 4000, + ), + ).height; + assert.ok( + Math.abs(taxied - (300.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `taxied at ${taxied} — a still-seeded contact would have taken the 100 m neighbours`, + ); }); test('display floor: taking off and landing elsewhere does NOT inherit the old field floor', () => { @@ -1394,25 +1856,62 @@ test('display floor: taking off and landing elsewhere does NOT inherit the old f const field = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); reportMeshFloorCell(30.2004, -97.6604, 200.0); // 1. grounded on a 200 m floor. - assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, field, false, 'trip', 1000, - )).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05); + assert.ok( + Math.abs( + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + field, + false, + 'trip', + 1000, + ), + ).height - + (200.0 + GROUND_FLOOR_LIFT_M), + ) < 0.05, + ); // 2. it taxis ~330 m onto ground nothing has resolved, so 200 m is now being // HELD — this is the state that must not survive the trip. const heldAt = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2034, 124.7); - assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, heldAt, false, 'trip', 1500, - )).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, 'the hold is active before takeoff'); + assert.ok( + Math.abs( + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + heldAt, + false, + 'trip', + 1500, + ), + ).height - + (200.0 + GROUND_FLOOR_LIFT_M), + ) < 0.05, + 'the hold is active before takeoff', + ); // 3. airborne — the hold belongs to the grounded-billboard regime, not to the // contact, so leaving that regime must retire it. - _floorGroundedDisplayPositionForTest({ onGround: false }, heldAt, false, 'trip', 2000); + _floorGroundedDisplayPositionForTest( + { onGround: false }, + heldAt, + false, + 'trip', + 2000, + ); // 4. lands at another field on a real 100 m floor. reportMeshFloorCell(31.5004, -97.6604, 100.0); - const landed = _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 31.5004, 60), false, 'trip', 3000, - )).height; - assert.ok(Math.abs(landed - (100.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `landed at ${landed} — a stale hold would arrive at 201.5 and ease down from it`); + const landed = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, 31.5004, 60), + false, + 'trip', + 3000, + ), + ).height; + assert.ok( + Math.abs(landed - (100.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `landed at ${landed} — a stale hold would arrive at 201.5 and ease down from it`, + ); }); test('display floor: a model-owned interval retires the hold too', () => { @@ -1421,21 +1920,47 @@ test('display floor: a model-owned interval retires the hold too', () => { setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); reportMeshFloorCell(30.2004, -97.6604, 200.0); - _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'handoff', 1000); + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'handoff', + 1000, + ); // It moves onto unresolved ground, so 200 m is being HELD. const heldAt = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2034, 124.7); - _floorGroundedDisplayPositionForTest({ onGround: true }, heldAt, false, 'handoff', 1500); + _floorGroundedDisplayPositionForTest( + { onGround: true }, + heldAt, + false, + 'handoff', + 1500, + ); // T7: the 3D model takes the visual. Our state is retired (nothing visual is // touched — groundSnap's one-shot input is untouched either way). - _floorGroundedDisplayPositionForTest({ onGround: true }, heldAt, true, 'handoff', 2000); + _floorGroundedDisplayPositionForTest( + { onGround: true }, + heldAt, + true, + 'handoff', + 2000, + ); // The billboard resumes over ground that has since resolved LOWER. A stale // heldActive would read this as a hold release and ease; it is a fresh start. reportMeshFloorCell(30.2034, -97.6604, 150.0); - const back = _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, heldAt, false, 'handoff', 3000, - )).height; - assert.ok(Math.abs(back - (150.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `resumed at ${back} — no ease, because there was no hold to release`); + const back = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + heldAt, + false, + 'handoff', + 3000, + ), + ).height; + assert.ok( + Math.abs(back - (150.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `resumed at ${back} — no ease, because there was no hold to release`, + ); }); test('display floor: a model handoff back onto a COLD cell takes the neighbourhood, not the seed', () => { @@ -1448,18 +1973,44 @@ test('display floor: a model handoff back onto a COLD cell takes the neighbourho // a floor it measured 330 m ago. Two adjacent cells have resolved at 150 m. const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); reportMeshFloorCell(30.2004, -97.6604, 200.0); - _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'cold-handoff', 1000); + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'cold-handoff', + 1000, + ); const heldAt = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2034, 124.7); - _floorGroundedDisplayPositionForTest({ onGround: true }, heldAt, false, 'cold-handoff', 1500); - _floorGroundedDisplayPositionForTest({ onGround: true }, heldAt, true, 'cold-handoff', 2000); + _floorGroundedDisplayPositionForTest( + { onGround: true }, + heldAt, + false, + 'cold-handoff', + 1500, + ); + _floorGroundedDisplayPositionForTest( + { onGround: true }, + heldAt, + true, + 'cold-handoff', + 2000, + ); _clearMeshFloorCellsForTest(); reportMeshFloorCell(30.2024, -97.6604, 150.0); reportMeshFloorCell(30.2044, -97.6604, 155.0); - const back = _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, heldAt, false, 'cold-handoff', 3000, - )).height; - assert.ok(Math.abs(back - (150.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `resumed at ${back} — a seed that outranked the neighbourhood would resume at 201.5`); + const back = _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + heldAt, + false, + 'cold-handoff', + 3000, + ), + ).height; + assert.ok( + Math.abs(back - (150.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `resumed at ${back} — a seed that outranked the neighbourhood would resume at 201.5`, + ); }); // --- F10: no contact waits on another -------------------------------------- @@ -1482,15 +2033,28 @@ test('display floor: every grounded contact is floored on the tick it asks', () } const heights = []; for (let i = 0; i < N; i += 1) { - heights.push(_floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.66, latOf(i), 100), false, `q${i}`, 1000, - )).height); + heights.push( + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.66, latOf(i), 100), + false, + `q${i}`, + 1000, + ), + ).height, + ); } const unfloored = heights - .map((h, i) => (Math.abs(h - (300 + i + GROUND_FLOOR_LIFT_M)) < 0.05 ? null : i)) + .map((h, i) => + Math.abs(h - (300 + i + GROUND_FLOOR_LIFT_M)) < 0.05 ? null : i, + ) .filter((i) => i != null); - assert.deepEqual(unfloored, [], - 'a global budget would leave the tail of the fleet on the geoid for later ticks'); + assert.deepEqual( + unfloored, + [], + 'a global budget would leave the tail of the fleet on the geoid for later ticks', + ); }); test('display floor: a re-probe that finds a LOWER neighbour eases down, never snaps', () => { @@ -1507,9 +2071,14 @@ test('display floor: a re-probe that finds a LOWER neighbour eases down, never s if (i === 8) reportMeshFloorCell(30.2004, -97.6614, 120.0); // past the 500 ms probe throttle }); const totalDropM = r.opened - target; - assert.ok(r.maxStepM <= totalDropM * 0.25, - `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m`); - assert.ok(Math.abs(r.settled - target) < 0.05, `settled at ${r.settled}, expected ${target}`); + assert.ok( + r.maxStepM <= totalDropM * 0.25, + `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m`, + ); + assert.ok( + Math.abs(r.settled - target) < 0.05, + `settled at ${r.settled}, expected ${target}`, + ); }); test('display floor: a SECOND lower re-latch mid-approach retargets without a jump', () => { @@ -1529,9 +2098,14 @@ test('display floor: a SECOND lower re-latch mid-approach retargets without a ju if (i === 16) reportMeshFloorCell(30.2004, -97.6594, 100.0); // -> 100 m, mid-approach }); const totalDropM = r.opened - target; - assert.ok(r.maxStepM <= totalDropM * 0.25, - `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m — the retarget must not be a seam`); - assert.ok(Math.abs(r.settled - target) < 0.05, `settled at ${r.settled}, expected ${target}`); + assert.ok( + r.maxStepM <= totalDropM * 0.25, + `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m — the retarget must not be a seam`, + ); + assert.ok( + Math.abs(r.settled - target) < 0.05, + `settled at ${r.settled}, expected ${target}`, + ); }); test('display floor: a DELAYED tick cannot close more of the gap than a prompt one', () => { @@ -1539,9 +2113,16 @@ test('display floor: a DELAYED tick cannot close more of the gap than a prompt o _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); - const at = (ms) => _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, pos, false, 'stall', ms, - )).height; + const at = (ms) => + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'stall', + ms, + ), + ).height; reportMeshFloorCell(30.2014, -97.6604, 400.0); reportMeshFloorCell(30.1994, -97.6604, 440.0); // borrowed floor = 400 m const opened = at(1000); @@ -1560,8 +2141,10 @@ test('display floor: a DELAYED tick cannot close more of the gap than a prompt o maxStepM = Math.max(maxStepM, previous - h); previous = h; } - assert.ok(maxStepM <= totalDropM * 0.25, - `largest step ${maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m across a stalled schedule`); + assert.ok( + maxStepM <= totalDropM * 0.25, + `largest step ${maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m across a stalled schedule`, + ); assert.ok(previous < opened, 'and it is still making progress downward'); }); @@ -1572,9 +2155,16 @@ test('display floor: a floor rising MID-APPROACH is taken whole, on that tick', const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); reportMeshFloorCell(30.2014, -97.6604, 400.0); reportMeshFloorCell(30.1994, -97.6604, 440.0); // borrowed floor = 400 m - const at = (ms) => _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, pos, false, 'riseMid', ms, - )).height; + const at = (ms) => + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + 'riseMid', + ms, + ), + ).height; at(1000); reportMeshFloorCell(30.2004, -97.6604, 150.0); // own cell resolves far below let h = 0; @@ -1585,8 +2175,10 @@ test('display floor: a floor rising MID-APPROACH is taken whole, on that tick', _clearMeshFloorCellsForTest(); reportMeshFloorCell(30.2004, -97.6604, 500.0); const risen = at(1000 + 5 * 80); - assert.ok(Math.abs(risen - (500.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `rose to ${risen} — an eased rise is time spent under the mesh`); + assert.ok( + Math.abs(risen - (500.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `rose to ${risen} — an eased rise is time spent under the mesh`, + ); }); test('display floor: a held floor is dropped once the contact leaves the ground it describes', () => { @@ -1594,18 +2186,38 @@ test('display floor: a held floor is dropped once the contact leaves the ground _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 200.0); - const at = (lat, ms) => _floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, lat, 124.7), false, 'drift', ms, + const at = (lat, ms) => + _floorGroundedDisplayPositionForTest( + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, lat, 124.7), + false, + 'drift', + ms, + ); + assert.ok( + Math.abs( + _floorCarto(at(30.2004, 1000)).height - (200.0 + GROUND_FLOOR_LIFT_M), + ) < 0.05, ); - assert.ok(Math.abs(_floorCarto(at(30.2004, 1000)).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05); // ~670 m on: cold cell, but that is inside one poll's rollout. Hold stands. - assert.ok(Math.abs(_floorCarto(at(30.2064, 2000)).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - 'inside the drift bound the held floor still describes the ground'); + assert.ok( + Math.abs( + _floorCarto(at(30.2064, 2000)).height - (200.0 + GROUND_FLOOR_LIFT_M), + ) < 0.05, + 'inside the drift bound the held floor still describes the ground', + ); // ~3.3 km on: the held value is no longer a measurement of anywhere this // contact has been, so it is dropped rather than stretched. const far = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2304, 124.7); assert.equal( - _floorGroundedDisplayPositionForTest({ onGround: true }, far, false, 'drift', 3000), far, + _floorGroundedDisplayPositionForTest( + { onGround: true }, + far, + false, + 'drift', + 3000, + ), + far, 'past the drift bound: back to no clamp, not a stretched one', ); }); @@ -1619,12 +2231,21 @@ test('display floor: an ordinary cell-to-cell floor DROP keeps its existing timi // untouched by this round. reportMeshFloorCell(30.2004, -97.6604, 200.0); reportMeshFloorCell(30.2044, -97.6604, 160.0); - const at = (lat, ms) => _floorCarto(_floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, lat, 124.7), false, 'plain', ms, - )).height; + const at = (lat, ms) => + _floorCarto( + _floorGroundedDisplayPositionForTest( + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, lat, 124.7), + false, + 'plain', + ms, + ), + ).height; assert.ok(Math.abs(at(30.2004, 1000) - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05); - assert.ok(Math.abs(at(30.2044, 1050) - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - 'the very next tick is on the new cell — no ease was introduced here'); + assert.ok( + Math.abs(at(30.2044, 1050) - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + 'the very next tick is on the new cell — no ease was introduced here', + ); }); test('display floor: the hold never applies to an airborne contact or a modelled one', () => { @@ -1636,11 +2257,23 @@ test('display floor: the hold never applies to an airborne contact or a modelled _floorGroundedDisplayPositionForTest({ onGround: true }, on, false, 'gates'); const cold = Cesium.Cartesian3.fromDegrees(-97.6704, 30.2104, 124.7); assert.equal( - _floorGroundedDisplayPositionForTest({ onGround: false }, cold, false, 'gates'), cold, + _floorGroundedDisplayPositionForTest( + { onGround: false }, + cold, + false, + 'gates', + ), + cold, 'airborne heights belong to the fix-time clamp, held floor or not', ); assert.equal( - _floorGroundedDisplayPositionForTest({ onGround: true }, cold, true, 'gates'), cold, + _floorGroundedDisplayPositionForTest( + { onGround: true }, + cold, + true, + 'gates', + ), + cold, 'T7: a model-owned contact keeps its billboard datum, held floor or not', ); }); @@ -1651,11 +2284,21 @@ test('display floor: two contacts on the same cell get their own outputs', () => setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140); const a = _floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 120), false, 'A', + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 120), + false, + 'A', ); const b = _floorGroundedDisplayPositionForTest( - { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.66041, 30.20041, 121), false, 'B', + { onGround: true }, + Cesium.Cartesian3.fromDegrees(-97.66041, 30.20041, 121), + false, + 'B', + ); + assert.notEqual( + a, + b, + 'a shared scratch would hand both contacts the same object', ); - assert.notEqual(a, b, 'a shared scratch would hand both contacts the same object'); assert.ok(Math.abs(_floorCarto(a).height - _floorCarto(b).height) < 0.05); }); diff --git a/src/data/gnssIntegrityProxy.test.mjs b/src/data/gnssIntegrityProxy.test.mjs index 7c5ca93ee..76849c8aa 100644 --- a/src/data/gnssIntegrityProxy.test.mjs +++ b/src/data/gnssIntegrityProxy.test.mjs @@ -205,11 +205,16 @@ test('an upstream 429 starts a cooldown with no further upstream calls', async ( assert.equal(calls, 2); }); -test('Retry-After accepts seconds or an HTTP date, clamped to 5 s – 120 s', () => { +test('Retry-After accepts seconds or an HTTP date, with a 5 s minimum and no truncated deadline', () => { const nowMs = Date.parse('2026-09-29T12:00:00Z'); assert.equal(gnssRetryCooldownMs('30', nowMs), 30_000); assert.equal(gnssRetryCooldownMs('1', nowMs), 5_000); - assert.equal(gnssRetryCooldownMs('3600', nowMs), 120_000); + assert.equal(gnssRetryCooldownMs('3600', nowMs), 3_600_000); + assert.equal( + gnssRetryCooldownMs('Tue, 29 Sep 2026 13:00:00 GMT', nowMs), + 3_600_000, + ); + assert.equal(gnssRetryCooldownMs('1e309', nowMs), 60_000); assert.equal( gnssRetryCooldownMs('Tue, 29 Sep 2026 12:00:45 GMT', nowMs), 45_000, @@ -223,6 +228,40 @@ test('Retry-After accepts seconds or an HTTP date, clamped to 5 s – 120 s', () assert.equal(gnssRetryCooldownMs(null, nowMs), 60_000); }); +test('overlapping 429s preserve the longest shared deadline', async () => { + let clock = 0, + calls = 0; + const pending = []; + const request = install({ + now: () => clock, + fetchImpl: () => { + calls += 1; + return new Promise((resolve) => pending.push(resolve)); + }, + }); + const long = request('/?lat=1&lon=0'), + short = request('/?lat=2&lon=0'); + await new Promise((resolve) => setImmediate(resolve)); + pending[0]( + new Response('', { status: 429, headers: { 'Retry-After': '3600' } }), + ); + assert.equal((await long).headers['Retry-After'], '3600'); + clock = 1000; + pending[1]( + new Response('', { status: 429, headers: { 'Retry-After': '5' } }), + ); + assert.equal((await short).headers['Retry-After'], '3599'); + clock = 7000; + assert.equal((await request('/?lat=3&lon=0')).status, 429); + assert.equal(calls, 2); + clock = 3_600_001; + const resumed = request('/?lat=3&lon=0'); + await new Promise((resolve) => setImmediate(resolve)); + pending[2](Response.json(snapshot)); + assert.equal((await resumed).status, 200); + assert.equal(calls, 3); +}); + test('a cached anchor keeps serving its last snapshot as stale during a cooldown', async () => { let clock = 0; let limited = false; diff --git a/src/data/layerState.js b/src/data/layerState.js index 468e9d117..72a3351b9 100644 --- a/src/data/layerState.js +++ b/src/data/layerState.js @@ -609,7 +609,7 @@ export const LAYER_STATE_REGISTRY = Object.freeze([ }), Object.freeze({ id: 'gnss-interference', - token: '0', + token: '3', disposition: 'enabled-only', }), Object.freeze({ id: 'local-dams', token: 'q', disposition: 'enabled-only' }), diff --git a/src/data/layerState.test.mjs b/src/data/layerState.test.mjs index 9aa0d6e1d..b782de5e2 100644 --- a/src/data/layerState.test.mjs +++ b/src/data/layerState.test.mjs @@ -202,8 +202,8 @@ function encode(state) { test('production registry is exact, canonical, and rejects incomplete contracts', async () => { assert.equal(validateLayerStateRegistry(), true); - assert.equal(REGISTERED_LAYER_IDS.length, 29); - assert.equal(new Set(REGISTERED_LAYER_IDS).size, 29); + assert.equal(REGISTERED_LAYER_IDS.length, 30); + assert.equal(new Set(REGISTERED_LAYER_IDS).size, 30); assert.ok(REGISTERED_LAYER_IDS.includes('transit')); assert.deepEqual(REGISTERED_LAYER_IDS, [...REGISTERED_LAYER_IDS].sort()); assert.deepEqual(LEGACY_LAYER_STATE_TOKENS, { @@ -242,19 +242,19 @@ test('production registry is exact, canonical, and rejects incomplete contracts' for (const [id, token] of Object.entries(LEGACY_LAYER_STATE_TOKENS)) { assert.equal(LAYER_STATE_TOKEN_RESERVATIONS[id], token); } - assert.equal(nextLayerStateToken(), '3'); + assert.equal(nextLayerStateToken(), '4'); assert.equal( nextLayerStateToken({ ...LAYER_STATE_TOKEN_RESERVATIONS, - alpha: '3', - bravo: '4', + alpha: '4', + bravo: '5', }), - '5', + '6', ); const digitsExhausted = { ...LAYER_STATE_TOKEN_RESERVATIONS, ...Object.fromEntries( - [...'3456789'].map((digit) => [`prior-${digit}`, digit]), + [...'456789'].map((digit) => [`prior-${digit}`, digit]), ), }; assert.equal(nextLayerStateToken(digitsExhausted), '00'); @@ -307,8 +307,8 @@ test('production registry is exact, canonical, and rejects incomplete contracts' assert.equal( validateLayerStateAllocations(LAYER_STATE_TOKEN_RESERVATIONS, { ...LAYER_STATE_TOKEN_RESERVATIONS, - future: '3', - next: '4', + future: '4', + next: '5', }), true, ); @@ -318,7 +318,7 @@ test('production registry is exact, canonical, and rejects incomplete contracts' ...LAYER_STATE_TOKEN_RESERVATIONS, future: '00', }), - /next free token 3/, + /next free token 4/, ); const beforeLastDigit = { ...digitsExhausted }; delete beforeLastDigit['prior-9']; @@ -2639,8 +2639,8 @@ test('the recent-imagery split is share-link only: never stored locally, and a s ); }); -test('GNSS interference uses digit 0 without colliding with other digit tokens', () => { - const decoded = decodeLayerStateParams(new URLSearchParams('v=2&l=0.2.1')); +test('GNSS interference uses digit 3 without colliding with other digit tokens', () => { + const decoded = decodeLayerStateParams(new URLSearchParams('v=2&l=3.2.1')); assert.deepEqual(decoded.enabledLayerIds, [ 'fire-perimeters', 'gnss-interference', @@ -2648,7 +2648,7 @@ test('GNSS interference uses digit 0 without colliding with other digit tokens', ]); assert.equal( LAYER_STATE_REGISTRY.find(({ id }) => id === 'gnss-interference').token, - '0', + '3', ); assert.deepEqual( decodeLayerStateParams(new URLSearchParams(encode(decoded))), diff --git a/src/data/layerStateTokenLedger.test.mjs b/src/data/layerStateTokenLedger.test.mjs index 29a5d5eeb..9d7544109 100644 --- a/src/data/layerStateTokenLedger.test.mjs +++ b/src/data/layerStateTokenLedger.test.mjs @@ -128,10 +128,14 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed { ...parseLayerStateTokenReservations(reservationRows) }, { ...LAYER_STATE_TOKEN_RESERVATIONS }, ); - assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 29); + assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 30); assert.deepEqual( { ...LAYER_STATE_TOKEN_RESERVATIONS }, - { ...LEGACY_LAYER_STATE_TOKENS, 'street-level': '0', 'gnss-interference': '0' }, + { + ...LEGACY_LAYER_STATE_TOKENS, + 'street-level': '0', + 'gnss-interference': '3', + }, ); assert.throws( () => parseLayerStateTokenReservations(reservationRows.slice(1)), @@ -151,9 +155,9 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed /Duplicate layer-state token reservation/, ); assert.equal( - parseLayerStateTokenReservations([...reservationRows, ['future', '3']]) + parseLayerStateTokenReservations([...reservationRows, ['future', '4']]) .future, - '3', + '4', ); assert.throws( () => @@ -171,14 +175,14 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed test('published retired digits and competing PRs advance the merge-time allocation', () => { withPublishedBase( - { rows: [...reservationRows, ['retired-layer', '3']] }, + { rows: [...reservationRows, ['retired-layer', '4']] }, (cwd) => { const published = readPublishedLayerStateReservations('HEAD', cwd); - assert.equal(nextLayerStateToken(published), '4'); + assert.equal(nextLayerStateToken(published), '5'); assert.equal( validateLayerStateAllocations(published, { ...published, - newcomer: '4', + newcomer: '5', }), true, ); @@ -186,15 +190,15 @@ test('published retired digits and competing PRs advance the merge-time allocati () => validateLayerStateAllocations(published, { ...published, - competing: '3', + competing: '4', }), - /next free token 4/, + /next free token 5/, ); assert.throws( () => validateLayerStateAllocations(published, { ...LAYER_STATE_TOKEN_RESERVATIONS, - newcomer: '3', + newcomer: '4', }), /changed or removed: retired-layer/, ); @@ -202,8 +206,8 @@ test('published retired digits and competing PRs advance the merge-time allocati ); }); -test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () => { - withPublishedBase({ rows: [...reservationRows, ['pr-a', '3']] }, (cwd) => { +test('PR B manually replaces provisional 4 with 5 after PR A publishes 4', () => { + withPublishedBase({ rows: [...reservationRows, ['pr-a', '4']] }, (cwd) => { mkdirSync(path.join(cwd, 'scripts')); const fixtureChecker = path.join( cwd, @@ -224,8 +228,8 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () => writeCodecFixture( cwd, - [...reservationRows, ['pr-b', '3']], - [{ id: 'pr-b', token: '3' }], + [...reservationRows, ['pr-b', '4']], + [{ id: 'pr-b', token: '4' }], ); const beforeRebase = runCheck(); assert.equal(beforeRebase.status, 1); @@ -233,10 +237,10 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () => writeCodecFixture( cwd, - [...reservationRows, ['pr-a', '3'], ['pr-b', '3']], + [...reservationRows, ['pr-a', '4'], ['pr-b', '4']], [ - { id: 'pr-a', token: '3' }, - { id: 'pr-b', token: '3' }, + { id: 'pr-a', token: '4' }, + { id: 'pr-b', token: '4' }, ], ); const staleRebase = runCheck(); @@ -244,16 +248,16 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () => assert.match(staleRebase.stderr, /Duplicate layer-state token reservation/); const published = readPublishedLayerStateReservations('HEAD', cwd); - assert.equal(nextLayerStateToken(published), '4'); - const validBaselineRows = [...reservationRows, ['pr-a', '3']]; - writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '3' }]); + assert.equal(nextLayerStateToken(published), '5'); + const validBaselineRows = [...reservationRows, ['pr-a', '4']]; + writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '4' }]); const helper = spawnSync( process.execPath, [realpathSync(fixtureNext), 'pr-b'], { cwd, encoding: 'utf8' }, ); assert.equal(helper.status, 0, helper.stderr); - assert.match(helper.stdout, /pr-b: 4/); + assert.match(helper.stdout, /pr-b: 5/); assert.deepEqual( JSON.parse(readFileSync(path.join(cwd, LEDGER_PATH), 'utf8')), validBaselineRows, @@ -261,20 +265,20 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () => ); writeCodecFixture( cwd, - [...reservationRows, ['pr-a', '3'], ['pr-b', '4']], + [...reservationRows, ['pr-a', '4'], ['pr-b', '5']], [ - { id: 'pr-a', token: '3' }, - { id: 'pr-b', token: '4' }, + { id: 'pr-a', token: '4' }, + { id: 'pr-b', token: '5' }, ], ); const corrected = runCheck(); assert.equal(corrected.status, 0, corrected.stderr); - assert.match(corrected.stdout, /30 published, 1 new/); + assert.match(corrected.stdout, /31 published, 1 new/); }); }); test('allocation batches cross the last digit and base-36 pair boundaries in order', () => { - const digitRows = [...'3456789'].map((digit) => [`retired-${digit}`, digit]); + const digitRows = [...'456789'].map((digit) => [`retired-${digit}`, digit]); const allDigits = parseLayerStateTokenReservations([ ...reservationRows, ...digitRows, @@ -326,7 +330,7 @@ test('allocation batches cross the last digit and base-36 pair boundaries in ord test('an isolated valid two-character fixture round-trips an l field beyond the old 64-character cap', async () => { const cwd = mkdtempSync(path.join(tmpdir(), 'gev-layer-token-width-')); try { - const priorDigits = [...'3456789'].map((digit) => [ + const priorDigits = [...'456789'].map((digit) => [ `qa-prior-digit-${digit}`, digit, ]); @@ -363,7 +367,7 @@ test('an isolated valid two-character fixture round-trips an l field beyond the const params = new URLSearchParams([['v', '2']]); codec.encodeLayerStateParams(params, state); assert.ok(params.get('l').length > 64, 'fixture must cross the old cap'); - assert.equal(params.get('l').length, 69); + assert.equal(params.get('l').length, 71); const restored = codec.decodeLayerStateParams(params); assert.deepEqual(restored?.enabledLayerIds, expectedLayerIds); assert.deepEqual(restored?.options, state.options); @@ -381,7 +385,7 @@ test('checker reads a complete future base ledger and rejects retired-token reus }, (cwd) => { const published = readPublishedLayerStateReservations('HEAD', cwd); - assert.equal(Object.keys(published).length, 30); + assert.equal(Object.keys(published).length, 31); assert.equal(published['retired-layer'], '00'); assert.throws( () => diff --git a/src/data/layerStateTokenReservations.json b/src/data/layerStateTokenReservations.json index b859de677..13a5d136b 100644 --- a/src/data/layerStateTokenReservations.json +++ b/src/data/layerStateTokenReservations.json @@ -9,7 +9,7 @@ ["earthquakes", "e"], ["fire-perimeters", "2"], ["flights", "f"], - ["gnss-interference", "0"], + ["gnss-interference", "3"], ["local-dams", "q"], ["local-datacenters", "d"], ["local-firms", "w"], diff --git a/src/data/militaryFlights.test.mjs b/src/data/militaryFlights.test.mjs index b6441aa52..0c518eb5f 100644 --- a/src/data/militaryFlights.test.mjs +++ b/src/data/militaryFlights.test.mjs @@ -24,8 +24,12 @@ test('share-Follow absence requires an accepted adsb.lol snapshot', async () => (await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234')).status, 'source-unavailable', ); - _setMilitaryTrackingRefreshOutcomeForTest({ status: 'accepted', ids: ['different'] }); - const missing = await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); + _setMilitaryTrackingRefreshOutcomeForTest({ + status: 'accepted', + ids: ['different'], + }); + const missing = + await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); assert.equal(missing.status, 'missing'); assert.equal(missing.reason, 'target-absent-from-snapshot'); }); @@ -72,29 +76,12 @@ test('military analyst record: full record maps every contract field', () => { emergencyKind: null, emergencySource: null, emergencyBroadcast: null, - observedAtMs: null, + lastContactAtMs: null, routeOrigin: null, routeDestination: null, }); }); -test('military analyst record: emergency flag is only raised for emergency severity', () => { - const hijack = mapAnalystRecord('ae01ce', { - ...FULL_INFO, - squawk: '7500', - emergency: { kind: 'unlawful', severity: 'emergency', squawk: '7500' }, - }); - assert.equal(hijack.squawk, '7500'); - assert.equal(hijack.emergency, true); - assert.equal(hijack.emergencyKind, 'unlawful'); - const lifeguard = mapAnalystRecord('ae01ce', { - ...FULL_INFO, - emergency: { kind: 'lifeguard', severity: 'priority', squawk: null }, - }); - assert.equal(lifeguard.emergency, false); - assert.equal(lifeguard.emergencyKind, 'lifeguard'); -}); - test('military analyst record: military is ALWAYS true, routes/country always null', () => { const r = mapAnalystRecord('ae01ce', undefined); assert.equal(r.military, true); @@ -104,8 +91,14 @@ test('military analyst record: military is ALWAYS true, routes/country always nu }); test('military analyst record: no callsign falls back to registration, then icao24', () => { - assert.equal(mapAnalystRecord('ae01ce', { ...FULL_INFO, callsign: '' }).id, '05-8152'); - assert.equal(mapAnalystRecord('ae01ce', { callsign: '', registration: ' ' }).id, 'ae01ce'); + assert.equal( + mapAnalystRecord('ae01ce', { ...FULL_INFO, callsign: '' }).id, + '05-8152', + ); + assert.equal( + mapAnalystRecord('ae01ce', { callsign: '', registration: ' ' }).id, + 'ae01ce', + ); }); test('military analyst record: empty info yields nulls, never NaN/undefined', () => { @@ -114,7 +107,8 @@ test('military analyst record: empty info yields nulls, never NaN/undefined', () assert.equal(r.onGround, false); for (const [key, value] of Object.entries(r)) { assert.notEqual(value, undefined, `${key} must not be undefined`); - if (typeof value === 'number') assert.ok(Number.isFinite(value), `${key} must not be NaN`); + if (typeof value === 'number') + assert.ok(Number.isFinite(value), `${key} must not be NaN`); } }); @@ -126,12 +120,17 @@ test('military analyst record: output is JSON-safe (no Cesium types)', () => { test('military first update forwards caller cancellation into the feed request', async () => { const realFetch = globalThis.fetch; let observedSignal = null; - globalThis.fetch = (_url, options = {}) => new Promise((_resolve, reject) => { - observedSignal = options.signal; - options.signal?.addEventListener('abort', () => { - reject(new DOMException('aborted', 'AbortError')); - }, { once: true }); - }); + globalThis.fetch = (_url, options = {}) => + new Promise((_resolve, reject) => { + observedSignal = options.signal; + options.signal?.addEventListener( + 'abort', + () => { + reject(new DOMException('aborted', 'AbortError')); + }, + { once: true }, + ); + }); try { const controller = new AbortController(); const work = militaryFlightsLayer.update({}, { signal: controller.signal }); @@ -161,11 +160,22 @@ test('nonempty adsb.lol payload with zero usable rows cannot prove share target json: async () => ({ ac: [null, {}, { hex: 'ae1234' }] }), }); try { - await militaryFlightsLayer.update({ camera: { positionCartographic: null }, scene: {} }); - const resolution = await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); + await militaryFlightsLayer.update({ + camera: { positionCartographic: null }, + scene: {}, + }); + const resolution = + await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); assert.equal(resolution.status, 'source-unavailable'); - assert.match(militaryFlightsLayer.getStats().error, /Malformed adsb\.lol aircraft rows/); - assert.equal(militaryFlightsLayer.getAnalystRecords().length, 1, 'warm aircraft data is preserved'); + assert.match( + militaryFlightsLayer.getStats().error, + /Malformed adsb\.lol aircraft rows/, + ); + assert.equal( + militaryFlightsLayer.getAnalystRecords().length, + 1, + 'warm aircraft data is preserved', + ); } finally { globalThis.fetch = realFetch; } @@ -211,21 +221,26 @@ test('military poll refreshes tracked callsign/altitude/kts and marks a missed p ok: true, status: 200, json: async () => ({ - ac: poll++ === 0 ? [{ - hex: icao24, - lon: -96.9, - lat: 31.1, - alt_baro: 28_000, - alt_geom: 28_100, - track: 95, - gs: 400, - seen: 0, - seen_pos: 0, - flight: 'RCH451 ', - t: 'C17', - r: '05-8152', - ownOp: 'United States Air Force', - }] : [], + ac: + poll++ === 0 + ? [ + { + hex: icao24, + lon: -96.9, + lat: 31.1, + alt_baro: 28_000, + alt_geom: 28_100, + track: 95, + gs: 400, + seen: 0, + seen_pos: 0, + flight: 'RCH451 ', + t: 'C17', + r: '05-8152', + ownOp: 'United States Air Force', + }, + ] + : [], }), }); @@ -250,7 +265,10 @@ test('cached military snapshots retain their observation time and expose stale f _setTrackedMilitaryRefreshStateForTest({ icao24: 'ae01ce', entity: { gevLabelModel: { title: 'RCH451', details: [] } }, - billboard: { position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000), show: false }, + billboard: { + position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000), + show: false, + }, billboardCollection: { show: false, remove() {} }, viewer, history, @@ -288,11 +306,18 @@ test('cached military snapshots retain their observation time and expose stale f await militaryFlightsLayer.update(viewer); assert.equal(history.length, 1); assert.equal(history[0].epochMs, receivedAt - 2000); - for (const [status, age] of [['HIT', 5000], ['STALE', 45000]]) { + for (const [status, age] of [ + ['HIT', 5000], + ['STALE', 45000], + ]) { cache = status; now = receivedAt + age; await militaryFlightsLayer.update(viewer); - assert.equal(history.length, 1, 'replayed positions cannot create a new fix'); + assert.equal( + history.length, + 1, + 'replayed positions cannot create a new fix', + ); assert.equal(militaryFlightsLayer.getStats().lastUpdate, receivedAt); assert.equal(militaryFlightsLayer.getStats().stale, status === 'STALE'); assert.equal(militaryFlightsLayer.getStats().error, null); @@ -319,14 +344,18 @@ test('real military track path creates no native label and publishes every cache trackedEntityChanged, clock: { currentTime: now }, camera: { - cancelFlight() { cancelledFlights += 1; }, + cancelFlight() { + cancelledFlights += 1; + }, position: new Cesium.Cartesian3(0, -10_000, 7_000), positionWC: new Cesium.Cartesian3(0, -10_000, 7_000), direction: Cesium.Cartesian3.UNIT_Y, transform: Cesium.Matrix4.IDENTITY, viewMatrix: Cesium.Matrix4.IDENTITY, frustum: { projectionMatrix: Cesium.Matrix4.IDENTITY }, - lookAtTransform() { appliedFrames += 1; }, + lookAtTransform() { + appliedFrames += 1; + }, }, scene: { canvas: { clientWidth: 1600, clientHeight: 900 }, @@ -365,9 +394,12 @@ test('real military track path creates no native label and publishes every cache const realFetch = globalThis.fetch; globalThis.window = new EventTarget(); const selectionEvents = []; - globalThis.window.addEventListener('gev:awareness-subject-selected', (event) => { - selectionEvents.push(event.detail); - }); + globalThis.window.addEventListener( + 'gev:awareness-subject-selected', + (event) => { + selectionEvents.push(event.detail); + }, + ); globalThis.fetch = async () => ({ ok: false }); _setTrackedOverlayHostForTest(host); try { @@ -379,13 +411,15 @@ test('real military track path creates no native label and publishes every cache billboardCollection: { show: true, remove() {} }, viewer, tracked: false, - history: [{ - time: now, - epochMs: Date.now(), - position, - velocity: 231.5, - track: 92.1, - }], + history: [ + { + time: now, + epochMs: Date.now(), + position, + velocity: 231.5, + track: 92.1, + }, + ], meta: { ...FULL_INFO, type: 'C17', @@ -395,34 +429,72 @@ test('real military track path creates no native label and publishes every cache }, }); - assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), true); + assert.equal( + militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), + true, + ); const entity = viewer.trackedEntity; - assert.ok(entity instanceof Cesium.Entity, 'trackById must create the real Cesium entity'); + assert.ok( + entity instanceof Cesium.Entity, + 'trackById must create the real Cesium entity', + ); assert.equal(entity.label, undefined); - assert.ok(entities.values.every((candidate) => candidate.label === undefined)); + assert.ok( + entities.values.every((candidate) => candidate.label === undefined), + ); assert.deepEqual(entity.gevLabelModel, { title: 'RCH451', - details: [ - 'C17 · 05-8152', - 'United States Air Force · 28000 ft · 450 kt', - ], + details: ['C17 · 05-8152', 'United States Air Force · 28000 ft · 450 kt'], accent: '#ffd166', }); viewer.scene.preUpdate.raiseEvent(); const initialAppliedFrames = appliedFrames; const initialCancelledFlights = cancelledFlights; - assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'user' }), true); - assert.equal(cancelledFlights, initialCancelledFlights, 'ordinary repeated tracking stays camera-idempotent'); - assert.equal(selectionEvents.at(-1)?.origin, 'user', 'same-target selection upgrades durable authority'); + assert.equal( + militaryFlightsLayer.trackById(icao24, { origin: 'user' }), + true, + ); + assert.equal( + cancelledFlights, + initialCancelledFlights, + 'ordinary repeated tracking stays camera-idempotent', + ); + assert.equal( + selectionEvents.at(-1)?.origin, + 'user', + 'same-target selection upgrades durable authority', + ); assert.equal(entity.gevSelectionOrigin, 'user'); - assert.equal(militaryFlightsLayer.refocusTrackedById('different-flight'), false); - assert.equal(militaryFlightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), true); - assert.equal(selectionEvents.at(-1)?.origin, 'voice', 'same-target refocus forwards explicit authority'); + assert.equal( + militaryFlightsLayer.refocusTrackedById('different-flight'), + false, + ); + assert.equal( + militaryFlightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), + true, + ); + assert.equal( + selectionEvents.at(-1)?.origin, + 'voice', + 'same-target refocus forwards explicit authority', + ); assert.equal(militaryFlightsLayer.refocusTrackedById(icao24), true); viewer.scene.preUpdate.raiseEvent(); - assert.equal(viewer.trackedEntity, entity, 'refocus must retain the exact tracked entity'); - assert.equal(appliedFrames, initialAppliedFrames + 1, 'repeated refocus keeps one camera-frame owner'); - assert.equal(cancelledFlights, initialCancelledFlights + 2, 'only explicit refocus requests cancel camera flights'); + assert.equal( + viewer.trackedEntity, + entity, + 'refocus must retain the exact tracked entity', + ); + assert.equal( + appliedFrames, + initialAppliedFrames + 1, + 'repeated refocus keeps one camera-frame owner', + ); + assert.equal( + cancelledFlights, + initialCancelledFlights + 2, + 'only explicit refocus requests cancel camera flights', + ); const publication = publications.at(-1); assert.equal(publication.sourceId, 'tracked'); const entry = publication.entries[0]; @@ -433,23 +505,40 @@ test('real military track path creates no native label and publishes every cache const display = entity.position.getValue(now); assert.ok(display, 'tracked position callback must seed its frame cache'); - assert.equal(entry.position(), display, 'host must reuse the exact cached display Cartesian'); + assert.equal( + entry.position(), + display, + 'host must reuse the exact cached display Cartesian', + ); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late001' }, { origin: 'share-restore' }, ); assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late001'); - assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'user' }), true); - assert.equal(_pendingMilitaryTrackingRestoreForTest(), null, 'new user selection cancels stale restore'); + assert.equal( + militaryFlightsLayer.trackById(icao24, { origin: 'user' }), + true, + ); + assert.equal( + _pendingMilitaryTrackingRestoreForTest(), + null, + 'new user selection cancels stale restore', + ); _addMilitaryTrackingCandidateForTest({ icao24: 'late001', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.9, 31.1, 8_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-96.9, 31.1, 8_000), + }, meta: { ...FULL_INFO, callsign: 'LATE1', rawLat: 31.1, rawLon: -96.9 }, history: [], }); _applyPendingMilitaryTrackingRestoreForTest(); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, icao24); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + icao24, + ); militaryFlightsLayer.stopTracking(); militaryFlightsLayer.setParams( @@ -458,15 +547,25 @@ test('real military track path creates no native label and publishes every cache ); assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late002'); militaryFlightsLayer.stopTracking({ origin: 'user' }); - assert.equal(_pendingMilitaryTrackingRestoreForTest(), null, 'explicit clear cancels stale restore'); + assert.equal( + _pendingMilitaryTrackingRestoreForTest(), + null, + 'explicit clear cancels stale restore', + ); _addMilitaryTrackingCandidateForTest({ icao24: 'late002', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.8, 31.2, 7_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-96.8, 31.2, 7_000), + }, meta: { ...FULL_INFO, callsign: 'LATE2', rawLat: 31.2, rawLon: -96.8 }, history: [], }); _applyPendingMilitaryTrackingRestoreForTest(); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, null); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + null, + ); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late003' }, @@ -481,12 +580,18 @@ test('real military track path creates no native label and publishes every cache ); _addMilitaryTrackingCandidateForTest({ icao24: 'late003', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000), + }, meta: { ...FULL_INFO, callsign: 'LATE3', rawLat: 30.6, rawLon: -97.3 }, history: [], }); _applyPendingMilitaryTrackingRestoreForTest(); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, null); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + null, + ); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late004' }, @@ -495,18 +600,27 @@ test('real military track path creates no native label and publishes every cache assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late004'); _addMilitaryTrackingCandidateForTest({ icao24: 'late004', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000), + }, meta: { ...FULL_INFO, callsign: 'LATE4', rawLat: 30.7, rawLon: -97.2 }, history: [], }); assert.equal(_applyPendingMilitaryTrackingRestoreForTest(), true); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, 'late004'); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + 'late004', + ); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late005' }, { origin: 'local-restore' }, ); - assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), true); + assert.equal( + militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), + true, + ); assert.equal( _pendingMilitaryTrackingRestoreForTest(), 'late005', @@ -514,12 +628,18 @@ test('real military track path creates no native label and publishes every cache ); _addMilitaryTrackingCandidateForTest({ icao24: 'late005', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.6, 31.4, 5_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-96.6, 31.4, 5_000), + }, meta: { ...FULL_INFO, callsign: 'LATE5', rawLat: 31.4, rawLon: -96.6 }, history: [], }); assert.equal(_applyPendingMilitaryTrackingRestoreForTest(), true); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, 'late005'); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + 'late005', + ); } finally { militaryFlightsLayer.stopTracking(); viewer.scene.preUpdate.raiseEvent(); @@ -564,21 +684,47 @@ test('a callsign-less contact is findable by the tail number the app displays', // The identity analyst_query hands back for this contact. const analystId = mapAnalystRecord(icao24, { - callsign: null, registration: '6606', rawLat: 40.71, rawLon: -74.01, + callsign: null, + registration: '6606', + rawLat: 40.71, + rawLon: -74.01, }).id; - assert.equal(analystId, '6606', 'the analyst names a callsign-less contact by its tail'); + assert.equal( + analystId, + '6606', + 'the analyst names a callsign-less contact by its tail', + ); const found = militaryFlightsLayer.findByQuery(analystId); - assert.equal(found?.icao24, icao24, 'and the tracker must resolve that same identity'); - assert.equal(militaryFlightsLayer.findByQuery('660')?.icao24, icao24, 'prefix match too'); - assert.equal(militaryFlightsLayer.findByQuery(icao24)?.icao24, icao24, 'hex still wins outright'); + assert.equal( + found?.icao24, + icao24, + 'and the tracker must resolve that same identity', + ); + assert.equal( + militaryFlightsLayer.findByQuery('660')?.icao24, + icao24, + 'prefix match too', + ); + assert.equal( + militaryFlightsLayer.findByQuery(icao24)?.icao24, + icao24, + 'hex still wins outright', + ); }); test('the analyst record carries the hex key the tracker keys on', () => { const record = mapAnalystRecord('ae7f01', { - callsign: null, registration: '6606', rawLat: 40.71, rawLon: -74.01, + callsign: null, + registration: '6606', + rawLat: 40.71, + rawLon: -74.01, }); - assert.equal(record.icao24, 'ae7f01', 'a resolvable key must ride along with the display label'); + assert.equal( + record.icao24, + 'ae7f01', + 'a resolvable key must ride along with the display label', + ); }); test('a registration never outranks another contact’s exact callsign', () => { @@ -591,18 +737,35 @@ test('a registration never outranks another contact’s exact callsign', () => { show: true, }); const base = { - type: 'C17', operator: 'USAF', altitudeFt: 25000, speedMps: 180, - track: 90, klass: 'widebody', onGround: false, + type: 'C17', + operator: 'USAF', + altitudeFt: 25000, + speedMps: 180, + track: 90, + klass: 'widebody', + onGround: false, }; // Registered FIRST, so insertion order favours it. _addMilitaryTrackingCandidateForTest({ icao24: 'ae0aa1', - meta: { ...base, callsign: null, registration: 'ZULU777', rawLat: 31.0, rawLon: -97.0 }, + meta: { + ...base, + callsign: null, + registration: 'ZULU777', + rawLat: 31.0, + rawLon: -97.0, + }, billboard: billboardAt(-97.0, 31.0), }); _addMilitaryTrackingCandidateForTest({ icao24: 'ae0bb2', - meta: { ...base, callsign: 'ZULU777', registration: '05-9999', rawLat: 31.2, rawLon: -97.2 }, + meta: { + ...base, + callsign: 'ZULU777', + registration: '05-9999', + rawLat: 31.2, + rawLon: -97.2, + }, billboard: billboardAt(-97.2, 31.2), }); @@ -625,24 +788,49 @@ test('two contacts matching at the same strength resolve deterministically', () show: true, }); const base = { - type: 'C130', operator: 'USAF', altitudeFt: 21000, speedMps: 150, - track: 45, klass: 'turboprop', onGround: false, + type: 'C130', + operator: 'USAF', + altitudeFt: 21000, + speedMps: 150, + track: 45, + klass: 'turboprop', + onGround: false, }; // Both registrations begin with QQ7 — a same-tier prefix tie. Registered // highest-hex first so insertion order would pick the wrong one. _addMilitaryTrackingCandidateForTest({ icao24: 'aef002', - meta: { ...base, callsign: null, registration: 'QQ7-222', rawLat: 32.0, rawLon: -98.0 }, + meta: { + ...base, + callsign: null, + registration: 'QQ7-222', + rawLat: 32.0, + rawLon: -98.0, + }, billboard: billboardAt(-98.0, 32.0), }); _addMilitaryTrackingCandidateForTest({ icao24: 'aef001', - meta: { ...base, callsign: null, registration: 'QQ7-111', rawLat: 32.1, rawLon: -98.1 }, + meta: { + ...base, + callsign: null, + registration: 'QQ7-111', + rawLat: 32.1, + rawLon: -98.1, + }, billboard: billboardAt(-98.1, 32.1), }); const first = militaryFlightsLayer.findByQuery('QQ7')?.icao24; const second = militaryFlightsLayer.findByQuery('qq7')?.icao24; - assert.equal(first, 'aef001', 'the stable key (lowest hex) wins, not the feed order'); - assert.equal(second, first, 'and the same query resolves the same way every time'); + assert.equal( + first, + 'aef001', + 'the stable key (lowest hex) wins, not the feed order', + ); + assert.equal( + second, + first, + 'and the same query resolves the same way every time', + ); }); diff --git a/src/firstRunExperience.test.mjs b/src/firstRunExperience.test.mjs index 49711163f..53524bada 100644 --- a/src/firstRunExperience.test.mjs +++ b/src/firstRunExperience.test.mjs @@ -673,11 +673,11 @@ test('the voice TOOL SCHEMA matches the pinned contract — the mission mapping // referent-only track_entity alternative (kept out of the model-facing // schema, which may not carry top-level anyOf), and Contacts requested-radius // list descriptions; the missions still ride existing tools. - // Street Level adds toggle enum values and the generated alias hint. - assert.equal(block.length, 29802, 'serialized tool schema length drifted'); + // GNSS and emergency/contact fields extend the manifest-generated contract. + assert.equal(block.length, 30208, 'serialized tool schema length drifted'); assert.equal( crypto.createHash('sha256').update(block).digest('hex'), - 'b316ca2e67848eb565b6847ac044a19d1465d8ef8252a5dfbd7d947a2a36d5ed', + 'f226ed9a1d1867b107b39073f527add7f9cb083ef013dbb2111842fcbd343c35', 'the first-run missions must ride EXISTING tools: no schema edit, no cache bust', ); const instructions = fs.readFileSync(new URL('../server/providers/openai/instructions.js', import.meta.url), 'utf8'); diff --git a/src/layers/flights/queries.js b/src/layers/flights/queries.js index 0714acfc5..c6a55f76c 100644 --- a/src/layers/flights/queries.js +++ b/src/layers/flights/queries.js @@ -225,7 +225,7 @@ export function createQueries({ * military: boolean, aircraftClass: string|null, originCountry: string|null, * operator: string|null, squawk: string|null, emergency: boolean, * emergencyKind: string|null, emergencySource: 'squawk'|'ads-b'|null, - * emergencyBroadcast: string|null, observedAtMs: number|null, routeOrigin: string|null, + * emergencyBroadcast: string|null, lastContactAtMs: number|null, routeOrigin: string|null, * routeDestination: string|null}} */ @@ -271,9 +271,9 @@ export function createQueries({ // when the Mode A code names the status, 'ads-b' for the readsb field. emergencySource: text(info?.emergency?.source), emergencyBroadcast: emergencyBroadcastPhrase(info?.emergency), - // Epoch ms of the last transponder message — when this squawk/status - // was heard (OpenSky time_contact; readsb snapshot time minus `seen`). - observedAtMs: num(info?.lastContactEpochMs), + // Latest known general aircraft-message contact, retained if a poll + // has no time. This is not the receipt time of the current status. + lastContactAtMs: num(info?.lastContactEpochMs), routeOrigin: routeOk ? text(info?.route?.origin?.code) : null, routeDestination: routeOk ? text(info?.route?.destination?.code) : null, }; diff --git a/src/layers/military/queries.js b/src/layers/military/queries.js index 0d418ddc1..b3d1b8cf3 100644 --- a/src/layers/military/queries.js +++ b/src/layers/military/queries.js @@ -162,7 +162,7 @@ export function createQueries({ * military: boolean, aircraftClass: string|null, originCountry: null, * operator: string|null, squawk: string|null, emergency: boolean, * emergencyKind: string|null, emergencySource: 'squawk'|'ads-b'|null, - * emergencyBroadcast: string|null, observedAtMs: number|null, routeOrigin: null, routeDestination: null}} + * emergencyBroadcast: string|null, lastContactAtMs: number|null, routeOrigin: null, routeDestination: null}} */ function mapAnalystRecord(icao24, info) { @@ -204,9 +204,9 @@ export function createQueries({ // when the Mode A code names the status, 'ads-b' for the readsb field. emergencySource: text(info?.emergency?.source), emergencyBroadcast: emergencyBroadcastPhrase(info?.emergency), - // Epoch ms of the last transponder message — when this squawk/status - // was heard (OpenSky time_contact; readsb snapshot time minus `seen`). - observedAtMs: num(info?.lastContactEpochMs), + // Latest known general aircraft-message contact, retained if a poll + // has no time. This is not the receipt time of the current status. + lastContactAtMs: num(info?.lastContactEpochMs), routeOrigin: null, routeDestination: null, }; diff --git a/src/sources/live/aircraft.js b/src/sources/live/aircraft.js index 977801aa7..faa77d08a 100644 --- a/src/sources/live/aircraft.js +++ b/src/sources/live/aircraft.js @@ -87,7 +87,13 @@ export function openSkySnapshot( ) { const admitted = admitRecords( payload?.states, - normalizeOpenSkyAircraft, + (row) => { + const record = normalizeOpenSkyAircraft(row); + const statuses = payload?.emergencyByIcao; + if (record && statuses && Object.hasOwn(statuses, record.id)) + record.emergency = cleanText(statuses[record.id]).toLowerCase() || null; + return record; + }, 'OpenSky', ); const observedAtMs = epoch(payload?.time, 1000); diff --git a/src/voice/actionSchemas.test.mjs b/src/voice/actionSchemas.test.mjs index d3b8e8a90..4a8f130a6 100644 --- a/src/voice/actionSchemas.test.mjs +++ b/src/voice/actionSchemas.test.mjs @@ -36,8 +36,8 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea // consolidated tool wording (each policy stated once); model-facing tools // omit top-level anyOf (OpenAI rejects it). Contacts list routing preserves // requested radius and center authority in descriptions only. - // Street Level adds its toggle enum values and generated alias hint. - '9eb113454c5af5e32e27ffcdc7919d0e4600ba1d204f8f175e071152be41ad0f', + // GNSS and emergency/contact fields extend the manifest-generated contract. + '3f18ec8718981cfa42e1462a0f1a9f0102cd90f1f86e0cc25a9c0d0f3b291a53', ); }); diff --git a/src/voice/gevActions.js b/src/voice/gevActions.js index 9e1e183d0..3a80ea8d7 100644 --- a/src/voice/gevActions.js +++ b/src/voice/gevActions.js @@ -4809,7 +4809,10 @@ function compactAnalystItem(r) { if (!present(r[key]) || key in compact) continue; if (spec.type === 'time') { const ms = Number(r[key]); - if (Number.isFinite(ms)) compact.time = new Date(ms).toISOString(); + if (Number.isFinite(ms)) { + if (key === 'lastContactAtMs') compact.lastContactAtMs = ms; + else compact.time = new Date(ms).toISOString(); + } } else compact[key] = r[key]; } if (present(r.ageHours)) compact.ageHours = r.ageHours; diff --git a/src/voice/layerManifest.js b/src/voice/layerManifest.js index 1e348ee41..c5b84f3dd 100644 --- a/src/voice/layerManifest.js +++ b/src/voice/layerManifest.js @@ -34,9 +34,29 @@ const AIRCRAFT_FIELDS = { aircraftClass: ['t'], military: ['f'], onGround: ['f'], + squawk: ['t'], + emergency: ['f'], + emergencyKind: ['t'], + emergencySource: ['t'], + emergencyBroadcast: ['t'], + lastContactAtMs: ['ms'], }; const layers = [ + { + id: 'gnss-interference', + aliases: [ + 'gnss integrity', + 'navigation integrity', + 'low navigation accuracy', + 'gnss interference', + 'gps interference', + 'gps jamming', + 'gnss', + ], + context: false, + noQuery: 'navigation-integrity cells; no countable analyst records', + }, { id: 'flights', track: true,