mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
1607 lines
66 KiB
JavaScript
1607 lines
66 KiB
JavaScript
import { readShellSource } from '../testSupport/readShellSource.mjs';
|
|
import { expandApplicationHtml } from '../../build/application-html.js';
|
|
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { DataLayerManager } from './manager.js';
|
|
import {
|
|
LAYER_STATE_REGISTRY,
|
|
LAYER_STATE_STORAGE_KEY,
|
|
LayerStateCoordinator,
|
|
REGISTERED_LAYER_IDS,
|
|
SHARE_TRACKING_RESTORE_POLICIES,
|
|
createDefaultLayerState,
|
|
decodeLayerStateParams,
|
|
encodeLayerStateParams,
|
|
normalizeLayerState,
|
|
parseStoredLayerState,
|
|
serializeStoredLayerState,
|
|
validateLayerStateRegistry,
|
|
} from './layerState.js';
|
|
import radioLayer from './radio.js';
|
|
import { stampInitialShareGesture } from '../navigationPolicy.js';
|
|
|
|
function deferred() {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
function paramsForLayer(id) {
|
|
if (id === 'flights' || id === 'military') {
|
|
return { models3d: false, models3dMode: 'proximity', irBoost: true };
|
|
}
|
|
if (id === 'satellites') return { catalog: 'core', showPoints: false, showOrbits: false };
|
|
if (id === 'cctv') {
|
|
return {
|
|
coverageMode: 'on',
|
|
showProjection: true,
|
|
autoHop: false,
|
|
autoHopSec: 22,
|
|
selectedCameraId: 'secret-camera',
|
|
calibrationMode: true,
|
|
calibration: { cameraId: 'secret-camera', values: { heading: 12 } },
|
|
};
|
|
}
|
|
if (id === 'radio') {
|
|
return {
|
|
filter: 'all',
|
|
volume: 0.8,
|
|
selectedStationId: 'private-station',
|
|
audioState: 'playing',
|
|
voiceDucked: true,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function fakeLayer(id, hooks = {}) {
|
|
let params = paramsForLayer(id);
|
|
return {
|
|
id,
|
|
name: id,
|
|
icon: '',
|
|
source: 'test',
|
|
async init() { return hooks.init ? hooks.init() : true; },
|
|
async enable() { return hooks.enable ? hooks.enable() : true; },
|
|
async update() { return hooks.update ? hooks.update() : true; },
|
|
async disable() { return hooks.disable ? hooks.disable() : true; },
|
|
...(hooks.resolveTrackingRestoreTarget ? {
|
|
async resolveTrackingRestoreTarget(targetId, options) {
|
|
return hooks.resolveTrackingRestoreTarget(targetId, options);
|
|
},
|
|
} : {}),
|
|
...(params ? {
|
|
setParams(next = {}, options = {}) {
|
|
if (hooks.setParams) {
|
|
const result = hooks.setParams(next, options);
|
|
if (result === false) return false;
|
|
// 'defer' models the production tracking latch: the layer ACCEPTS the
|
|
// request and holds it pending, but getParams() keeps reporting the
|
|
// previous (still-untracked) value until the subject really arrives.
|
|
if (result === 'defer') return true;
|
|
}
|
|
params = { ...params, ...next };
|
|
return true;
|
|
},
|
|
/** Test seam: a deferred subject finally arrives on a later poll. */
|
|
_arrive(next) { params = { ...params, ...next }; },
|
|
getParams() { return { ...params }; },
|
|
...(hooks.cancelPendingTrackingRestore ? {
|
|
cancelPendingTrackingRestore(options) { hooks.cancelPendingTrackingRestore(options); },
|
|
} : {}),
|
|
} : {}),
|
|
};
|
|
}
|
|
|
|
function productionManager(hooksById = {}) {
|
|
const manager = new DataLayerManager({});
|
|
for (const id of REGISTERED_LAYER_IDS) manager.register(fakeLayer(id, hooksById[id] || {}));
|
|
manager.finalizeRegistrations(LAYER_STATE_REGISTRY);
|
|
return manager;
|
|
}
|
|
|
|
/**
|
|
* Deterministic clock + timer queue for the pending-tracking window, so the
|
|
* 90 s / 45 s / 300 s expiries are provable without sleeping.
|
|
*/
|
|
function manualTimers(startMs = 0) {
|
|
let nowMs = startMs;
|
|
let queued = null;
|
|
return {
|
|
now: () => nowMs,
|
|
setTimer: (fn, ms) => { queued = { fn, ms }; return queued; },
|
|
clearTimer: (handle) => { if (queued === handle) queued = null; },
|
|
/** Fire queued polls, advancing the clock, until nothing re-arms. */
|
|
runUntilIdle(maxTicks = 2_000) {
|
|
for (let tick = 0; tick < maxTicks; tick += 1) {
|
|
const pending = queued;
|
|
if (!pending) return;
|
|
queued = null;
|
|
nowMs += pending.ms;
|
|
pending.fn();
|
|
}
|
|
throw new Error('pending-tracking watch never settled');
|
|
},
|
|
};
|
|
}
|
|
|
|
function memoryStorage(initial = null) {
|
|
const values = new Map();
|
|
if (initial !== null) values.set(LAYER_STATE_STORAGE_KEY, initial);
|
|
return {
|
|
writes: [],
|
|
getItem(key) { return values.has(key) ? values.get(key) : null; },
|
|
setItem(key, value) {
|
|
values.set(key, value);
|
|
this.writes.push([key, value]);
|
|
},
|
|
};
|
|
}
|
|
|
|
function shareSink() {
|
|
return {
|
|
provider: null,
|
|
updates: 0,
|
|
setLayerStateProvider(provider) { this.provider = provider; },
|
|
onLayerStateChange() { this.updates += 1; },
|
|
};
|
|
}
|
|
|
|
function encode(state) {
|
|
const params = new URLSearchParams([['v', '2']]);
|
|
encodeLayerStateParams(params, state);
|
|
return params.toString();
|
|
}
|
|
|
|
test('production registry is exact, canonical, and rejects incomplete contracts', async () => {
|
|
assert.equal(validateLayerStateRegistry(), true);
|
|
assert.equal(REGISTERED_LAYER_IDS.length, 21);
|
|
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 21);
|
|
assert.ok(REGISTERED_LAYER_IDS.includes('transit'));
|
|
assert.deepEqual(REGISTERED_LAYER_IDS, [...REGISTERED_LAYER_IDS].sort());
|
|
assert.throws(
|
|
() => validateLayerStateRegistry([...LAYER_STATE_REGISTRY, LAYER_STATE_REGISTRY[0]]),
|
|
/Duplicate layer-state id/,
|
|
);
|
|
|
|
const manager = new DataLayerManager({});
|
|
manager.register(fakeLayer('earthquakes'));
|
|
assert.throws(() => manager.register(fakeLayer('earthquakes')), /Duplicate data-layer id/);
|
|
await assert.rejects(manager.restoreLayerState('earthquakes', { enabled: true }), /finalized/);
|
|
assert.throws(() => manager.finalizeRegistrations([]), /registry mismatch/);
|
|
assert.throws(
|
|
() => manager.finalizeRegistrations([{ id: 'earthquakes', disposition: 'default' }]),
|
|
/Invalid layer serialization disposition/,
|
|
);
|
|
assert.equal(manager.finalizeRegistrations([
|
|
{ id: 'earthquakes', disposition: 'enabled-only' },
|
|
]), true);
|
|
assert.throws(() => manager.register(fakeLayer('radio')), /finalized/);
|
|
assert.throws(() => manager.registerForQa(fakeLayer('radio')), /not authorized/);
|
|
const qaManager = new DataLayerManager({}, { allowQaRegistration: true });
|
|
qaManager.register(fakeLayer('earthquakes'));
|
|
qaManager.finalizeRegistrations([{ id: 'earthquakes', disposition: 'enabled-only' }]);
|
|
qaManager.registerForQa(fakeLayer('radio'));
|
|
assert.equal(qaManager.layers.has('radio'), true);
|
|
assert.equal(await qaManager.unregisterForQa('radio'), true);
|
|
assert.equal(qaManager.layers.has('radio'), false);
|
|
});
|
|
|
|
test('v2 codec distinguishes absent from empty and keeps canonical deterministic ordering', () => {
|
|
assert.equal(decodeLayerStateParams(new URLSearchParams('lat=1&lon=2')), null);
|
|
assert.equal(decodeLayerStateParams(new URLSearchParams('v=1&l=e')), null);
|
|
assert.equal(decodeLayerStateParams(new URLSearchParams('v=3&l=e')), null);
|
|
assert.equal(decodeLayerStateParams(new URLSearchParams('v=2')), null);
|
|
|
|
const empty = decodeLayerStateParams(new URLSearchParams('v=2&l='));
|
|
assert.deepEqual(empty.enabledLayerIds, []);
|
|
assert.deepEqual(empty.options.cctv, {
|
|
coverageMode: 'on',
|
|
showProjection: true,
|
|
autoHop: false,
|
|
});
|
|
|
|
const first = normalizeLayerState({
|
|
enabledLayerIds: ['traffic', 'cctv', 'earthquakes', 'cctv'],
|
|
options: {
|
|
radio: { volume: 0.37, filter: 'news' },
|
|
cctv: { autoHop: true, coverageMode: 'viewshed', showProjection: false },
|
|
flights: { models3dMode: 'all', models3d: true },
|
|
satellites: { catalog: 'dense' },
|
|
},
|
|
});
|
|
const second = normalizeLayerState({
|
|
enabledLayerIds: ['earthquakes', 'cctv', 'traffic'],
|
|
options: {
|
|
satellites: { catalog: 'dense' },
|
|
flights: { models3d: true, models3dMode: 'all' },
|
|
cctv: { showProjection: false, coverageMode: 'viewshed', autoHop: true },
|
|
radio: { filter: 'news', volume: 0.37 },
|
|
},
|
|
});
|
|
assert.equal(encode(first), encode(second));
|
|
assert.deepEqual(decodeLayerStateParams(new URLSearchParams(encode(first))), first);
|
|
});
|
|
|
|
test('unknown enabled-layer tokens reject the payload instead of becoming an empty set', () => {
|
|
assert.equal(decodeLayerStateParams(new URLSearchParams('v=2&l=unknown')), null);
|
|
assert.equal(decodeLayerStateParams(new URLSearchParams('v=2&l=c.unknown')), null);
|
|
});
|
|
|
|
test('Nepal event and locator have distinct enabled-only share tokens', () => {
|
|
const decoded = decodeLayerStateParams(new URLSearchParams('v=2&l=h.z'));
|
|
assert.deepEqual(decoded.enabledLayerIds, ['bhote-koshi-2026', 'bhote-koshi-locator']);
|
|
assert.ok(encode(decoded).includes('l=h.z'));
|
|
});
|
|
|
|
test('unknown and forbidden option fields are ignored while missing options use codec defaults', () => {
|
|
const decoded = decodeLayerStateParams(new URLSearchParams(
|
|
'v=2&l=c.e&lo=c.c.v_c.z.1_z.c.1_f.e.1_f.m.a_r.f.n_r.v.35',
|
|
));
|
|
assert.deepEqual(decoded.enabledLayerIds, ['cctv', 'earthquakes']);
|
|
assert.deepEqual(decoded.options.cctv, {
|
|
coverageMode: 'viewshed',
|
|
showProjection: true,
|
|
autoHop: false,
|
|
});
|
|
assert.deepEqual(decoded.options.flights, {
|
|
models3d: true,
|
|
models3dMode: 'all',
|
|
selectedFlightsTrackingId: null,
|
|
selectedMilitaryTrackingId: null,
|
|
});
|
|
assert.deepEqual(decoded.options.radio, { filter: 'news', volume: 0.35 });
|
|
|
|
const raw = normalizeLayerState({
|
|
enabledLayerIds: ['cctv', 'unknown-layer'],
|
|
options: {
|
|
cctv: {
|
|
coverageMode: 'off',
|
|
selectedCameraId: 'private-camera',
|
|
calibrationMode: true,
|
|
calibration: { secret: 'do-not-share' },
|
|
autoHopSec: 99,
|
|
},
|
|
flights: { models3d: true, irBoost: true },
|
|
satellites: { catalog: 'dense', showPoints: false, showOrbits: false },
|
|
radio: {
|
|
filter: 'genre:ambient',
|
|
volume: 0.66,
|
|
selectedStationId: 'private-station',
|
|
audioState: 'playing',
|
|
voiceDucked: true,
|
|
},
|
|
},
|
|
});
|
|
const serialized = `${encode(raw)} ${serializeStoredLayerState(raw)}`;
|
|
for (const forbidden of [
|
|
'private-camera', 'calibration', 'autoHopSec', 'irBoost', 'showPoints',
|
|
'showOrbits', 'private-station', 'audioState', 'voiceDucked', 'secret',
|
|
]) assert.equal(serialized.includes(forbidden), false, forbidden);
|
|
});
|
|
|
|
test('Radio genre ids with spaces and ampersands round-trip through v2', () => {
|
|
for (const filter of ['genre:hip hop', 'genre:r&b']) {
|
|
const state = normalizeLayerState({
|
|
enabledLayerIds: ['radio'],
|
|
options: { radio: { filter, volume: 0.5 } },
|
|
});
|
|
const decoded = decodeLayerStateParams(new URLSearchParams(encode(state)));
|
|
assert.equal(decoded.options.radio.filter, filter);
|
|
}
|
|
});
|
|
|
|
test('mirrored option owners decode into the owner owner-options bucket', () => {
|
|
const decoded = decodeLayerStateParams(new URLSearchParams(
|
|
'v=2&l=f.m&lo=m.u.xyz',
|
|
));
|
|
assert.deepEqual(decoded.enabledLayerIds, ['flights', 'military']);
|
|
assert.equal(decoded.options.flights.selectedFlightsTrackingId, null);
|
|
assert.equal(decoded.options.flights.selectedMilitaryTrackingId, 'xyz');
|
|
});
|
|
|
|
test('ambiguous cross-family tracking IDs fail closed instead of racing feed arrival', () => {
|
|
const decoded = decodeLayerStateParams(new URLSearchParams(
|
|
'v=2&l=f.m.s&lo=f.t.flightA_f.u.militaryB_s.t.25544',
|
|
));
|
|
assert.deepEqual(decoded.enabledLayerIds, ['flights', 'military', 'satellites']);
|
|
assert.equal(decoded.options.flights.selectedFlightsTrackingId, null);
|
|
assert.equal(decoded.options.flights.selectedMilitaryTrackingId, null);
|
|
assert.equal(decoded.options.satellites.selectedSatTrackingId, null);
|
|
|
|
const canonical = encodeLayerStateParams(new URLSearchParams('v=2'), decoded);
|
|
assert.equal(canonical.has('lo'), false);
|
|
});
|
|
|
|
test('compact URL omits absent-meaning option state and still resolves to it', () => {
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights', 'satellites'];
|
|
// Spelled out rather than reused from createDefaultLayerState() on purpose:
|
|
// this is the ledger of what an OMITTED token means, so changing any of these
|
|
// fails HERE and forces the change to be acknowledged.
|
|
//
|
|
// Note this is the absent-meaning ledger, NOT the default ledger — the two
|
|
// diverged for `models3d` on 2026-08-22. Its default moved to `true`, but an
|
|
// omitted `e` still means `false` because that is what schema v2 has always
|
|
// meant to the links already in the wild, so `false` is what belongs in a
|
|
// URL-omission test. The default's own value is pinned in the fresh-boot test
|
|
// below, and the divergence itself in the two codec tests above.
|
|
state.options.flights = { models3d: false, models3dMode: 'proximity', selectedFlightsTrackingId: null, selectedMilitaryTrackingId: null };
|
|
state.options.satellites = { catalog: 'core', showPoints: true, showOrbits: true, selectedSatTrackingId: null };
|
|
const params = encodeLayerStateParams(new URLSearchParams('v=2'), state);
|
|
assert.equal(params.has('lo'), false);
|
|
const roundTrip = decodeLayerStateParams(params);
|
|
assert.equal(roundTrip.options.flights.models3d, false);
|
|
assert.equal(roundTrip.options.flights.models3dMode, 'proximity');
|
|
assert.deepEqual(roundTrip.options.flights.selectedFlightsTrackingId, null);
|
|
assert.deepEqual(roundTrip.options.flights.selectedMilitaryTrackingId, null);
|
|
assert.deepEqual(roundTrip.options.satellites.selectedSatTrackingId, null);
|
|
});
|
|
|
|
test('a fresh boot starts 3D aircraft ON in proximity — codec, both layers, and the rail agree', async () => {
|
|
// Owner directive 2026-08-22: the DISPLAY-rail 3D toggle defaults ON with mode
|
|
// `proximity`, because proximity is itself the budget — models materialize only
|
|
// below the fleet altitude ceiling and only for the nearest MODEL_MAX in view,
|
|
// so "on" costs nothing at globe scale and `all` stays a deliberate opt-in.
|
|
//
|
|
// The reason this is one test rather than four is the early return in `start()`
|
|
// below: with no share payload and no stored state, restoration NEVER RUNS, so
|
|
// nothing pushes the codec default into the layers. Four independent
|
|
// initializers decide what a first-run operator actually sees, and changing any
|
|
// one alone ships a lit button over an unarmed layer, or an armed layer under a
|
|
// dark button. Pinning them together is what makes "state and UI agree" a fact.
|
|
const defaults = createDefaultLayerState().options.flights;
|
|
assert.equal(defaults.models3d, true, 'the durable default is 3D ON');
|
|
assert.equal(defaults.models3dMode, 'proximity', 'and proximity, never all');
|
|
|
|
const paramsCalls = [];
|
|
const manager = productionManager({
|
|
flights: { setParams: (params) => { paramsCalls.push({ ...params }); return true; } },
|
|
military: { setParams: (params) => { paramsCalls.push({ ...params }); return true; } },
|
|
});
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
await coordinator.start();
|
|
assert.equal(coordinator.source, 'defaults');
|
|
assert.equal(coordinator.getDurableState().options.flights.models3d, true);
|
|
assert.equal(coordinator.getDurableState().options.flights.models3dMode, 'proximity');
|
|
assert.deepEqual(paramsCalls, [],
|
|
'a fresh boot restores nothing — which is exactly why the module initializers below must match');
|
|
coordinator.destroy();
|
|
|
|
// The other three surfaces, read from source, because each is the literal a
|
|
// first-run session actually boots from.
|
|
const { readFile } = await import('node:fs/promises');
|
|
for (const name of ['flights.js', 'militaryFlights.js']) {
|
|
const source = readLayerSource(new URL(`./${name}`, import.meta.url));
|
|
assert.match(source, /^\s*(?:let |flightState\.)_models3dEnabled = true;$/m,
|
|
`${name}: the fleet starts armed, matching the codec default`);
|
|
assert.match(source, /^\s*(?:let |flightState\.)_models3dMode = 'proximity';/m,
|
|
`${name}: and starts in proximity, matching the codec default`);
|
|
}
|
|
const ui = await readShellSource();
|
|
assert.match(ui, /^\s*this\.(?:flightState\.)?_models3dEnabled = true;$/m,
|
|
'ui.js: the DISPLAY rail believes 3D is on before any layer-state sync arrives');
|
|
assert.match(ui, /this\.(?:flightState\.)?_models3dMode = 'proximity';/,
|
|
'ui.js: and believes the mode is proximity');
|
|
const html = expandApplicationHtml(await readFile(new URL('../../index.html', import.meta.url), 'utf8'));
|
|
assert.match(html, /class="pp-toggle-btn active" id="models3d-toggle" aria-pressed="true"/,
|
|
'index.html: the 3D button paints lit on first paint, before ui.js runs — and says so');
|
|
assert.match(ui, /this\._models3dBtn\?\.setAttribute\(\s*'aria-pressed',\s*String\(this\.(?:flightState\.)?_models3dEnabled\),?\s*\)/,
|
|
'ui.js: and keeps aria-pressed synchronized, so the lit state is not colour-only');
|
|
assert.match(html, /class="pp-slider-row visible" id="models3d-mode-row"/,
|
|
'index.html: and the Proximity/All row paints open with it');
|
|
assert.match(html, /id="models3d-mode-proximity"[^>]*aria-checked="true"/,
|
|
'index.html: Proximity is the selected mode in the markup');
|
|
});
|
|
|
|
test('a v2 link written before the flip still means what its author saw: 3D OFF', () => {
|
|
// The regression this pins, caught in review: schema v2 shipped with 3D OFF as
|
|
// the OMITTED default, so every link already in the wild says "off" by saying
|
|
// nothing. Moving the default without moving the codec turned all of them ON —
|
|
// the same omission meaning two different things inside one schema version.
|
|
//
|
|
// `v=2&l=f` is the canonical parent-era link. It must decode OFF forever.
|
|
const parentEra = decodeLayerStateParams(new URLSearchParams('v=2&l=f'));
|
|
assert.equal(parentEra.options.flights.models3d, false,
|
|
'an omitted `e` is a v2 author saying OFF, not "whatever today\'s default is"');
|
|
|
|
// The nastier one: an OFF link that DID remember a non-default mode. Reading
|
|
// the omission as the new default would resurrect it as ON + All.
|
|
const parentEraAll = decodeLayerStateParams(new URLSearchParams('v=2&l=f&lo=f.m.a'));
|
|
assert.equal(parentEraAll.options.flights.models3d, false);
|
|
assert.equal(parentEraAll.options.flights.models3dMode, 'all',
|
|
'the mode it really carried survives; only the omission is read as OFF');
|
|
|
|
// A parent-era link that carried 3D ON wrote it explicitly (ON was non-default
|
|
// then), so it still restores ON. Both eras round-trip.
|
|
assert.equal(
|
|
decodeLayerStateParams(new URLSearchParams('v=2&l=f&lo=f.e.1')).options.flights.models3d,
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('the new default is written EXPLICITLY, so no omission is ambiguous', () => {
|
|
// Because an absent `e` is pinned to the historical OFF above, the new ON
|
|
// default cannot ride in on the omission — it has to be emitted. This is the
|
|
// other half of the same invariant: break it and fresh ON links silently become
|
|
// OFF links for their recipients.
|
|
const on = createDefaultLayerState();
|
|
on.enabledLayerIds = ['flights'];
|
|
assert.equal(on.options.flights.models3d, true, 'precondition: ON is the default');
|
|
const onEncoded = encode(on);
|
|
assert.match(onEncoded, /(^|&)lo=[^&]*f\.e\.1/,
|
|
'a fresh ON link states ON outright rather than relying on omission');
|
|
assert.equal(decodeLayerStateParams(new URLSearchParams(onEncoded)).options.flights.models3d, true);
|
|
|
|
// And OFF is the omission, matching what v2 already meant — one meaning, one
|
|
// encoding, across both eras.
|
|
const off = createDefaultLayerState();
|
|
off.enabledLayerIds = ['flights'];
|
|
off.options.flights = { ...off.options.flights, models3d: false };
|
|
const offEncoded = encode(off);
|
|
assert.doesNotMatch(offEncoded, /f\.e\./,
|
|
'OFF is the absent token, exactly as it was before the flip');
|
|
assert.equal(decodeLayerStateParams(new URLSearchParams(offEncoded)).options.flights.models3d, false);
|
|
|
|
// An interim build briefly emitted `f.e.0`; it must still decode as OFF.
|
|
assert.equal(
|
|
decodeLayerStateParams(new URLSearchParams('v=2&l=f&lo=f.e.0')).options.flights.models3d,
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('a share link that carries 3D OFF still restores OFF at both aircraft layers', async () => {
|
|
// The 2026-08-22 default governs FRESH sessions only. It must never overwrite a
|
|
// choice a link states — and after the codec fix above, OFF is stated by the
|
|
// absent token, which is what v2 always meant.
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights', 'military'];
|
|
state.options.flights = { ...state.options.flights, models3d: false };
|
|
const encoded = encode(state);
|
|
|
|
const decoded = decodeLayerStateParams(new URLSearchParams(encoded));
|
|
assert.equal(decoded.options.flights.models3d, false, 'the link decodes back to OFF');
|
|
assert.equal(decoded.options.flights.models3dMode, 'proximity');
|
|
|
|
// And the restore really pushes OFF at both aircraft layers — `military`
|
|
// mirrors the `flights` option owner, so one shared link disarms both.
|
|
const calls = { flights: [], military: [] };
|
|
const manager = productionManager({
|
|
flights: { setParams: (params) => { calls.flights.push({ ...params }); return true; } },
|
|
military: { setParams: (params) => { calls.military.push({ ...params }); return true; } },
|
|
});
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
await coordinator.start({ shareLayerState: decoded, shareCreatedAtMs: 1_000 });
|
|
assert.deepEqual(calls.flights, [{ models3d: false, models3dMode: 'proximity' }]);
|
|
assert.deepEqual(calls.military, [{ models3d: false, models3dMode: 'proximity' }]);
|
|
assert.equal(coordinator.getDurableState().options.flights.models3d, false);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('explicit navigation tracking clear removes the active durable ID from the generated hash', async () => {
|
|
const manager = productionManager();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
await coordinator.start();
|
|
await manager.setEnabled('flights', true, { origin: 'user' });
|
|
manager.setLayerParams('flights', { selectedFlightsTrackingId: 'abc123' }, { origin: 'user' });
|
|
assert.equal(coordinator.getDurableState().options.flights.selectedFlightsTrackingId, 'abc123');
|
|
assert.equal(encode(coordinator.getDurableState()).includes('abc123'), true);
|
|
manager.setLayerParams('flights', { selectedFlightsTrackingId: null }, { origin: 'tool' });
|
|
assert.equal(coordinator.getDurableState().options.flights.selectedFlightsTrackingId, null);
|
|
assert.equal(encode(coordinator.getDurableState()).includes('abc123'), false);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('a direct Context selection promotes its owned tracker layer before persisting the ID', async () => {
|
|
const manager = productionManager();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
await coordinator.start();
|
|
await manager.setEnabled('flights', true, { origin: 'programmatic' });
|
|
manager.setLayerParams(
|
|
'flights',
|
|
{ selectedFlightsTrackingId: 'context-flight' },
|
|
{ origin: 'programmatic' },
|
|
);
|
|
assert.equal(manager.adoptLayerVisibility('flights', true, {
|
|
origin: 'user',
|
|
adoptedFromSelection: true,
|
|
}), true);
|
|
assert.equal(manager.adoptLayerParams(
|
|
'flights',
|
|
{ selectedFlightsTrackingId: 'context-flight' },
|
|
{ origin: 'user' },
|
|
), true);
|
|
const durable = coordinator.getDurableState();
|
|
assert.equal(durable.enabledLayerIds.includes('flights'), true);
|
|
assert.equal(durable.options.flights.selectedFlightsTrackingId, 'context-flight');
|
|
assert.equal(encode(durable).includes('context-flight'), true);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('explicit Flight to Satellite replacement keeps the new Satellite ID durable', async () => {
|
|
const manager = productionManager();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
await coordinator.start();
|
|
await manager.setEnabled('flights', true, { origin: 'user' });
|
|
await manager.setEnabled('satellites', true, { origin: 'user' });
|
|
manager.setLayerParams('flights', { selectedFlightsTrackingId: 'abc123' }, { origin: 'user' });
|
|
assert.equal(coordinator.getDurableState().options.flights.selectedFlightsTrackingId, 'abc123');
|
|
|
|
// UI replacement ordering clears the prior family before a direct tracker
|
|
// publishes its already-applied selection through adoptLayerParams().
|
|
manager.setLayerParams('flights', { selectedFlightsTrackingId: null }, { origin: 'user' });
|
|
manager.setLayerParams('satellites', { selectedSatTrackingId: 25544 }, { origin: 'programmatic' });
|
|
assert.equal(manager.adoptLayerParams(
|
|
'satellites',
|
|
{ selectedSatTrackingId: 25544 },
|
|
{ origin: 'user' },
|
|
), true);
|
|
|
|
const durable = coordinator.getDurableState();
|
|
assert.equal(durable.options.flights.selectedFlightsTrackingId, null);
|
|
assert.equal(durable.options.flights.selectedMilitaryTrackingId, null);
|
|
assert.equal(durable.options.satellites.selectedSatTrackingId, 25544);
|
|
assert.equal(new URLSearchParams(encode(durable)).get('lo')?.includes('s.t.25544'), true);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('explicit layer OFF clears its durable selected entity and prevents later resurrection', async () => {
|
|
const manager = productionManager();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
await coordinator.start();
|
|
await manager.setEnabled('flights', true, { origin: 'user' });
|
|
manager.setLayerParams('flights', { selectedFlightsTrackingId: 'abc123' }, { origin: 'user' });
|
|
assert.equal(coordinator.getDurableState().options.flights.selectedFlightsTrackingId, 'abc123');
|
|
await manager.setEnabled('flights', false, { origin: 'user' });
|
|
assert.equal(coordinator.getDurableState().options.flights.selectedFlightsTrackingId, null);
|
|
assert.equal(Boolean(
|
|
new URLSearchParams(encode(coordinator.getDurableState())).get('lo')?.includes('f.t.'),
|
|
), false);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('unrelated explicit params retain the family live active ID while cancelling only pending state', async () => {
|
|
const manager = productionManager();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
await coordinator.start();
|
|
assert.equal(manager.setLayerParams('flights', {
|
|
selectedFlightsTrackingId: 'active001',
|
|
}, { origin: 'programmatic' }), true);
|
|
await manager.setEnabled('flights', true, { origin: 'user' });
|
|
assert.equal(manager.setLayerParams('flights', {
|
|
models3d: true,
|
|
}, { origin: 'user' }), true);
|
|
assert.equal(
|
|
coordinator.getDurableState().options.flights.selectedFlightsTrackingId,
|
|
'active001',
|
|
);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('stored state is deterministic, rejects other versions, and stays within a tested URL bound', () => {
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = [...REGISTERED_LAYER_IDS].reverse();
|
|
state.options.flights = { models3d: true, models3dMode: 'all' };
|
|
state.options.satellites = { catalog: 'dense' };
|
|
state.options.cctv = { coverageMode: 'viewshed', showProjection: false, autoHop: true };
|
|
state.options.radio = { filter: 'genre:experimental-ambient', volume: 1 };
|
|
const stored = serializeStoredLayerState(state);
|
|
assert.deepEqual(parseStoredLayerState(stored), normalizeLayerState(state));
|
|
assert.equal(parseStoredLayerState('{"v":1,"l":[]}'), null);
|
|
assert.ok(encode(state).length < 420, encode(state));
|
|
});
|
|
|
|
test('restore applies sanitized params after init and before enable', async () => {
|
|
const order = [];
|
|
const manager = productionManager({
|
|
flights: {
|
|
init: () => { order.push('init'); return true; },
|
|
setParams: () => { order.push('params'); return true; },
|
|
enable: () => { order.push('enable'); return true; },
|
|
update: () => { order.push('update'); return true; },
|
|
},
|
|
});
|
|
const outcome = await manager.restoreLayerState('flights', {
|
|
enabled: true,
|
|
params: { models3d: true, models3dMode: 'all' },
|
|
}, { origin: 'share-restore' });
|
|
assert.deepEqual(order, ['init', 'params', 'enable', 'update']);
|
|
assert.equal(outcome.succeeded, true);
|
|
assert.equal(outcome.persistenceWrite, false);
|
|
assert.deepEqual(outcome.appliedOptions, { models3d: true, models3dMode: 'all' });
|
|
});
|
|
|
|
test('manager forwards passive restore origin into module parameter application', async () => {
|
|
const seen = [];
|
|
const manager = productionManager({
|
|
flights: {
|
|
setParams: (_params, options) => { seen.push(options); },
|
|
},
|
|
});
|
|
await manager.restoreLayerState('flights', {
|
|
enabled: false,
|
|
params: { selectedFlightsTrackingId: 'abc123' },
|
|
}, { origin: 'share-restore' });
|
|
assert.deepEqual(seen, [{ origin: 'share-restore', paramsIntentEpoch: 1 }]);
|
|
});
|
|
|
|
test('explicit manager params and visibility revoke module-owned pending tracking restore', async () => {
|
|
const cancellations = [];
|
|
const manager = productionManager({
|
|
flights: {
|
|
cancelPendingTrackingRestore: (options) => { cancellations.push(options); },
|
|
},
|
|
});
|
|
|
|
await manager.restoreLayerState('flights', {
|
|
enabled: false,
|
|
params: { selectedFlightsTrackingId: 'late001' },
|
|
}, { origin: 'share-restore' });
|
|
assert.deepEqual(cancellations, [], 'passive restoration cannot cancel itself');
|
|
|
|
manager.setLayerParams('flights', { models3d: true }, { origin: 'voice' });
|
|
await manager.setEnabled('flights', false, { origin: 'user' });
|
|
assert.deepEqual(cancellations, [
|
|
{ origin: 'voice', reason: 'explicit-params' },
|
|
{ origin: 'user', reason: 'explicit-visibility' },
|
|
]);
|
|
});
|
|
|
|
test('share payload wins over local, passive restore writes nothing, and explicit success persists', async () => {
|
|
const local = createDefaultLayerState();
|
|
local.enabledLayerIds = ['traffic'];
|
|
const storage = memoryStorage(serializeStoredLayerState(local));
|
|
const manager = productionManager();
|
|
const share = shareSink();
|
|
const coordinator = new LayerStateCoordinator(manager, share, { storage });
|
|
const explicitEmpty = createDefaultLayerState();
|
|
await coordinator.start({ shareLayerState: explicitEmpty });
|
|
|
|
assert.equal(coordinator.source, 'share');
|
|
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, []);
|
|
assert.deepEqual(storage.writes, []);
|
|
assert.equal(share.provider().enabledLayerIds.length, 0);
|
|
|
|
await manager.setEnabled('earthquakes', true, { origin: 'user' });
|
|
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, ['earthquakes']);
|
|
assert.equal(storage.writes.length, 1);
|
|
|
|
await manager.setEnabled('traffic', true, { origin: 'scene' });
|
|
assert.equal(storage.writes.length, 1);
|
|
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, ['earthquakes']);
|
|
|
|
await manager.setEnabled('traffic', true, { origin: 'tool' });
|
|
assert.equal(storage.writes.length, 2);
|
|
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, ['earthquakes', 'traffic']);
|
|
|
|
manager.setLayerParams('cctv', { selectedCameraId: 'private-camera' }, { origin: 'user' });
|
|
assert.equal(storage.writes.length, 2);
|
|
assert.deepEqual(coordinator.getDurableState().options.cctv, {
|
|
coverageMode: 'on',
|
|
showProjection: true,
|
|
autoHop: false,
|
|
});
|
|
|
|
manager.setLayerParams('cctv', { coverageMode: 'off' }, { origin: 'scene' });
|
|
assert.equal(storage.writes.length, 2);
|
|
assert.equal(coordinator.getDurableState().options.cctv.coverageMode, 'on');
|
|
|
|
manager.setLayerParams('cctv', { coverageMode: 'viewshed' }, { origin: 'voice' });
|
|
assert.equal(storage.writes.length, 3);
|
|
assert.equal(coordinator.getDurableState().options.cctv.coverageMode, 'viewshed');
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('absent share payload restores local state without rewriting it', async () => {
|
|
const local = createDefaultLayerState();
|
|
local.enabledLayerIds = ['earthquakes', 'radio'];
|
|
local.options.radio = { filter: 'talk', volume: 0.42 };
|
|
const storage = memoryStorage(serializeStoredLayerState(local));
|
|
const manager = productionManager();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage });
|
|
const results = await coordinator.start();
|
|
assert.equal(coordinator.source, 'local');
|
|
assert.equal(manager.isEnabled('earthquakes'), true);
|
|
assert.equal(manager.isEnabled('radio'), true);
|
|
assert.deepEqual(manager.getLayerParams('radio'), {
|
|
filter: 'talk',
|
|
volume: 0.42,
|
|
selectedStationId: 'private-station',
|
|
audioState: 'playing',
|
|
voiceDucked: true,
|
|
});
|
|
assert.equal(results.every((result) => result.persistenceWrite === false), true);
|
|
assert.deepEqual(storage.writes, []);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('historical share payload suppresses unrelated local layer preferences', async () => {
|
|
const local = createDefaultLayerState();
|
|
local.enabledLayerIds = ['traffic', 'radio'];
|
|
const storage = memoryStorage(serializeStoredLayerState(local));
|
|
const manager = productionManager();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage });
|
|
await coordinator.start({ allowLocalState: false });
|
|
assert.equal(coordinator.source, 'legacy-share');
|
|
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, []);
|
|
assert.equal(manager.getEnabledLayerIds().size, 0);
|
|
assert.deepEqual(storage.writes, []);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('one layer failure is isolated from sibling restoration', async () => {
|
|
const manager = productionManager({
|
|
cctv: { init: () => { throw new Error('missing key'); } },
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['cctv', 'earthquakes'];
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
const results = await coordinator.start({ shareLayerState: state });
|
|
assert.equal(manager.isEnabled('cctv'), false);
|
|
assert.equal(manager.isEnabled('earthquakes'), true);
|
|
const failed = results.find((result) => result.layerId === 'cctv');
|
|
assert.equal(failed.succeeded, false);
|
|
assert.equal(failed.phase, 'init');
|
|
assert.equal(failed.errorClass, 'Error');
|
|
assert.equal(failed.error, 'missing key');
|
|
assert.equal(results.find((result) => result.layerId === 'earthquakes').succeeded, true);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('later explicit visibility during delayed restore wins for that layer only', async () => {
|
|
const gate = deferred();
|
|
const manager = productionManager();
|
|
const storage = memoryStorage();
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['radio', 'earthquakes'];
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
|
storage,
|
|
restoreGate: gate.promise,
|
|
});
|
|
const restore = coordinator.start({ shareLayerState: state });
|
|
await manager.setEnabled('radio', false, { origin: 'user' });
|
|
gate.resolve();
|
|
const results = await restore;
|
|
assert.equal(manager.isEnabled('radio'), false);
|
|
assert.equal(manager.isEnabled('earthquakes'), true);
|
|
assert.equal(results.find((result) => result.layerId === 'radio').cancellationReason, 'superseded');
|
|
assert.equal(storage.writes.length, 1);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('share restore waits for a superseding same-target visibility successor', async () => {
|
|
const firstUpdateStarted = deferred();
|
|
const releaseFirstUpdate = deferred();
|
|
const secondUpdateStarted = deferred();
|
|
const releaseSecondUpdate = deferred();
|
|
let updateCount = 0;
|
|
const manager = productionManager({
|
|
radio: {
|
|
update: async () => {
|
|
updateCount += 1;
|
|
if (updateCount === 1) {
|
|
firstUpdateStarted.resolve();
|
|
await releaseFirstUpdate.promise;
|
|
} else if (updateCount === 2) {
|
|
secondUpdateStarted.resolve();
|
|
await releaseSecondUpdate.promise;
|
|
}
|
|
return true;
|
|
},
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['radio'];
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
|
|
let restoreSettled = false;
|
|
const restore = coordinator.start({ shareLayerState: state })
|
|
.then((result) => { restoreSettled = true; return result; });
|
|
await firstUpdateStarted.promise;
|
|
let successorSettled = false;
|
|
const explicitOn = manager.setEnabled('radio', true, { origin: 'user' })
|
|
.then((result) => { successorSettled = true; return result; });
|
|
releaseFirstUpdate.resolve();
|
|
await secondUpdateStarted.promise;
|
|
await Promise.resolve();
|
|
assert.equal(restoreSettled, false, 'aggregate must wait for the authoritative successor');
|
|
assert.equal(successorSettled, false);
|
|
|
|
releaseSecondUpdate.resolve();
|
|
assert.equal(await explicitOn, true);
|
|
const results = await restore;
|
|
const radio = results.find((result) => result.layerId === 'radio');
|
|
assert.equal(radio.cancellationReason, 'superseded');
|
|
assert.equal(radio.successorEnabled, true);
|
|
assert.equal(radio.authoritativeIntentEpoch, radio.successorIntentEpoch);
|
|
assert.equal(radio.authoritativeEnabled, true);
|
|
assert.equal(radio.succeeded, true);
|
|
assert.equal(manager.getLayerLifecycleState('radio').lifecycleState, 'enabled');
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('share restore waits for a superseding opposite-target visibility successor', async () => {
|
|
const updateStarted = deferred();
|
|
const releaseUpdate = deferred();
|
|
const disableStarted = deferred();
|
|
const releaseDisable = deferred();
|
|
const manager = productionManager({
|
|
radio: {
|
|
update: async () => {
|
|
updateStarted.resolve();
|
|
await releaseUpdate.promise;
|
|
return true;
|
|
},
|
|
disable: async () => {
|
|
disableStarted.resolve();
|
|
await releaseDisable.promise;
|
|
return true;
|
|
},
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['radio'];
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
|
|
let restoreSettled = false;
|
|
const restore = coordinator.start({ shareLayerState: state })
|
|
.then((result) => { restoreSettled = true; return result; });
|
|
await updateStarted.promise;
|
|
const explicitOff = manager.setEnabled('radio', false, { origin: 'user' });
|
|
releaseUpdate.resolve();
|
|
await disableStarted.promise;
|
|
await Promise.resolve();
|
|
assert.equal(restoreSettled, false, 'aggregate must wait for the OFF successor to settle');
|
|
|
|
releaseDisable.resolve();
|
|
assert.equal(await explicitOff, true);
|
|
const results = await restore;
|
|
const radio = results.find((result) => result.layerId === 'radio');
|
|
assert.equal(radio.cancellationReason, 'superseded');
|
|
assert.equal(radio.successorEnabled, false);
|
|
assert.equal(radio.authoritativeIntentEpoch, radio.successorIntentEpoch);
|
|
assert.equal(radio.authoritativeEnabled, false);
|
|
assert.equal(radio.succeeded, false, 'the newer OFF must not count as successful shared ON');
|
|
assert.equal(manager.getLayerLifecycleState('radio').lifecycleState, 'disabled');
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('later explicit params during init replace options without cancelling visibility', async () => {
|
|
const initGate = deferred();
|
|
const initStarted = deferred();
|
|
const manager = productionManager({
|
|
flights: {
|
|
init: async () => {
|
|
initStarted.resolve();
|
|
await initGate.promise;
|
|
return true;
|
|
},
|
|
},
|
|
});
|
|
const storage = memoryStorage();
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights'];
|
|
state.options.flights = {
|
|
models3d: true,
|
|
models3dMode: 'all',
|
|
selectedFlightsTrackingId: 'late001',
|
|
};
|
|
const share = shareSink();
|
|
const coordinator = new LayerStateCoordinator(manager, share, { storage });
|
|
const restore = coordinator.start({ shareLayerState: state });
|
|
await initStarted.promise;
|
|
assert.equal(manager.setLayerParams('flights', {
|
|
models3d: false,
|
|
models3dMode: 'proximity',
|
|
}, { origin: 'user' }), true);
|
|
initGate.resolve();
|
|
const results = await restore;
|
|
assert.equal(manager.isEnabled('flights'), true);
|
|
assert.deepEqual(coordinator.getDurableState().options.flights, {
|
|
models3d: false,
|
|
models3dMode: 'proximity',
|
|
selectedFlightsTrackingId: null,
|
|
selectedMilitaryTrackingId: null,
|
|
});
|
|
assert.equal(results.find((result) => result.layerId === 'flights').succeeded, true);
|
|
assert.equal(storage.writes.length, 1);
|
|
assert.equal(
|
|
new URLSearchParams(encode(share.provider())).get('lo')?.includes('f.t.late001') || false,
|
|
false,
|
|
);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('explicit navigation preserves unrelated visibility and option restoration', async () => {
|
|
const initGate = deferred();
|
|
const initStarted = deferred();
|
|
const paramsCalls = [];
|
|
const manager = productionManager({
|
|
flights: {
|
|
init: async () => {
|
|
initStarted.resolve();
|
|
await initGate.promise;
|
|
return true;
|
|
},
|
|
setParams: (params) => { paramsCalls.push(params); return true; },
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights'];
|
|
state.options.flights = {
|
|
models3d: true,
|
|
models3dMode: 'all',
|
|
selectedFlightsTrackingId: 'stale-flight',
|
|
selectedMilitaryTrackingId: null,
|
|
};
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage: memoryStorage() });
|
|
const restore = coordinator.start({ shareLayerState: state });
|
|
await initStarted.promise;
|
|
// Navigation owns only camera and pending entity selection. It must not
|
|
// revoke the layer visibility or unrelated display-option lanes.
|
|
initGate.resolve();
|
|
const results = await restore;
|
|
assert.equal(manager.isEnabled('flights'), true);
|
|
assert.deepEqual(paramsCalls, [{
|
|
models3d: true,
|
|
models3dMode: 'all',
|
|
}]);
|
|
assert.equal(results.find((result) => result.layerId === 'flights').succeeded, true);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('startup gesture clears passive Follow but preserves slow layer and display options', async () => {
|
|
const flightsInit = deferred();
|
|
const flightsStarted = deferred();
|
|
const radioInit = deferred();
|
|
const radioStarted = deferred();
|
|
const manager = productionManager({
|
|
flights: {
|
|
init: async () => {
|
|
flightsStarted.resolve();
|
|
await flightsInit.promise;
|
|
return true;
|
|
},
|
|
},
|
|
radio: {
|
|
init: async () => {
|
|
radioStarted.resolve();
|
|
await radioInit.promise;
|
|
return true;
|
|
},
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights', 'radio'];
|
|
state.options.flights = {
|
|
models3d: true,
|
|
models3dMode: 'all',
|
|
selectedFlightsTrackingId: 'late-share',
|
|
selectedMilitaryTrackingId: null,
|
|
};
|
|
state.options.radio = { filter: 'genre:r&b', volume: 0.42 };
|
|
const storage = memoryStorage();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage });
|
|
const restore = coordinator.start({ shareLayerState: state });
|
|
await Promise.all([flightsStarted.promise, radioStarted.promise]);
|
|
|
|
let cameraGeneration = 0;
|
|
stampInitialShareGesture(({ cancelPendingSelection }) => {
|
|
cameraGeneration += 1;
|
|
if (cancelPendingSelection) {
|
|
coordinator.cancelPendingShareTracking('startup-gesture', { clearSelection: true });
|
|
}
|
|
});
|
|
flightsInit.resolve();
|
|
radioInit.resolve();
|
|
const results = await restore;
|
|
|
|
assert.equal(cameraGeneration, 1);
|
|
assert.equal(manager.isEnabled('flights'), true);
|
|
assert.equal(manager.isEnabled('radio'), true);
|
|
assert.deepEqual(manager.getLayerParams('flights'), {
|
|
models3d: true,
|
|
models3dMode: 'all',
|
|
irBoost: true,
|
|
selectedFlightsTrackingId: null,
|
|
});
|
|
assert.deepEqual(manager.getLayerParams('radio'), {
|
|
filter: 'genre:r&b',
|
|
volume: 0.42,
|
|
selectedStationId: 'private-station',
|
|
audioState: 'playing',
|
|
voiceDucked: true,
|
|
});
|
|
assert.equal(results.find((result) => result.layerId === 'flights').succeeded, true);
|
|
assert.equal(results.find((result) => result.layerId === 'radio').succeeded, true);
|
|
assert.deepEqual(storage.writes, []);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('tracking restore refreshes the destination feed before the module resolves Follow', async () => {
|
|
const order = [];
|
|
const manager = productionManager({
|
|
flights: {
|
|
update: () => { order.push('refresh'); return true; },
|
|
resolveTrackingRestoreTarget: (targetId, options) => {
|
|
order.push(`resolve:${targetId}:${options.origin}`);
|
|
return { status: 'found', source: 'test' };
|
|
},
|
|
},
|
|
});
|
|
await manager.setEnabled('flights', true, { origin: 'scene' });
|
|
order.length = 0;
|
|
const result = await manager.resolveLayerTrackingTarget('flights', 'abc123', {
|
|
origin: 'share-restore',
|
|
});
|
|
assert.deepEqual(order, ['refresh', 'resolve:abc123:share-restore']);
|
|
assert.equal(result.status, 'found');
|
|
assert.equal(result.refreshSucceeded, true);
|
|
});
|
|
|
|
test('shared flight selection is deferred until destination refresh and then follows', async () => {
|
|
const storage = memoryStorage();
|
|
const statuses = [];
|
|
const paramsCalls = [];
|
|
const manager = productionManager({
|
|
flights: {
|
|
setParams: (params) => { paramsCalls.push({ ...params }); return true; },
|
|
resolveTrackingRestoreTarget: (targetId) => ({ status: 'found', targetId }),
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights'];
|
|
state.options.flights = {
|
|
...state.options.flights,
|
|
models3d: true,
|
|
selectedFlightsTrackingId: 'abc123',
|
|
};
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
|
storage,
|
|
onTrackingRestoreStatus: (status) => statuses.push(status),
|
|
});
|
|
await coordinator.start({ shareLayerState: state, shareCreatedAtMs: 1_000 });
|
|
assert.deepEqual(paramsCalls, [{ models3d: true, models3dMode: 'proximity' }]);
|
|
|
|
const result = await coordinator.restoreShareTrackingSelection();
|
|
assert.equal(result.status, 'found');
|
|
assert.equal(result.classification, 'followed');
|
|
assert.equal(result.cleared, false);
|
|
assert.equal(statuses.length, 1);
|
|
assert.equal(coordinator.getDurableState().options.flights.selectedFlightsTrackingId, 'abc123');
|
|
assert.deepEqual(storage.writes, []);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('authoritative missing target clears only the passive ID and uses strict expiry boundary', async () => {
|
|
const run = async ({ nowMs, copiedAtMs }) => {
|
|
const storage = memoryStorage();
|
|
const manager = productionManager({
|
|
flights: {
|
|
resolveTrackingRestoreTarget: () => ({ status: 'missing' }),
|
|
setParams: (next) => (
|
|
Object.hasOwn(next, 'selectedFlightsTrackingId') && next.selectedFlightsTrackingId
|
|
? 'defer'
|
|
: true
|
|
),
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights'];
|
|
state.options.flights = {
|
|
...state.options.flights,
|
|
models3d: true,
|
|
models3dMode: 'all',
|
|
selectedFlightsTrackingId: 'gone001',
|
|
};
|
|
// The subject is absent, so the verdict now lands only after the pending
|
|
// window has genuinely expired. Drive that window synchronously.
|
|
const timers = manualTimers(nowMs);
|
|
const statuses = [];
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
|
storage,
|
|
now: timers.now,
|
|
setTimer: timers.setTimer,
|
|
clearTimer: timers.clearTimer,
|
|
onTrackingRestoreStatus: (status) => statuses.push(status),
|
|
});
|
|
await coordinator.start({ shareLayerState: state, shareCreatedAtMs: copiedAtMs });
|
|
const pending = await coordinator.restoreShareTrackingSelection();
|
|
assert.equal(pending.classification, 'pending', 'absence must first be held pending');
|
|
assert.equal(statuses.length, 1, 'pending is published for soft progress feedback');
|
|
assert.equal(statuses[0].classification, 'pending');
|
|
timers.runUntilIdle();
|
|
const result = statuses.at(-1);
|
|
const durable = coordinator.getDurableState();
|
|
coordinator.destroy();
|
|
return { result, durable, writes: storage.writes };
|
|
};
|
|
|
|
const boundary = await run({ nowMs: 190_000, copiedAtMs: 100_000 });
|
|
assert.equal(boundary.result.classification, 'unavailable');
|
|
assert.equal(boundary.result.cleared, true);
|
|
assert.equal(boundary.durable.options.flights.selectedFlightsTrackingId, null);
|
|
assert.equal(boundary.durable.options.flights.models3d, true);
|
|
assert.equal(boundary.durable.options.flights.models3dMode, 'all');
|
|
assert.deepEqual(boundary.writes, []);
|
|
|
|
const expired = await run({ nowMs: 190_001, copiedAtMs: 100_000 });
|
|
assert.equal(expired.result.classification, 'expired');
|
|
});
|
|
|
|
test('feed failure is not misreported as target absence and malformed time never says expired', async () => {
|
|
const storage = memoryStorage();
|
|
const statuses = [];
|
|
const manager = productionManager({
|
|
satellites: {
|
|
resolveTrackingRestoreTarget: () => ({ status: 'source-unavailable', reason: 'partial catalog' }),
|
|
setParams: (next) => (
|
|
Object.hasOwn(next, 'selectedSatTrackingId') && next.selectedSatTrackingId
|
|
? 'defer'
|
|
: true
|
|
),
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['satellites'];
|
|
state.options.satellites = {
|
|
...state.options.satellites,
|
|
selectedSatTrackingId: 25544,
|
|
};
|
|
const timers = manualTimers();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
|
storage,
|
|
now: timers.now,
|
|
setTimer: timers.setTimer,
|
|
clearTimer: timers.clearTimer,
|
|
onTrackingRestoreStatus: (status) => statuses.push(status),
|
|
});
|
|
await coordinator.start({ shareLayerState: state, shareCreatedAtMs: null });
|
|
const pending = await coordinator.restoreShareTrackingSelection();
|
|
// A partial catalog is a "not here YET" too — hold it, do not announce.
|
|
assert.equal(pending.classification, 'pending');
|
|
assert.equal(statuses.length, 1);
|
|
assert.equal(statuses[0].classification, 'pending');
|
|
timers.runUntilIdle();
|
|
const result = statuses.at(-1);
|
|
assert.equal(result.status, 'source-unavailable');
|
|
assert.equal(result.classification, 'source-unavailable');
|
|
assert.equal(result.cleared, true);
|
|
assert.equal(statuses.length, 2);
|
|
assert.deepEqual(storage.writes, []);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Share restore must be as resilient as reload-from-local.
|
|
//
|
|
// Reload-from-local arms the layer's own deferred-restore latch, which
|
|
// re-attempts on every later poll, so a contact that misses the first refresh
|
|
// is still picked up. The shared path used to decide on that single refresh:
|
|
// it cleared the subject from durable state AND the URL and posted a failure
|
|
// notice seconds into startup, so the SAME link healed on reload but never on
|
|
// the share. These pin both directions.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function pendingTrackingFixture({ resolveStatus = 'missing', copiedAtMs = null } = {}) {
|
|
const storage = memoryStorage();
|
|
const statuses = [];
|
|
const paramsCalls = [];
|
|
const cancellations = [];
|
|
const manager = productionManager({
|
|
flights: {
|
|
resolveTrackingRestoreTarget: () => ({ status: resolveStatus }),
|
|
setParams: (next, options) => {
|
|
paramsCalls.push({ params: { ...next }, origin: options?.origin });
|
|
// Production holds the id pending; getParams keeps reporting untracked
|
|
// until the subject actually arrives on a later poll.
|
|
return Object.hasOwn(next, 'selectedFlightsTrackingId') && next.selectedFlightsTrackingId
|
|
? 'defer'
|
|
: true;
|
|
},
|
|
cancelPendingTrackingRestore: (options) => cancellations.push(options),
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights'];
|
|
state.options.flights = {
|
|
...state.options.flights,
|
|
selectedFlightsTrackingId: 'late007',
|
|
};
|
|
const timers = manualTimers();
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
|
storage,
|
|
now: timers.now,
|
|
setTimer: timers.setTimer,
|
|
clearTimer: timers.clearTimer,
|
|
onTrackingRestoreStatus: (status) => statuses.push(status),
|
|
});
|
|
return {
|
|
manager,
|
|
coordinator,
|
|
statuses,
|
|
storage,
|
|
timers,
|
|
state,
|
|
copiedAtMs,
|
|
paramsCalls,
|
|
cancellations,
|
|
};
|
|
}
|
|
|
|
test('share tracking policies pin each owner, key, label, and acquisition deadline', () => {
|
|
assert.deepEqual(SHARE_TRACKING_RESTORE_POLICIES, {
|
|
flights: {
|
|
optionOwner: 'flights',
|
|
optionKey: 'selectedFlightsTrackingId',
|
|
expiryWindowMs: 90_000,
|
|
label: 'flight',
|
|
},
|
|
military: {
|
|
optionOwner: 'flights',
|
|
optionKey: 'selectedMilitaryTrackingId',
|
|
expiryWindowMs: 45_000,
|
|
label: 'military flight',
|
|
},
|
|
satellites: {
|
|
optionOwner: 'satellites',
|
|
optionKey: 'selectedSatTrackingId',
|
|
expiryWindowMs: 300_000,
|
|
label: 'satellite',
|
|
},
|
|
});
|
|
});
|
|
|
|
test('a shared subject that has not arrived yet is held pending, never declared gone', async () => {
|
|
const f = pendingTrackingFixture();
|
|
await f.coordinator.start({ shareLayerState: f.state, shareCreatedAtMs: f.copiedAtMs });
|
|
const pending = await f.coordinator.restoreShareTrackingSelection();
|
|
|
|
assert.equal(pending.status, 'pending');
|
|
assert.equal(pending.classification, 'pending');
|
|
assert.equal(pending.cleared, false);
|
|
// No notice, and the subject survives in durable state AND in the URL.
|
|
assert.equal(f.statuses.length, 1, 'a not-yet-arrived subject publishes progress');
|
|
assert.equal(f.statuses[0].classification, 'pending');
|
|
assert.equal(
|
|
f.coordinator.getDurableState().options.flights.selectedFlightsTrackingId,
|
|
'late007',
|
|
'the shared subject must not be cleared while it is still pending',
|
|
);
|
|
// The layer's own deferred-restore latch was armed, under the passive origin.
|
|
const armed = f.paramsCalls.filter((call) => call.params.selectedFlightsTrackingId === 'late007');
|
|
assert.equal(armed.length, 1, 'the layer latch is armed exactly once');
|
|
assert.equal(armed[0].origin, 'share-restore', 'arming must stay passive');
|
|
assert.deepEqual(f.storage.writes, [], 'recipient preferences are never written');
|
|
f.coordinator.destroy();
|
|
});
|
|
|
|
test('a pending shared subject latches on when it arrives on a later poll', async () => {
|
|
const f = pendingTrackingFixture();
|
|
await f.coordinator.start({ shareLayerState: f.state, shareCreatedAtMs: f.copiedAtMs });
|
|
await f.coordinator.restoreShareTrackingSelection();
|
|
assert.equal(f.statuses.length, 1);
|
|
assert.equal(f.statuses[0].classification, 'pending');
|
|
|
|
// The contact shows up on a later feed poll, exactly as the layer latch would.
|
|
f.manager.layers.get('flights').module._arrive({ selectedFlightsTrackingId: 'late007' });
|
|
f.timers.runUntilIdle();
|
|
|
|
assert.equal(f.statuses.length, 2);
|
|
assert.equal(f.statuses[1].classification, 'followed');
|
|
assert.equal(f.statuses[1].cleared, false);
|
|
assert.equal(
|
|
f.coordinator.getDurableState().options.flights.selectedFlightsTrackingId,
|
|
'late007',
|
|
);
|
|
assert.deepEqual(f.storage.writes, []);
|
|
f.coordinator.destroy();
|
|
});
|
|
|
|
test('caller abort after pending handoff revokes the watch and layer latch exactly once', async () => {
|
|
const f = pendingTrackingFixture();
|
|
const caller = new AbortController();
|
|
await f.coordinator.start({ shareLayerState: f.state, shareCreatedAtMs: f.copiedAtMs });
|
|
const pending = await f.coordinator.restoreShareTrackingSelection({ signal: caller.signal });
|
|
assert.equal(pending.classification, 'pending');
|
|
|
|
caller.abort('caller-cancelled');
|
|
f.timers.runUntilIdle();
|
|
caller.abort('late-repeat');
|
|
|
|
assert.deepEqual(f.statuses.map((status) => status.classification), ['pending', 'cancelled']);
|
|
assert.equal(f.statuses[1].reason, 'caller-cancelled');
|
|
assert.equal(f.cancellations.length, 1, 'the module latch is revoked exactly once');
|
|
assert.equal(f.cancellations[0].reason, 'caller-cancelled');
|
|
assert.equal(
|
|
f.coordinator.getDurableState().options.flights.selectedFlightsTrackingId,
|
|
'late007',
|
|
'cancellation does not rewrite recipient state',
|
|
);
|
|
f.coordinator.destroy();
|
|
});
|
|
|
|
test('a failed deferred-latch arm publishes one terminal failure and never ACQUIRING', async () => {
|
|
for (const armFailure of [
|
|
() => false,
|
|
() => Promise.reject(new Error('async latch rejection')),
|
|
() => { throw new Error('sync latch throw'); },
|
|
]) {
|
|
const f = pendingTrackingFixture();
|
|
await f.coordinator.start({ shareLayerState: f.state, shareCreatedAtMs: f.copiedAtMs });
|
|
const setLayerParams = f.manager.setLayerParams.bind(f.manager);
|
|
f.manager.setLayerParams = (layerId, params, options) => (
|
|
params?.selectedFlightsTrackingId
|
|
? armFailure()
|
|
: setLayerParams(layerId, params, options)
|
|
);
|
|
|
|
const result = await f.coordinator.restoreShareTrackingSelection();
|
|
f.timers.runUntilIdle();
|
|
|
|
assert.equal(result.status, 'source-unavailable');
|
|
assert.equal(result.classification, 'source-unavailable');
|
|
assert.equal(result.cleared, true);
|
|
assert.deepEqual(
|
|
f.statuses.map((status) => status.classification),
|
|
['source-unavailable'],
|
|
'a rejected latch never publishes pending progress',
|
|
);
|
|
assert.equal(f.coordinator.getDurableState().options.flights.selectedFlightsTrackingId, null);
|
|
f.coordinator.destroy();
|
|
}
|
|
});
|
|
|
|
test('same-target supersession cancels the old acquisition before publishing the successor', async () => {
|
|
const f = pendingTrackingFixture();
|
|
await f.coordinator.start({ shareLayerState: f.state, shareCreatedAtMs: f.copiedAtMs });
|
|
await f.coordinator.restoreShareTrackingSelection();
|
|
await f.coordinator.restoreShareTrackingSelection();
|
|
|
|
assert.deepEqual(
|
|
f.statuses.map((status) => status.classification),
|
|
['pending', 'cancelled', 'pending'],
|
|
);
|
|
assert.equal(f.cancellations.length, 1);
|
|
f.coordinator.cancelPendingShareTracking('test-complete');
|
|
f.coordinator.destroy();
|
|
});
|
|
|
|
test('destroying or superseding the coordinator clears pending progress explicitly', async () => {
|
|
for (const teardown of [
|
|
(coordinator) => coordinator.destroy(),
|
|
(coordinator) => coordinator.cancelPendingShareTracking('explicit-navigation'),
|
|
(coordinator) => coordinator.cancelPendingRestores('explicit-navigation'),
|
|
]) {
|
|
const f = pendingTrackingFixture();
|
|
await f.coordinator.start({ shareLayerState: f.state, shareCreatedAtMs: f.copiedAtMs });
|
|
await f.coordinator.restoreShareTrackingSelection();
|
|
teardown(f.coordinator);
|
|
f.timers.runUntilIdle();
|
|
assert.deepEqual(
|
|
f.statuses.map((status) => status.classification),
|
|
['pending', 'cancelled'],
|
|
'an abandoned pending watch publishes the clear event exactly once',
|
|
);
|
|
f.coordinator.destroy();
|
|
}
|
|
});
|
|
|
|
test('tracking resolver false and rejection stay source-unavailable while AbortError stays cancelled', async () => {
|
|
const run = async (resolveTrackingRestoreTarget, { signal = null } = {}) => {
|
|
const manager = productionManager({
|
|
flights: { resolveTrackingRestoreTarget },
|
|
});
|
|
await manager.setEnabled('flights', true, { origin: 'scene' });
|
|
return manager.resolveLayerTrackingTarget('flights', 'abc123', {
|
|
origin: 'share-restore',
|
|
signal,
|
|
});
|
|
};
|
|
|
|
const semanticFalse = await run(() => false);
|
|
assert.equal(semanticFalse.status, 'source-unavailable');
|
|
|
|
const rejected = await run(() => Promise.reject(new Error('feed unavailable')));
|
|
assert.equal(rejected.status, 'source-unavailable');
|
|
assert.equal(rejected.reason, 'feed unavailable');
|
|
|
|
const abortError = new Error('restore cancelled');
|
|
abortError.name = 'AbortError';
|
|
const cancelled = await run(() => Promise.reject(abortError));
|
|
assert.equal(cancelled.status, 'cancelled');
|
|
assert.equal(cancelled.errorClass, 'AbortError');
|
|
});
|
|
|
|
test('late target resolution after coordinator destroy cannot clear state or publish status', async () => {
|
|
const resolution = deferred();
|
|
const statuses = [];
|
|
const storage = memoryStorage();
|
|
const manager = productionManager({
|
|
flights: {
|
|
resolveTrackingRestoreTarget: () => resolution.promise,
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights'];
|
|
state.options.flights = {
|
|
...state.options.flights,
|
|
selectedFlightsTrackingId: 'stale001',
|
|
};
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
|
storage,
|
|
onTrackingRestoreStatus: (status) => statuses.push(status),
|
|
});
|
|
await coordinator.start({ shareLayerState: state });
|
|
const restore = coordinator.restoreShareTrackingSelection();
|
|
coordinator.destroy();
|
|
resolution.resolve({ status: 'missing' });
|
|
const result = await restore;
|
|
|
|
assert.equal(result.status, 'cancelled');
|
|
assert.equal(statuses.length, 0);
|
|
assert.equal(storage.writes.length, 0);
|
|
});
|
|
|
|
test('newer explicit tracked target cancels stale shared resolution without clearing the winner', async () => {
|
|
const resolution = deferred();
|
|
const storage = memoryStorage();
|
|
const manager = productionManager({
|
|
flights: {
|
|
resolveTrackingRestoreTarget: async () => resolution.promise,
|
|
},
|
|
});
|
|
const state = createDefaultLayerState();
|
|
state.enabledLayerIds = ['flights'];
|
|
state.options.flights = {
|
|
...state.options.flights,
|
|
selectedFlightsTrackingId: 'stale001',
|
|
};
|
|
const coordinator = new LayerStateCoordinator(manager, shareSink(), { storage });
|
|
await coordinator.start({ shareLayerState: state });
|
|
const restore = coordinator.restoreShareTrackingSelection();
|
|
manager.setLayerParams('flights', {
|
|
selectedFlightsTrackingId: 'newer002',
|
|
}, { origin: 'user' });
|
|
resolution.resolve({ status: 'missing' });
|
|
const result = await restore;
|
|
assert.equal(result.status, 'cancelled');
|
|
assert.equal(coordinator.getDurableState().options.flights.selectedFlightsTrackingId, 'newer002');
|
|
assert.equal(storage.writes.length, 1);
|
|
coordinator.destroy();
|
|
});
|
|
|
|
test('Radio durable params work before enable and never start playback', () => {
|
|
assert.equal(radioLayer.setParams({ filter: 'news', volume: 0.33 }), true);
|
|
assert.deepEqual(radioLayer.getParams(), { filter: 'news', volume: 0.33 });
|
|
assert.equal(radioLayer.getUIState().audioState, 'stopped');
|
|
assert.equal(radioLayer.getUIState().playingStationId, null);
|
|
radioLayer.setParams({ filter: 'all', volume: 0.8 });
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Share IDs and payloads are untrusted input and must be BOUNDED.
|
|
//
|
|
// A tracking ID is a transponder address, not free text. Identity is never
|
|
// truncated to fit: half an address is a different aircraft, not a shorter name
|
|
// for the same one, so an out-of-grammar ID is rejected outright and an
|
|
// oversized payload fails closed exactly like an unknown layer token.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('oversized and out-of-grammar tracking IDs are rejected, never truncated', () => {
|
|
const huge = 'a'.repeat(100_000);
|
|
|
|
// Normalization (the durable-state door).
|
|
const normalized = normalizeLayerState({
|
|
enabledLayerIds: ['flights'],
|
|
options: { flights: { selectedFlightsTrackingId: huge } },
|
|
});
|
|
assert.equal(
|
|
normalized.options.flights.selectedFlightsTrackingId,
|
|
null,
|
|
'a 100k-char ID must be rejected, not stored',
|
|
);
|
|
|
|
// Decoding (the untrusted-URL door).
|
|
const decoded = decodeLayerStateParams(new URLSearchParams([
|
|
['v', '2'], ['l', 'f'], ['lo', `f.t.${huge}`],
|
|
]));
|
|
assert.equal(decoded, null, 'an oversized lo payload fails closed');
|
|
|
|
// A merely long-but-under-cap ID is still out of grammar, and rejected
|
|
// WITHOUT taking the rest of the payload down with it.
|
|
const longish = decodeLayerStateParams(new URLSearchParams([
|
|
['v', '2'], ['l', 'f'], ['lo', `f.e.1_f.t.${'b'.repeat(40)}`],
|
|
]));
|
|
assert.deepEqual(longish.enabledLayerIds, ['flights']);
|
|
assert.equal(longish.options.flights.selectedFlightsTrackingId, null);
|
|
assert.equal(longish.options.flights.models3d, true, 'sibling options survive');
|
|
|
|
for (const bad of ['ab cd', 'ab/cd', '../../etc', 'a'.repeat(17), '<script>', '']) {
|
|
assert.equal(
|
|
normalizeLayerState({
|
|
enabledLayerIds: ['flights'],
|
|
options: { flights: { selectedFlightsTrackingId: bad } },
|
|
}).options.flights.selectedFlightsTrackingId,
|
|
null,
|
|
`out-of-grammar ID rejected: ${JSON.stringify(bad)}`,
|
|
);
|
|
}
|
|
|
|
// Real addresses still round-trip untouched, including TIS-B forms.
|
|
for (const good of ['aaa001', 'ae1fa4', '~ab1234', 'A1B2C3']) {
|
|
const state = normalizeLayerState({
|
|
enabledLayerIds: ['flights'],
|
|
options: { flights: { selectedFlightsTrackingId: good } },
|
|
});
|
|
assert.equal(state.options.flights.selectedFlightsTrackingId, good.toLowerCase());
|
|
const params = new URLSearchParams([['v', '2']]);
|
|
encodeLayerStateParams(params, state);
|
|
assert.equal(
|
|
decodeLayerStateParams(params).options.flights.selectedFlightsTrackingId,
|
|
good.toLowerCase(),
|
|
`round-trip preserved: ${good}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('an oversized enabled-layer field fails closed instead of decoding a prefix', () => {
|
|
assert.equal(
|
|
decodeLayerStateParams(new URLSearchParams([['v', '2'], ['l', 'f.'.repeat(5_000)]])),
|
|
null,
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The pending watch and the LAYER's deferred-restore latch are two halves of
|
|
// one mechanism and must die together.
|
|
//
|
|
// Aborting only the restore controller was a no-op by the time the watch
|
|
// existed (the controller has already settled), so the orphaned timer went on
|
|
// to announce "Shared … unavailable" at its deadline — a verdict about work
|
|
// nothing was attempting any more. Both paths below cancel the module latch in
|
|
// production: an explicit parameter replacement, and the owner layer going
|
|
// away (at any origin, including a programmatic disable).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('an explicit parameter change revokes the pending watch, not just its controller', async () => {
|
|
const f = pendingTrackingFixture();
|
|
await f.coordinator.start({ shareLayerState: f.state, shareCreatedAtMs: f.copiedAtMs });
|
|
await f.coordinator.restoreShareTrackingSelection();
|
|
assert.deepEqual(f.statuses.map((status) => status.classification), ['pending']);
|
|
|
|
// The operator changes an unrelated Flights option. Production cancels the
|
|
// module latch on exactly this event, so nothing is restoring any more.
|
|
await f.manager.setLayerParams('flights', { models3d: true }, { origin: 'user' });
|
|
f.timers.runUntilIdle();
|
|
|
|
assert.deepEqual(
|
|
f.statuses.map((status) => status.classification),
|
|
['pending', 'cancelled'],
|
|
'a revoked restore clears progress without announcing a terminal failure',
|
|
);
|
|
f.coordinator.destroy();
|
|
});
|
|
|
|
test('the owner layer going away revokes the pending watch at any origin', async () => {
|
|
for (const origin of ['programmatic', 'user']) {
|
|
const f = pendingTrackingFixture();
|
|
await f.coordinator.start({ shareLayerState: f.state, shareCreatedAtMs: f.copiedAtMs });
|
|
await f.coordinator.restoreShareTrackingSelection();
|
|
assert.deepEqual(f.statuses.map((status) => status.classification), ['pending']);
|
|
|
|
await f.manager.setEnabled('flights', false, { origin });
|
|
f.timers.runUntilIdle();
|
|
|
|
assert.deepEqual(
|
|
f.statuses.map((status) => status.classification),
|
|
['pending', 'cancelled'],
|
|
`a disabled owner layer clears progress without a terminal failure (origin=${origin})`,
|
|
);
|
|
f.coordinator.destroy();
|
|
}
|
|
});
|