fix(flights): don't treat OpenSky's position-less rows as a partial snapshot

OpenSky's state-vector spec allows null longitude/latitude, and every live
/states/all response carries such rows (about 50 of 7,800). The normaliser
rejects them, so admitRecords reported complete: false on effectively every
poll. While a snapshot is incomplete, FlightRecords.absence() retains any
aircraft received in the last five minutes, which meant the stale fade,
MISSING_POLL_LIMIT and the landed fast-cull never ran: vanished and landed
aircraft stayed on the globe at full opacity for up to five minutes.

admitRecords now takes an optional unpositionedId(row). A rejected row it
names is a contact without a position: its id is returned in retainIds and
the row does not count against completeness. OpenSky opts in for rows with an
ICAO24 address and strictly null longitude and latitude; the id comes from
the same helper the normaliser uses. Rows with no usable id, out-of-range or
half-null coordinates, truncated rows and positioned duplicates still make
the snapshot incomplete, and a nonempty feed with no positioned rows is
still malformed.

absence() retains exactly the retainIds aircraft under the existing
five-minute bound, except likely-landed ones, which keep the landed limit.
Everything else ages out normally. Military readsb admission does not opt
in and is unchanged.
This commit is contained in:
Aditya committed 2026-10-13 17:18:28 +05:30
1 parent 8b8ccc9a81
commit b90bcf1829
8 files changed
+118 -9

No files matched your search

+44
View File
@@ -67,6 +67,50 @@ test('mixed-invalid and duplicate admissions are explicitly incomplete; all-inva
assert.equal(openSkySnapshot({ states: [] }).complete, true);
});
test('position-less OpenSky rows are seen, not missing; malformed rows stay incomplete', () => {
// OpenSky's spec allows null longitude/latitude; every live /states/all
// carries such rows.
const unpositioned = ['DEF456', ...aircraft.slice(1)];
unpositioned[5] = null;
unpositioned[6] = null;
const snapshot = openSkySnapshot({ states: [aircraft, unpositioned] });
assert.equal(snapshot.records.length, 1);
assert.equal(snapshot.complete, true);
assert.deepEqual([...snapshot.retainIds], ['def456']);
assert.equal(snapshot.rejectedCount, 1);
const malformed = [...aircraft];
malformed[5] = 181;
const garbled = openSkySnapshot({
states: [aircraft, unpositioned, malformed],
});
assert.equal(garbled.complete, false);
assert.deepEqual([...garbled.retainIds], ['def456']);
// No usable id, only half a position, a truncated row, or no row at all.
for (const row of [
[' ', ...unpositioned.slice(1)],
Object.assign([...unpositioned], { 5: -97 }),
['DEF456'],
null,
])
assert.equal(openSkySnapshot({ states: [aircraft, row] }).complete, false);
// Repeated position-less rows stay complete.
const repeated = openSkySnapshot({
states: [aircraft, unpositioned, unpositioned],
});
assert.equal(repeated.complete, true);
// A feed with no positions at all still cannot prove absence.
assert.throws(() => openSkySnapshot({ states: [unpositioned] }), {
code: 'malformed',
});
// Military readsb admission keeps its existing semantics.
const military = readsbSnapshot(
{ ac: [{ hex: 'abc123', lat: 30, lon: -97 }, { hex: 'def456' }] },
{ observedAtMs: null },
);
assert.equal(military.complete, false);
assert.equal(military.retainIds.size, 0);
});
test('readsb units and position/contact ages are normalized once against the source epoch', () => {
const row = normalizeReadsbAircraft(
{