mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
# Conflicts: # docs/CURRENT-STATE.md # src/app/constructCatalog.test.mjs # src/app/sourceComposition.test.mjs # src/data/layerState.test.mjs # src/data/layerStateTokenLedger.test.mjs # src/i18n/locales/ar.json
3016 lines
96 KiB
JavaScript
3016 lines
96 KiB
JavaScript
import { encodeParams, decodeParams } from '../layers/streetLevel/params.js';
|
||
import { readShellSource } from '../testSupport/readShellSource.mjs';
|
||
import { renderApplicationHtml } from '../testSupport/applicationHtml.mjs';
|
||
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
|
||
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
|
||
import { DataLayerManager } from './manager.js';
|
||
import {
|
||
LEGACY_LAYER_STATE_TOKENS,
|
||
LAYER_STATE_REGISTRY,
|
||
LAYER_STATE_STORAGE_KEY,
|
||
LAYER_STATE_TOKEN_ALPHABET,
|
||
LAYER_STATE_TOKEN_RESERVATIONS,
|
||
REGISTERED_LAYER_IDS,
|
||
SHARE_TRACKING_RESTORE_POLICIES,
|
||
createDefaultLayerState,
|
||
decodeLayerStateParams,
|
||
encodeLayerStateParams,
|
||
nextLayerStateToken,
|
||
normalizeLayerState,
|
||
parseStoredLayerState,
|
||
serializeStoredLayerState,
|
||
validateLayerStateAllocations,
|
||
validateLayerStateRegistry,
|
||
} from './layerState.js';
|
||
import { LayerStateCoordinator } from './layerStateCoordinator.js';
|
||
import radioLayer from './radio.js';
|
||
import { stampInitialShareGesture } from '../navigationPolicy.js';
|
||
import { MAX_SINCE_DAYS } from '../layers/streetLevel/policy.js';
|
||
import { SINCE_STOPS } from '../ui/streetLevelPresentation.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, 35);
|
||
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 35);
|
||
assert.ok(REGISTERED_LAYER_IDS.includes('transit'));
|
||
assert.deepEqual(REGISTERED_LAYER_IDS, [...REGISTERED_LAYER_IDS].sort());
|
||
assert.deepEqual(LEGACY_LAYER_STATE_TOKENS, {
|
||
'ais-live-vessels': 'a',
|
||
'alpr-cameras': 'p',
|
||
'bhote-koshi-2026': 'h',
|
||
'bhote-koshi-locator': 'z',
|
||
bikeshare: 'b',
|
||
cctv: 'c',
|
||
directions: 'n',
|
||
earthquakes: 'e',
|
||
'fire-perimeters': '2',
|
||
flights: 'f',
|
||
'local-dams': 'q',
|
||
'local-datacenters': 'd',
|
||
'local-firms': 'w',
|
||
military: 'm',
|
||
'military-awareness': 'g',
|
||
'military-installations': 'i',
|
||
radio: 'r',
|
||
'recent-imagery': '1',
|
||
'rocket-launches': 'x',
|
||
satellites: 's',
|
||
'telegeography-submarine-cables': 'u',
|
||
traffic: 't',
|
||
transit: 'j',
|
||
'weather-cyclones': 'y',
|
||
'weather-lightning': 'l',
|
||
'weather-radar': 'v',
|
||
'weather-satellite': 'o',
|
||
wind: 'k',
|
||
});
|
||
for (const { id, token } of LAYER_STATE_REGISTRY) {
|
||
assert.equal(LAYER_STATE_TOKEN_RESERVATIONS[id], token);
|
||
}
|
||
for (const [id, token] of Object.entries(LEGACY_LAYER_STATE_TOKENS)) {
|
||
assert.equal(LAYER_STATE_TOKEN_RESERVATIONS[id], token);
|
||
}
|
||
assert.equal(nextLayerStateToken(), '9');
|
||
assert.equal(
|
||
nextLayerStateToken({
|
||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||
alpha: '9',
|
||
bravo: '00',
|
||
}),
|
||
'01',
|
||
);
|
||
const digitsExhausted = {
|
||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||
...Object.fromEntries([...'9'].map((digit) => [`prior-${digit}`, digit])),
|
||
};
|
||
assert.equal(nextLayerStateToken(digitsExhausted), '00');
|
||
assert.equal(
|
||
nextLayerStateToken({ ...digitsExhausted, retired: '00', used: '01' }),
|
||
'02',
|
||
);
|
||
assert.equal(
|
||
nextLayerStateToken({
|
||
...digitsExhausted,
|
||
...Object.fromEntries(
|
||
[...LAYER_STATE_TOKEN_ALPHABET].map((second) => [
|
||
`pair-0${second}`,
|
||
`0${second}`,
|
||
]),
|
||
),
|
||
}),
|
||
'10',
|
||
);
|
||
assert.throws(
|
||
() =>
|
||
nextLayerStateToken(
|
||
Object.fromEntries(
|
||
[
|
||
...'0123456789',
|
||
...[...LAYER_STATE_TOKEN_ALPHABET].flatMap((first) =>
|
||
[...LAYER_STATE_TOKEN_ALPHABET].map(
|
||
(second) => `${first}${second}`,
|
||
),
|
||
),
|
||
].map((token, index) => [`occupied-${index}`, token]),
|
||
),
|
||
),
|
||
/namespace exhausted/,
|
||
);
|
||
assert.equal(
|
||
validateLayerStateRegistry(
|
||
[{ id: 'future-layer', token: '0', disposition: 'enabled-only' }],
|
||
{ 'future-layer': '0' },
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
validateLayerStateRegistry(
|
||
[{ id: 'future-layer', token: '01', disposition: 'enabled-only' }],
|
||
{ retired: '00', 'future-layer': '01' },
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
validateLayerStateAllocations(LAYER_STATE_TOKEN_RESERVATIONS, {
|
||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||
future: '9',
|
||
next: '00',
|
||
}),
|
||
true,
|
||
);
|
||
assert.throws(
|
||
() =>
|
||
validateLayerStateAllocations(LAYER_STATE_TOKEN_RESERVATIONS, {
|
||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||
future: '00',
|
||
}),
|
||
/next free token 9/,
|
||
);
|
||
const beforeLastDigit = { ...digitsExhausted };
|
||
delete beforeLastDigit['prior-9'];
|
||
assert.equal(
|
||
validateLayerStateAllocations(beforeLastDigit, {
|
||
...beforeLastDigit,
|
||
futurePair: '00',
|
||
futureDigit: '9',
|
||
}),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
validateLayerStateAllocations(digitsExhausted, {
|
||
...digitsExhausted,
|
||
pairB: '01',
|
||
pairA: '00',
|
||
}),
|
||
true,
|
||
);
|
||
assert.throws(
|
||
() =>
|
||
validateLayerStateAllocations(
|
||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '9' },
|
||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '9', competing: '9' },
|
||
),
|
||
/next free token 00/,
|
||
);
|
||
assert.throws(
|
||
() => validateLayerStateAllocations({ future: '00' }, { future: '01' }),
|
||
/changed or removed/,
|
||
);
|
||
assert.throws(
|
||
() => validateLayerStateAllocations({ retired: '00' }, { newcomer: '00' }),
|
||
/changed or removed/,
|
||
);
|
||
assert.throws(
|
||
() =>
|
||
validateLayerStateRegistry([
|
||
...LAYER_STATE_REGISTRY,
|
||
LAYER_STATE_REGISTRY[0],
|
||
]),
|
||
/Duplicate layer-state id/,
|
||
);
|
||
assert.throws(
|
||
() =>
|
||
validateLayerStateRegistry(
|
||
[{ id: 'future-layer', token: '000', disposition: 'enabled-only' }],
|
||
{ 'future-layer': '000' },
|
||
),
|
||
/Invalid layer-state token reservation/,
|
||
);
|
||
assert.throws(
|
||
() =>
|
||
validateLayerStateRegistry(
|
||
[{ id: 'future-layer', token: 'a', disposition: 'enabled-only' }],
|
||
{ 'future-layer': 'a' },
|
||
),
|
||
/Legacy layer-state token is immutable/,
|
||
);
|
||
assert.throws(
|
||
() =>
|
||
validateLayerStateRegistry(
|
||
[{ id: 'flights', token: '0', disposition: 'enabled-only' }],
|
||
{ flights: '0' },
|
||
),
|
||
/Legacy layer-state token is immutable/,
|
||
);
|
||
|
||
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('all production layers and options round-trip through a v2 share URL', () => {
|
||
const allEnabled = normalizeLayerState({
|
||
enabledLayerIds: REGISTERED_LAYER_IDS,
|
||
options: {
|
||
cctv: { coverageMode: 'viewshed', showProjection: false, autoHop: true },
|
||
flights: { models3d: true, models3dMode: 'all' },
|
||
radio: { filter: 'news', volume: 0.37 },
|
||
},
|
||
});
|
||
const shareUrl = new URL('https://example.invalid/');
|
||
shareUrl.hash = encode(allEnabled);
|
||
const params = new URLSearchParams(shareUrl.hash.slice(1));
|
||
const restored = decodeLayerStateParams(params);
|
||
|
||
assert.equal(params.get('l')?.split('.').length, REGISTERED_LAYER_IDS.length);
|
||
assert.deepEqual(restored?.enabledLayerIds, REGISTERED_LAYER_IDS);
|
||
assert.deepEqual(restored?.options, allEnabled.options);
|
||
});
|
||
|
||
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('malformed enabled-layer lists reject the entire payload', () => {
|
||
for (const value of ['.c', 'c.', 'c..e', 'c.c', '00', 'c.00']) {
|
||
assert.equal(
|
||
decodeLayerStateParams(new URLSearchParams(`v=2&l=${value}`)),
|
||
null,
|
||
`l=${value}`,
|
||
);
|
||
}
|
||
assert.deepEqual(
|
||
decodeLayerStateParams(new URLSearchParams('v=2&l=c.e')).enabledLayerIds,
|
||
['cctv', 'earthquakes'],
|
||
);
|
||
for (const fields of ['l=f&l=f', 'l=f&l=unknown', 'l=&l=f']) {
|
||
assert.equal(
|
||
decodeLayerStateParams(new URLSearchParams(`v=2&${fields}`)),
|
||
null,
|
||
fields,
|
||
);
|
||
}
|
||
});
|
||
|
||
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,
|
||
};
|
||
state.options.wind.overlay = 'speed'; // Frozen v2 omitted-token meaning; new boots use trails.
|
||
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 = await renderApplicationHtml(
|
||
await readFile(new URL('../../index.html', import.meta.url), 'utf8'),
|
||
);
|
||
assert.match(
|
||
html,
|
||
/<button(?=[^>]*\sid="models3d-toggle")(?=[^>]*\sclass="(?=[^"]*\bpp-toggle-btn\b)(?=[^"]*\bactive\b))(?=[^>]*\saria-pressed="true")/,
|
||
'index.html: the 3D button paints lit on first paint, before ui.js runs — and says so',
|
||
);
|
||
assert.match(
|
||
ui,
|
||
/this\._models3dPressedShown = this\._models3dEnabled;/,
|
||
'ui.js: and keeps the pressed state synchronized, so the lit state is not colour-only',
|
||
);
|
||
const panel = await readFile(
|
||
new URL('../ui/views/panels/DisplayPanel.vue', import.meta.url),
|
||
'utf8',
|
||
);
|
||
assert.match(
|
||
panel,
|
||
/:pressed="models3dOn" pressed-class="active" id="models3d-toggle"/,
|
||
'DisplayPanel.vue: the view renders aria-pressed with the lit state',
|
||
);
|
||
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();
|
||
}
|
||
});
|
||
|
||
test('wind appearance shares round trip while old links retain weather defaults', () => {
|
||
const state = normalizeLayerState({
|
||
enabledLayerIds: ['wind'],
|
||
options: {
|
||
wind: { model: 'ifs', overlay: 'pressure', units: 'mph', paused: true },
|
||
},
|
||
});
|
||
assert.deepEqual(
|
||
decodeLayerStateParams(new URLSearchParams(encode(state))).options.wind,
|
||
{ model: 'ifs', overlay: 'pressure', units: 'mph', paused: true },
|
||
);
|
||
const defaults = createDefaultLayerState().options.wind;
|
||
assert.deepEqual(defaults, {
|
||
model: 'gfs',
|
||
overlay: 'none',
|
||
units: 'km/h',
|
||
paused: false,
|
||
});
|
||
const legacy = decodeLayerStateParams(new URLSearchParams('v=2&l=k'));
|
||
assert.equal(
|
||
legacy.options.wind.overlay,
|
||
'speed',
|
||
'old links retain their authored field',
|
||
);
|
||
assert.equal(
|
||
decodeLayerStateParams(
|
||
new URLSearchParams(encode(createDefaultLayerState())),
|
||
).options.wind.overlay,
|
||
'none',
|
||
'new default is encoded explicitly',
|
||
);
|
||
const old = normalizeLayerState({ options: { wind: { model: 'ifs' } } });
|
||
assert.deepEqual(old.options.wind, { ...defaults, model: 'ifs' });
|
||
const invalid = normalizeLayerState({
|
||
options: {
|
||
wind: {
|
||
model: 'unknown',
|
||
overlay: 'clouds',
|
||
units: '<script>',
|
||
paused: 'yes',
|
||
},
|
||
},
|
||
});
|
||
assert.deepEqual(invalid.options.wind, defaults);
|
||
});
|
||
|
||
test('observed weather round trips product and presets without persisting historical playback', () => {
|
||
const state = normalizeLayerState({
|
||
enabledLayerIds: ['weather-radar', 'weather-satellite'],
|
||
options: {
|
||
'weather-radar': { opacity: 'light', play: true },
|
||
'weather-satellite': {
|
||
product: 'clouds',
|
||
opacity: 'light',
|
||
infrared: 'filtered',
|
||
step: -1,
|
||
},
|
||
},
|
||
});
|
||
const params = new URLSearchParams(encode(state));
|
||
const decoded = decodeLayerStateParams(params);
|
||
assert.deepEqual(decoded, state);
|
||
assert.equal(state.options['weather-satellite'].product, 'clouds');
|
||
assert.equal(state.options['weather-satellite'].opacity, 'light');
|
||
assert.equal(state.options['weather-satellite'].infrared, 100);
|
||
assert.equal(Object.hasOwn(state.options['weather-radar'], 'play'), false);
|
||
});
|
||
|
||
test('satellite filter strength percentages round-trip and legacy mode codes remain readable', () => {
|
||
const state = normalizeLayerState({
|
||
enabledLayerIds: ['weather-satellite'],
|
||
options: { 'weather-satellite': { infrared: 53, opacity: 'light' } },
|
||
});
|
||
const params = encodeLayerStateParams(
|
||
new URLSearchParams([['v', '2']]),
|
||
state,
|
||
);
|
||
assert.deepEqual(decodeLayerStateParams(params), state);
|
||
assert.equal(state.options['weather-satellite'].infrared, 53);
|
||
assert.equal(state.options['weather-satellite'].opacity, 'light');
|
||
for (const [legacy, expected] of [
|
||
['f', 100],
|
||
['a', 0],
|
||
]) {
|
||
const decoded = decodeLayerStateParams(
|
||
new URLSearchParams(`v=2&l=o&lo=o.i.${legacy}`),
|
||
);
|
||
assert.equal(decoded.options['weather-satellite'].infrared, expected);
|
||
}
|
||
for (const [legacy, expected] of [
|
||
['filtered', 100],
|
||
['full', 0],
|
||
]) {
|
||
const normalized = normalizeLayerState({
|
||
enabledLayerIds: ['weather-satellite'],
|
||
options: { 'weather-satellite': { infrared: legacy } },
|
||
});
|
||
assert.equal(normalized.options['weather-satellite'].infrared, expected);
|
||
}
|
||
const invalid = normalizeLayerState({
|
||
enabledLayerIds: ['weather-satellite'],
|
||
options: { 'weather-satellite': { infrared: 101 } },
|
||
});
|
||
assert.equal(invalid.options['weather-satellite'].infrared, 100);
|
||
});
|
||
|
||
test('satellite cloud filter round trips and invalid or absent values default to Clouds only', () => {
|
||
for (const [infrared, expected] of [
|
||
['full', 0],
|
||
['filtered', 100],
|
||
[53, 53],
|
||
[undefined, 100],
|
||
['invalid', 100],
|
||
]) {
|
||
const state = normalizeLayerState({
|
||
enabledLayerIds: ['weather-satellite'],
|
||
options: {
|
||
'weather-satellite': {
|
||
infrared,
|
||
product: 'clouds',
|
||
step: -1,
|
||
play: true,
|
||
},
|
||
},
|
||
});
|
||
assert.deepEqual(
|
||
decodeLayerStateParams(new URLSearchParams(encode(state))),
|
||
state,
|
||
);
|
||
assert.equal(state.options['weather-satellite'].infrared, expected);
|
||
assert.equal(
|
||
Object.hasOwn(state.options['weather-satellite'], 'step'),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
Object.hasOwn(state.options['weather-satellite'], 'play'),
|
||
false,
|
||
);
|
||
}
|
||
});
|
||
|
||
// Recent Imagery (`1`): the box travels as four integers at degrees × 100000,
|
||
// the two days as product letter + compact date, the split as a percent.
|
||
const imageryOptions = (lo) =>
|
||
decodeLayerStateParams(
|
||
new URLSearchParams([
|
||
['v', '2'],
|
||
['l', '1'],
|
||
['lo', lo],
|
||
]),
|
||
).options['recent-imagery'];
|
||
|
||
test('recent-imagery box, days, mode, split and overview toggle round-trip under token 1; defaults stay out of the URL', () => {
|
||
const entry = LAYER_STATE_REGISTRY.find((row) => row.id === 'recent-imagery');
|
||
assert.deepEqual(
|
||
[entry.token, entry.disposition, entry.optionOwner],
|
||
['1', 'enabled+options', 'recent-imagery'],
|
||
);
|
||
assert.deepEqual(createDefaultLayerState().options['recent-imagery'], {
|
||
west: null,
|
||
south: null,
|
||
east: null,
|
||
north: null,
|
||
a: null,
|
||
b: null,
|
||
mode: 0,
|
||
split: 50,
|
||
viirs: false,
|
||
});
|
||
const state = createDefaultLayerState();
|
||
state.enabledLayerIds = ['recent-imagery', 'flights'];
|
||
state.options['recent-imagery'] = {
|
||
west: -9781235,
|
||
south: -3020000,
|
||
east: -9770000,
|
||
north: 3040000,
|
||
a: 'S30:2026-09-18',
|
||
b: 'L30:2026-09-10',
|
||
mode: 2,
|
||
split: 37,
|
||
viirs: true,
|
||
};
|
||
const encoded = encode(state);
|
||
assert.ok(
|
||
encoded.includes(
|
||
'1.w.-9781235_1.s.-3020000_1.e.-9770000_1.n.3040000_1.a.S20260918_1.b.L20260910_1.m.2_1.p.37_1.v.1',
|
||
),
|
||
encoded,
|
||
);
|
||
const decoded = decodeLayerStateParams(new URLSearchParams(encoded));
|
||
assert.deepEqual(
|
||
decoded.options['recent-imagery'],
|
||
state.options['recent-imagery'],
|
||
);
|
||
assert.deepEqual(decoded.options.flights, state.options.flights);
|
||
const bare = createDefaultLayerState();
|
||
bare.enabledLayerIds = ['recent-imagery'];
|
||
assert.doesNotMatch(
|
||
new URLSearchParams(encode(bare)).get('lo') || '',
|
||
/(^|_)1\./,
|
||
);
|
||
// The codec is calendar-only: a link decodes the same whenever it is opened.
|
||
assert.equal(imageryOptions('1.a.L19991231').a, 'L30:1999-12-31');
|
||
assert.equal(imageryOptions('1.b.V20991231').b, 'VIIRS:2099-12-31');
|
||
});
|
||
|
||
test('recent-imagery rejects impossible days and out-of-range edges instead of rolling or clamping them', () => {
|
||
for (const bad of [
|
||
'S20260230',
|
||
'L20261301',
|
||
'S20260900',
|
||
'X20260918',
|
||
's20260918',
|
||
'S2026091',
|
||
'S30:2026-09-18',
|
||
])
|
||
assert.equal(imageryOptions(`1.a.${bad}`).a, null, bad);
|
||
assert.equal(
|
||
imageryOptions('1.b.S20240229').b,
|
||
'S30:2024-02-29',
|
||
'a real leap day',
|
||
);
|
||
assert.equal(imageryOptions('1.b.S20230229').b, null);
|
||
const mixed = imageryOptions('1.a.S20260230_1.b.V20260921_1.p.80');
|
||
assert.deepEqual(
|
||
[mixed.a, mixed.b, mixed.split],
|
||
[null, 'VIIRS:2026-09-21', 80],
|
||
);
|
||
for (const bad of [
|
||
'1.w.18000001',
|
||
'1.n.8505111',
|
||
'1.s.-8505111',
|
||
'1.w.1.5',
|
||
'1.w.abc',
|
||
'1.w.1234567890',
|
||
]) {
|
||
const field = { w: 'west', s: 'south', n: 'north' }[bad.split('.')[1]];
|
||
assert.equal(imageryOptions(bad)[field], null, bad);
|
||
}
|
||
assert.equal(imageryOptions('1.w.18000000').west, 18000000);
|
||
assert.equal(imageryOptions('1.p.101').split, 50);
|
||
assert.equal(imageryOptions('1.m.1').mode, 1);
|
||
for (const bad of ['1.m.3', '1.m.-1', '1.m.x'])
|
||
assert.equal(imageryOptions(bad).mode, 0, bad);
|
||
assert.deepEqual(
|
||
normalizeLayerState({
|
||
enabledLayerIds: ['recent-imagery'],
|
||
options: {
|
||
'recent-imagery': {
|
||
a: 'S30:2026-02-30',
|
||
b: ' L30:2026-09-10 ',
|
||
split: 150,
|
||
west: 'abc',
|
||
north: -8505110,
|
||
},
|
||
},
|
||
}).options['recent-imagery'],
|
||
{
|
||
west: null,
|
||
south: null,
|
||
east: null,
|
||
north: -8505110,
|
||
a: null,
|
||
b: 'L30:2026-09-10',
|
||
mode: 0,
|
||
split: 50,
|
||
viirs: false,
|
||
},
|
||
);
|
||
// An oversized payload fails closed at the shared 512-character cap.
|
||
const long = '1.a.S20260918_'.repeat(40);
|
||
assert.ok(imageryOptions(long.slice(0, 512)));
|
||
assert.equal(
|
||
decodeLayerStateParams(
|
||
new URLSearchParams([
|
||
['v', '2'],
|
||
['l', '1'],
|
||
['lo', long.slice(0, 513)],
|
||
]),
|
||
),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test('the recent-imagery split is share-link only: never stored locally, and a stored value reads as the default', () => {
|
||
const state = createDefaultLayerState();
|
||
state.enabledLayerIds = ['recent-imagery'];
|
||
state.options['recent-imagery'] = {
|
||
a: 'S30:2026-09-18',
|
||
split: 8,
|
||
viirs: true,
|
||
};
|
||
assert.ok(encode(state).includes('1.p.8'));
|
||
const stored = JSON.parse(serializeStoredLayerState(state)).o[
|
||
'recent-imagery'
|
||
];
|
||
assert.deepEqual(
|
||
[stored.split, stored.a, stored.viirs],
|
||
[50, 'S30:2026-09-18', true],
|
||
);
|
||
const previous = JSON.stringify({
|
||
v: 2,
|
||
l: ['recent-imagery'],
|
||
o: { 'recent-imagery': { split: 8 } },
|
||
});
|
||
assert.equal(
|
||
parseStoredLayerState(previous).options['recent-imagery'].split,
|
||
50,
|
||
);
|
||
});
|
||
|
||
test('drone restrictions uses digit 8 without colliding with fire perimeters', () => {
|
||
const decoded = decodeLayerStateParams(new URLSearchParams('v=2&l=8.2'));
|
||
assert.deepEqual(decoded.enabledLayerIds, [
|
||
'drone-airspace',
|
||
'fire-perimeters',
|
||
]);
|
||
assert.equal(
|
||
LAYER_STATE_REGISTRY.find(({ id }) => id === 'drone-airspace').token,
|
||
'8',
|
||
);
|
||
assert.deepEqual(
|
||
decodeLayerStateParams(new URLSearchParams(encode(decoded))),
|
||
decoded,
|
||
);
|
||
});
|
||
|
||
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-integrity',
|
||
'recent-imagery',
|
||
]);
|
||
assert.equal(
|
||
LAYER_STATE_REGISTRY.find(({ id }) => id === 'gnss-integrity').token,
|
||
'3',
|
||
);
|
||
assert.deepEqual(
|
||
decodeLayerStateParams(new URLSearchParams(encode(decoded))),
|
||
decoded,
|
||
);
|
||
});
|
||
|
||
test('fire perimeters uses digit 2 without colliding with wind or recent imagery', () => {
|
||
const decoded = decodeLayerStateParams(new URLSearchParams('v=2&l=2.k.1'));
|
||
assert.deepEqual(decoded.enabledLayerIds, [
|
||
'fire-perimeters',
|
||
'recent-imagery',
|
||
'wind',
|
||
]);
|
||
assert.equal(
|
||
LAYER_STATE_REGISTRY.find(({ id }) => id === 'fire-perimeters').token,
|
||
'2',
|
||
);
|
||
assert.deepEqual(
|
||
decodeLayerStateParams(new URLSearchParams(encode(decoded))),
|
||
decoded,
|
||
);
|
||
});
|
||
|
||
/** Street Level options decoded from a v2 link with these `lo` assignments. */
|
||
function streetLevelOptions(lo) {
|
||
const params = new URLSearchParams([
|
||
['v', '2'],
|
||
['l', '0'],
|
||
]);
|
||
if (lo) params.set('lo', lo);
|
||
return decodeLayerStateParams(params).options['street-level'];
|
||
}
|
||
|
||
test('Street Level: every option round-trips through a share link and stored state', () => {
|
||
const state = createDefaultLayerState();
|
||
state.enabledLayerIds = ['street-level'];
|
||
state.options['street-level'] = {
|
||
mapillary: false,
|
||
providerSwitches: '',
|
||
pano: 'flat',
|
||
sinceDays: MAX_SINCE_DAYS,
|
||
};
|
||
const decoded = decodeLayerStateParams(new URLSearchParams(encode(state)));
|
||
assert.deepEqual(decoded.options['street-level'], {
|
||
mapillary: false,
|
||
providerSwitches: '',
|
||
pano: 'flat',
|
||
sinceDays: MAX_SINCE_DAYS,
|
||
});
|
||
assert.deepEqual(
|
||
parseStoredLayerState(serializeStoredLayerState(state)).options[
|
||
'street-level'
|
||
],
|
||
state.options['street-level'],
|
||
);
|
||
});
|
||
|
||
test('Street Level: the link codec accepts exactly the windows the filter keeps', () => {
|
||
// A window past MAX_SINCE_DAYS reads as "any date".
|
||
assert.equal(
|
||
streetLevelOptions(`0.s.${MAX_SINCE_DAYS}`).sinceDays,
|
||
MAX_SINCE_DAYS,
|
||
);
|
||
assert.equal(streetLevelOptions(`0.s.${MAX_SINCE_DAYS + 1}`).sinceDays, 0);
|
||
// Every SINCE slider stop survives, the LAST 10 YEARS one included.
|
||
for (const { days } of SINCE_STOPS)
|
||
assert.equal(streetLevelOptions(`0.s.${days}`).sinceDays, days);
|
||
assert.equal(streetLevelOptions('0.s.3652').sinceDays, 3652);
|
||
});
|
||
|
||
test('Street Level: a link without the provider switches keeps Mapillary on and Street View off', () => {
|
||
assert.deepEqual(streetLevelOptions(''), {
|
||
mapillary: true,
|
||
providerSwitches: '',
|
||
pano: 'all',
|
||
sinceDays: 0,
|
||
});
|
||
// Street View rides the shared provider-switch field, off unless lit.
|
||
assert.equal(streetLevelOptions('0.r.google-1').providerSwitches, 'google-1');
|
||
assert.equal(
|
||
createDefaultLayerState().options['street-level'].providerSwitches,
|
||
'',
|
||
);
|
||
assert.equal(streetLevelOptions('0.p.f').mapillary, true);
|
||
assert.equal(streetLevelOptions('0.m.0').mapillary, false);
|
||
assert.equal(
|
||
createDefaultLayerState().options['street-level'].mapillary,
|
||
true,
|
||
);
|
||
});
|
||
|
||
test('explicit provider switches reach durable state without adopting unrelated live switches', () => {
|
||
const manager = new DataLayerManager({});
|
||
const providers = new Map([
|
||
['mapillary', true],
|
||
['alpha', true],
|
||
['beta', true],
|
||
]);
|
||
const filter = { pano: 'all', sinceDays: 0 };
|
||
for (const id of REGISTERED_LAYER_IDS) {
|
||
manager.register(
|
||
id === 'street-level'
|
||
? {
|
||
...fakeLayer(id),
|
||
getParams: () => encodeParams({ providers, filter }),
|
||
setParams(params) {
|
||
const decoded = decodeParams(params, {
|
||
providerIds: providers.keys(),
|
||
filter,
|
||
});
|
||
for (const [id, on] of decoded.providers) providers.set(id, on);
|
||
return true;
|
||
},
|
||
}
|
||
: fakeLayer(id),
|
||
);
|
||
}
|
||
manager.finalizeRegistrations(LAYER_STATE_REGISTRY);
|
||
const storage = memoryStorage();
|
||
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
||
storage,
|
||
});
|
||
try {
|
||
providers.set('beta', false); // Passive state does not belong to the next action.
|
||
assert.equal(
|
||
manager.setLayerParams(
|
||
'street-level',
|
||
{ alpha: false },
|
||
{ origin: 'user' },
|
||
),
|
||
true,
|
||
);
|
||
let saved = parseStoredLayerState(storage.getItem(LAYER_STATE_STORAGE_KEY));
|
||
assert.equal(saved.options['street-level'].providerSwitches, 'alpha-0');
|
||
const shared = decodeLayerStateParams(new URLSearchParams(encode(saved)));
|
||
const restored = decodeParams(shared.options['street-level'], {
|
||
providerIds: ['beta', 'alpha'],
|
||
filter,
|
||
});
|
||
assert.deepEqual([...restored.providers], [['alpha', false]]);
|
||
manager.setLayerParams('street-level', { beta: true }, { origin: 'user' });
|
||
saved = parseStoredLayerState(storage.getItem(LAYER_STATE_STORAGE_KEY));
|
||
assert.equal(
|
||
saved.options['street-level'].providerSwitches,
|
||
'alpha-0*beta-1',
|
||
);
|
||
manager.setLayerParams(
|
||
'street-level',
|
||
{ alpha: true },
|
||
{ origin: 'local-restore' },
|
||
);
|
||
saved = parseStoredLayerState(storage.getItem(LAYER_STATE_STORAGE_KEY));
|
||
assert.equal(
|
||
saved.options['street-level'].providerSwitches,
|
||
'alpha-0*beta-1',
|
||
);
|
||
} finally {
|
||
coordinator.destroy();
|
||
}
|
||
});
|
||
|
||
function enabledLayerParam(sink) {
|
||
return new URLSearchParams(encode(sink.provider())).get('l');
|
||
}
|
||
|
||
test('Clear All drops a saved layer a context mode had already switched off', async () => {
|
||
const manager = productionManager();
|
||
const storage = memoryStorage();
|
||
const sink = shareSink();
|
||
const coordinator = new LayerStateCoordinator(manager, sink, { storage });
|
||
await coordinator.start();
|
||
await manager.setEnabled('earthquakes', true, { origin: 'user' });
|
||
await manager.setEnabled('flights', true, { origin: 'user' });
|
||
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, [
|
||
'earthquakes',
|
||
'flights',
|
||
]);
|
||
// Context isolation is a temporary, passive takeover: the saved choice stays.
|
||
await manager.setEnabled('earthquakes', false, { origin: 'programmatic' });
|
||
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, [
|
||
'earthquakes',
|
||
'flights',
|
||
]);
|
||
|
||
const updatesBefore = sink.updates;
|
||
const result = await manager.clearSelectedLayers({ origin: 'user' });
|
||
assert.deepEqual(result.targetIds, ['flights']);
|
||
assert.ok(sink.updates > updatesBefore, 'the URL is told to refresh');
|
||
assert.deepEqual([...manager.getEnabledLayerIds()], []);
|
||
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, []);
|
||
assert.equal(enabledLayerParam(sink), '', 'the URL carries no layers');
|
||
assert.deepEqual(
|
||
parseStoredLayerState(storage.getItem(LAYER_STATE_STORAGE_KEY))
|
||
.enabledLayerIds,
|
||
[],
|
||
'a reload restores nothing',
|
||
);
|
||
coordinator.destroy();
|
||
});
|
||
|
||
test('Clear All drops a saved layer a dependency release superseded mid-batch', async () => {
|
||
let manager;
|
||
manager = productionManager({
|
||
// Contacts-style owner: its own teardown releases a borrowed dependency
|
||
// without awaiting it, which supersedes Clear All's turn for that layer.
|
||
'military-awareness': {
|
||
disable() {
|
||
void manager.setEnabled('flights', false, { origin: 'programmatic' });
|
||
return true;
|
||
},
|
||
},
|
||
});
|
||
const storage = memoryStorage();
|
||
const sink = shareSink();
|
||
const coordinator = new LayerStateCoordinator(manager, sink, { storage });
|
||
await coordinator.start();
|
||
await manager.setEnabled('earthquakes', true, { origin: 'user' });
|
||
await manager.setEnabled('flights', true, { origin: 'user' });
|
||
await manager.setEnabled('military-awareness', true, { origin: 'user' });
|
||
|
||
const result = await manager.clearSelectedLayers({ origin: 'user' });
|
||
assert.equal(
|
||
result.items.find((item) => item.id === 'flights')?.superseded,
|
||
true,
|
||
'the fixture reproduces the superseded flights turn',
|
||
);
|
||
assert.deepEqual([...manager.getEnabledLayerIds()], []);
|
||
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, []);
|
||
assert.equal(enabledLayerParam(sink), '');
|
||
coordinator.destroy();
|
||
});
|
||
|
||
test('Clear All keeps a layer a newer explicit request turned back on', async () => {
|
||
const manager = productionManager();
|
||
const sink = shareSink();
|
||
const coordinator = new LayerStateCoordinator(manager, sink, {
|
||
storage: memoryStorage(),
|
||
});
|
||
await coordinator.start();
|
||
await manager.setEnabled('earthquakes', true, { origin: 'user' });
|
||
await manager.setEnabled('flights', true, { origin: 'user' });
|
||
const clearing = manager.clearSelectedLayers({ origin: 'user' });
|
||
// A direct request issued during the batch owns its layer.
|
||
const reenable = manager.setEnabled('earthquakes', true, { origin: 'user' });
|
||
await Promise.all([clearing, reenable]);
|
||
assert.deepEqual([...manager.getEnabledLayerIds()], ['earthquakes']);
|
||
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, [
|
||
'earthquakes',
|
||
]);
|
||
assert.equal(enabledLayerParam(sink), 'e');
|
||
coordinator.destroy();
|
||
});
|
||
|
||
test('a passive clear never rewrites saved layers', async () => {
|
||
const manager = productionManager();
|
||
const storage = memoryStorage();
|
||
const coordinator = new LayerStateCoordinator(manager, shareSink(), {
|
||
storage,
|
||
});
|
||
await coordinator.start();
|
||
await manager.setEnabled('flights', true, { origin: 'user' });
|
||
await manager.setEnabled('flights', false, { origin: 'programmatic' });
|
||
const writes = storage.writes.length;
|
||
await manager.clearSelectedLayers({ origin: 'programmatic' });
|
||
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, ['flights']);
|
||
assert.equal(storage.writes.length, writes);
|
||
coordinator.destroy();
|
||
});
|
||
|
||
test('Clear All cancels a saved restore still waiting to start', async () => {
|
||
const gate = deferred();
|
||
const manager = productionManager();
|
||
const sink = shareSink();
|
||
const coordinator = new LayerStateCoordinator(manager, sink, {
|
||
storage: memoryStorage(),
|
||
restoreGate: gate.promise,
|
||
});
|
||
const params = new URLSearchParams('v=2&l=e.f');
|
||
const restore = coordinator.start({
|
||
shareLayerState: decodeLayerStateParams(params),
|
||
});
|
||
await manager.clearSelectedLayers({ origin: 'user' });
|
||
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, []);
|
||
gate.resolve();
|
||
const results = await restore;
|
||
for (const id of ['earthquakes', 'flights']) {
|
||
assert.equal(results.find((result) => result.layerId === id).succeeded, false);
|
||
}
|
||
assert.deepEqual([...manager.getEnabledLayerIds()], []);
|
||
assert.equal(enabledLayerParam(sink), '');
|
||
coordinator.destroy();
|
||
});
|