fix(gnss): expire stale cells on re-enable; pin the unpruned return-after-window path

Re-enabling after a disable longer than the window no longer shows the
old cells until the next refresh. A healthy report arriving 31 minutes
after a degraded one, with no prune in between, starts clean; exactly
30 minutes still counts.
This commit is contained in:
Leo Pechnicki authored and Mazeyar Moeini Feizabadi committed 2026-10-13 17:20:43 +02:00
1 parent fde8b2a64f
commit 7e570a79ad
4 files changed
+43 -6

No files matched your search

-1
View File
@@ -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) {
+5 -1
View File
@@ -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() {
+15
View File
@@ -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)],
});
+23 -4
View File
@@ -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', () => {