diff --git a/scripts/qa-gnss.mjs b/scripts/qa-gnss.mjs index ae7e47a54..d763da134 100644 --- a/scripts/qa-gnss.mjs +++ b/scripts/qa-gnss.mjs @@ -63,7 +63,6 @@ const getOpt = (name, dflt) => { const APP_URL = getOpt('--url', 'http://localhost:4173'); const HEADFUL = argv.includes('--headful'); const LAYER_ID = 'gnss-interference'; -/** Live rows below this cannot reliably fill a 3-aircraft cell. */ const results = []; function record(name, ok, detail) { diff --git a/src/layers/gnss/index.js b/src/layers/gnss/index.js index 52922af4d..0123c4c3f 100644 --- a/src/layers/gnss/index.js +++ b/src/layers/gnss/index.js @@ -122,7 +122,11 @@ export function createGnssIntegrityLayer({ enable() { _enabled = true; - if (_dataSource) _dataSource.show = true; + // Cells left from before a long disable must not reappear past the window. + if (_dataSource) { + expire(); + _dataSource.show = true; + } }, disable() { diff --git a/src/layers/gnss/ownership.test.mjs b/src/layers/gnss/ownership.test.mjs index ba4940b10..ed7308428 100644 --- a/src/layers/gnss/ownership.test.mjs +++ b/src/layers/gnss/ownership.test.mjs @@ -284,6 +284,21 @@ test('cells expire while no view anchor is available', async () => { assert.equal(layer.getStats().error, null); }); +test('re-enabling after a long disable does not show cells past the window', async () => { + const { layer, sources, advance, now } = harness(async () => ({ + rows: threeHealthy(), + fetchedAt: now(), + })); + layer.enable(); + await layer.update(); + assert.equal(sources[0].entities.values.length, 1); + layer.disable(); + advance(31 * MIN); + layer.enable(); + assert.equal(layer.getStats().count, 0); + assert.equal(sources[0].entities.values.length, 0); +}); + const threeCells = () => ({ rows: [row('a1', 50.1, true), row('a2', 50.1, true), row('a3', 50.2, false)], }); diff --git a/src/layers/gnss/records.test.mjs b/src/layers/gnss/records.test.mjs index 273fde10e..593f3e6f3 100644 --- a/src/layers/gnss/records.test.mjs +++ b/src/layers/gnss/records.test.mjs @@ -305,10 +305,29 @@ test('an aircraft returning after the window starts fresh', () => { degradedAt: null, }); // Returning degraded dates the evidence from the new report. - accumulateGnssObservations(store, [report('a9', true)], 0); - accumulateGnssObservations(store, [], 31 * MIN); - accumulateGnssObservations(store, [report('a9', true)], 33 * MIN); - assert.equal(entry(store, 'a9').degradedAt, 33 * MIN); + const again = new Map(); + accumulateGnssObservations(again, [report('a9', true)], 0); + accumulateGnssObservations(again, [], 31 * MIN); + accumulateGnssObservations(again, [report('a9', true)], 33 * MIN); + assert.equal(entry(again, 'a9').degradedAt, 33 * MIN); +}); + +test('an aircraft heard healthy right after the window drops its stale degraded evidence', () => { + // No prune runs in between: the old entry is still in the store when the + // healthy report arrives, so the new report alone must not carry it over. + const store = new Map(); + accumulateGnssObservations(store, [report('a1', true)], 0); + accumulateGnssObservations(store, [report('a1', false)], 31 * MIN); + assert.deepEqual(entry(store, 'a1'), { + cell: gnssCellKey(50.1, 20.1), + seenAt: 31 * MIN, + degradedAt: null, + }); + // Exactly one window after the degraded report it still counts. + const edge = new Map(); + accumulateGnssObservations(edge, [report('a1', true)], 0); + accumulateGnssObservations(edge, [report('a1', false)], 30 * MIN); + assert.equal(entry(edge, 'a1').degradedAt, 0); }); test('rows are dated by the snapshot observation time, so a replay never restarts the window', () => {