Files
gods-eye-view/src/contextModePolicy.test.mjs
T

590 lines
22 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
cockpitEntryAllowed,
contextAllowedLayerIds,
contextLayerEnableBlockReason,
contextRestoreLayerIds,
isExplicitUserIntentOrigin,
mergeContextTransitionErrors,
contextSnapshotLayerIds,
recordContextSessionUserChange,
recordContextRestoreExplicitChange,
runWithContextModeChanging,
settleContextModeChange,
settleContextIntentReplay,
settleUserFacingContextAction,
shouldCaptureContextSession,
shouldDeferContextEntryDuringClear,
shouldExitContextForLayerChange,
spaceMissionEntryCancellationDisposition,
} from './contextModePolicy.js';
test('Context transition errors preserve primary identity and aggregate failed layers', () => {
const primary = Object.assign(new Error('restore failed'), { failedLayerIds: ['flights'] });
const replay = Object.assign(new Error('replay failed'), { failedLayerIds: ['radio'] });
assert.equal(mergeContextTransitionErrors(primary, replay), primary);
assert.deepEqual(primary.failedLayerIds, ['flights', 'radio']);
});
test('Context transition errors retain a production coordinator failure with replay details', () => {
const coordinator = Object.assign(new Error('Contacts restore failed'), {
failedLayerIds: ['military-awareness'],
});
const replay = Object.assign(new Error('Radio replay failed'), {
failedLayerIds: ['radio'],
});
const merged = mergeContextTransitionErrors(coordinator, replay);
assert.deepEqual(merged.failedLayerIds, ['military-awareness', 'radio']);
});
test('explicit Context replay reports rejection and fulfilled-false failures after every sibling settles', async () => {
let releaseSlow;
const slow = new Promise((resolve) => { releaseSlow = resolve; });
const rejection = new Error('replay rejected');
const restoreState = {
explicitLayerStates: new Map([
['radio', true],
['slow-companion', false],
]),
};
let settled = false;
const replay = settleContextIntentReplay({
restoreState,
setEnabled: (layerId) => (layerId === 'radio' ? Promise.reject(rejection) : slow),
}).finally(() => { settled = true; });
await Promise.resolve();
assert.equal(settled, false);
releaseSlow(true);
assert.equal(await replay, rejection);
const semanticFailure = await settleContextIntentReplay({
restoreState: { explicitLayerStates: new Map([['radio', true]]) },
setEnabled: async () => false,
});
assert.match(semanticFailure.message, /Context intent replay failed for "radio"/);
});
test('a cancelled Context restore never replays an older explicit companion intent', async () => {
const calls = [];
const result = await settleContextIntentReplay({
restoreState: {
cancelled: true,
explicitLayerStates: new Map([['radio', true]]),
},
setEnabled: async (...args) => { calls.push(args); },
});
assert.equal(result, null);
assert.deepEqual(calls, []);
});
test('user-facing Context actions surface rejection and semantic false without rethrowing', async () => {
const surfaced = [];
const rejection = new Error('restore rejected');
assert.equal(await settleUserFacingContextAction({
operation: async () => { throw rejection; },
onFailure: (error) => surfaced.push(error),
}), false);
assert.equal(await settleUserFacingContextAction({
operation: async () => false,
onFailure: (error) => surfaced.push(error),
}), false);
assert.equal(surfaced[0], rejection);
assert.match(surfaced[1].message, /did not complete/);
assert.equal(await settleUserFacingContextAction({
operation: async () => false,
falseIsFailure: false,
onFailure: () => assert.fail('handled false must not surface twice'),
}), false);
assert.equal(await settleUserFacingContextAction({
operation: async () => { throw rejection; },
onFailure: () => { throw new Error('broken toast'); },
}), false);
});
const userEnable = (layerId) => ({
type: 'visibility',
layerId,
enabled: true,
origin: 'user',
});
test('user-enabled layers are additive and do not exit Context', () => {
assert.equal(shouldExitContextForLayerChange({
contextMode: 'flights',
globalContextEnabled: true,
change: userEnable('earthquakes'),
}), false);
assert.equal(shouldExitContextForLayerChange({
contextMode: 'space-missions',
globalContextEnabled: false,
change: userEnable('traffic'),
}), false);
assert.equal(shouldExitContextForLayerChange({
contextMode: null,
globalContextEnabled: true,
change: userEnable('earthquakes'),
}), false, 'the neutral shell uses the same permissive rule');
});
test('programmatic Context dependencies and direct mode setup remain allowed', () => {
assert.equal(shouldExitContextForLayerChange({
contextMode: 'space-missions',
globalContextEnabled: true,
change: { ...userEnable('satellites'), origin: 'programmatic' },
}), false);
});
test('Space Missions cancellation retains only a newer authoritative ON entry', () => {
const cancelledEntry = {
type: 'visibility-cancelled',
layerId: 'rocket-launches',
enabled: true,
intentEpoch: 8,
cancellationReason: 'superseded',
successorIntentEpoch: 9,
successorEnabled: true,
successorOrigin: 'programmatic',
};
assert.equal(spaceMissionEntryCancellationDisposition({
change: cancelledEntry,
}), 'replacement');
assert.equal(spaceMissionEntryCancellationDisposition({
change: { ...cancelledEntry, successorEnabled: false },
}), 'restore', 'a newer OFF releases the entry shell');
assert.equal(spaceMissionEntryCancellationDisposition({
change: {
...cancelledEntry,
cancellationReason: 'caller-abort',
successorIntentEpoch: undefined,
successorEnabled: undefined,
},
}), 'restore', 'caller abort at the same epoch restores the isolated session');
assert.equal(spaceMissionEntryCancellationDisposition({
change: { ...cancelledEntry, enabled: false },
currentIntentEpoch: 9,
effectivelyEnabled: true,
}), 'ignore');
});
test('only explicit user-owned shell enables capture a Context restoration snapshot', () => {
const userMissionEnable = {
type: 'visibility-requested',
layerId: 'rocket-launches',
enabled: true,
origin: 'user',
};
assert.equal(shouldCaptureContextSession(userMissionEnable), true);
assert.equal(shouldCaptureContextSession({ ...userMissionEnable, origin: 'voice' }), true);
assert.equal(shouldCaptureContextSession({ ...userMissionEnable, type: 'visibility-will-change' }), true);
assert.equal(shouldCaptureContextSession({ ...userMissionEnable, origin: 'programmatic' }), false);
assert.equal(shouldCaptureContextSession({ ...userMissionEnable, origin: 'dependency-restore' }), false);
assert.equal(shouldCaptureContextSession({ ...userMissionEnable, origin: 'context-restore' }), false);
assert.equal(shouldCaptureContextSession({ ...userMissionEnable, enabled: false }), false);
assert.equal(shouldCaptureContextSession({ ...userMissionEnable, layerId: 'satellites' }), false);
});
test('voice dependency OFF exits Space Missions like the equivalent UI action', () => {
assert.equal(shouldExitContextForLayerChange({
contextMode: 'space-missions',
globalContextEnabled: false,
change: { type: 'visibility', layerId: 'satellites', enabled: false, origin: 'voice' },
}), true);
});
test('only a newer explicit mission request is deferred during Clear All', () => {
const change = {
type: 'visibility-requested',
layerId: 'rocket-launches',
enabled: true,
origin: 'voice',
intentEpoch: 12,
};
assert.equal(shouldDeferContextEntryDuringClear({ change, clearInFlight: true }), true);
assert.equal(shouldDeferContextEntryDuringClear({ change, clearInFlight: false }), false);
assert.equal(shouldDeferContextEntryDuringClear({
change: { ...change, origin: 'programmatic' },
clearInFlight: true,
}), false);
assert.equal(shouldDeferContextEntryDuringClear({
change: { ...change, enabled: false },
clearInFlight: true,
}), false);
});
test('a rapid re-entry snapshots the settled restore target, not partial manager state', () => {
assert.deepEqual(
[...contextSnapshotLayerIds(
new Set(['rocket-launches', 'earthquakes']),
new Set(['flights', 'traffic']),
)],
['flights', 'traffic'],
);
assert.deepEqual(
[...contextSnapshotLayerIds(new Set(['earthquakes']))],
['earthquakes'],
);
assert.deepEqual(
[...contextSnapshotLayerIds(
new Set(['rocket-launches', 'satellites']),
null,
['rocket-launches'],
)],
['satellites'],
'the synchronous entry intent is not part of the pre-entry snapshot',
);
});
test('Space Missions blocks unrelated layer enables before replay data can mix', () => {
const change = { layerId: 'flights', enabled: true, origin: 'user' };
assert.match(
contextLayerEnableBlockReason({ contextMode: 'space-missions', change, layerName: 'Live Flights' }),
/Exit the mode to enable Live Flights/,
);
assert.equal(contextLayerEnableBlockReason({
contextMode: 'space-missions',
change: { ...change, layerId: 'satellites' },
}), null);
assert.equal(contextLayerEnableBlockReason({
contextMode: 'flights',
change,
}), null, 'live cockpit keeps additive user layers');
});
test('the Global Context shell and layers selected while Context is off remain allowed', () => {
assert.equal(shouldExitContextForLayerChange({
contextMode: null,
globalContextEnabled: true,
change: userEnable('military-awareness'),
}), false);
assert.equal(shouldExitContextForLayerChange({
contextMode: null,
globalContextEnabled: false,
change: userEnable('earthquakes'),
}), false);
});
test('manually disabling a mode dependency exits that Context bundle', () => {
assert.equal(shouldExitContextForLayerChange({
contextMode: 'flights',
globalContextEnabled: true,
change: { ...userEnable('military'), enabled: false },
}), true);
assert.equal(shouldExitContextForLayerChange({
contextMode: 'space-missions',
globalContextEnabled: false,
change: { ...userEnable('satellites'), enabled: false },
}), true);
});
test('Radio remains an independent companion across Context transitions', () => {
for (const contextMode of ['flights', 'space-missions']) {
for (const enabled of [true, false]) {
assert.equal(shouldExitContextForLayerChange({
contextMode,
globalContextEnabled: true,
change: { type: 'visibility', origin: 'user', layerId: 'radio', enabled },
}), false);
}
assert.equal(contextAllowedLayerIds(contextMode).has('radio'), true);
}
assert.equal(contextLayerEnableBlockReason({
contextMode: 'space-missions',
change: { type: 'visibility', origin: 'user', layerId: 'radio', enabled: true },
layerName: 'Radio',
}), null);
});
test('each Context mode exposes only its shell and dependencies', () => {
assert.deepEqual(
[...contextAllowedLayerIds(null)],
['military-awareness', 'radio'],
);
assert.deepEqual(
[...contextAllowedLayerIds('space-missions')],
['rocket-launches', 'satellites', 'radio'],
);
});
test('Context restoration keeps the pre-entry set and user-added layers', () => {
assert.deepEqual(
[...contextRestoreLayerIds({
enabledLayerIds: new Set(['flights', 'traffic']),
userAdded: new Set(['earthquakes', 'traffic']),
})],
['flights', 'traffic', 'earthquakes'],
);
});
test('Context restoration honors an explicit companion disable', () => {
assert.deepEqual(
[...contextRestoreLayerIds({
enabledLayerIds: new Set(['flights', 'radio']),
userAdded: new Set(['earthquakes']),
userRemoved: new Set(['radio']),
})],
['flights', 'earthquakes'],
);
});
test('the latest explicit Radio state wins across a Context session', () => {
for (const effectiveContextMode of [null, 'flights', 'space-missions']) {
for (const origin of ['user', 'voice']) {
const snapshot = { userAdded: new Set(), userRemoved: new Set() };
const record = (enabled) => recordContextSessionUserChange({
snapshot,
change: { type: 'visibility', origin, layerId: 'radio', enabled },
effectiveContextMode,
});
assert.equal(record(true), true);
assert.deepEqual([...snapshot.userAdded], ['radio']);
assert.deepEqual([...snapshot.userRemoved], []);
assert.equal(record(false), true);
assert.deepEqual([...snapshot.userAdded], []);
assert.deepEqual([...snapshot.userRemoved], ['radio']);
assert.equal(contextRestoreLayerIds(snapshot).has('radio'), false);
assert.equal(record(true), true);
assert.deepEqual([...snapshot.userAdded], ['radio']);
assert.deepEqual([...snapshot.userRemoved], []);
assert.equal(contextRestoreLayerIds(snapshot).has('radio'), true);
}
}
});
test('only direct UI and voice origins count as explicit Context intent', () => {
assert.equal(isExplicitUserIntentOrigin('user', 'cctv'), true);
assert.equal(isExplicitUserIntentOrigin('voice', 'radio'), true);
assert.equal(isExplicitUserIntentOrigin('voice', 'cctv'), true);
for (const origin of ['programmatic', 'context-restore', 'dependency', 'voice-cleanup', undefined]) {
assert.equal(isExplicitUserIntentOrigin(origin), false);
const snapshot = { userAdded: new Set(), userRemoved: new Set() };
assert.equal(recordContextSessionUserChange({
snapshot,
change: { type: 'visibility', origin, layerId: 'radio', enabled: true },
effectiveContextMode: 'space-missions',
}), false);
assert.equal(snapshot.userAdded.size, 0);
}
});
test('explicit layer intent finishing during restore overrides the stale queued target', () => {
for (const origin of ['user', 'voice']) {
const restoreState = {
enabledLayerIds: new Set(['flights']),
explicitLayerStates: new Map(),
};
assert.equal(recordContextRestoreExplicitChange({
restoreState,
change: { type: 'visibility', origin, layerId: 'cctv', enabled: true },
}), true);
assert.equal(restoreState.enabledLayerIds.has('cctv'), true);
assert.deepEqual([...restoreState.explicitLayerStates], [['cctv', true]]);
assert.equal(recordContextRestoreExplicitChange({
restoreState,
change: { type: 'visibility', origin: 'context-restore', layerId: 'cctv', enabled: false },
}), false);
assert.deepEqual([...restoreState.explicitLayerStates], [['cctv', true]]);
assert.equal(recordContextRestoreExplicitChange({
restoreState,
change: { type: 'visibility', origin, layerId: 'cctv', enabled: false },
}), true);
assert.equal(restoreState.enabledLayerIds.has('cctv'), false);
assert.deepEqual([...restoreState.explicitLayerStates], [['cctv', false]]);
}
});
test('cockpit entry requires Contacts context with both aircraft feeds enabled', () => {
assert.equal(cockpitEntryAllowed({
contextMode: 'flights',
contextModeChanging: false,
flightsEnabled: true,
militaryEnabled: true,
}), true);
assert.equal(cockpitEntryAllowed({
contextMode: 'flights',
contextModeChanging: true,
flightsEnabled: true,
militaryEnabled: true,
}), false, 'Cockpit remains unavailable until Contacts activation settles');
assert.equal(cockpitEntryAllowed({
contextMode: null,
contextModeChanging: false,
flightsEnabled: true,
militaryEnabled: true,
}), false, 'ordinary layer selection is not an operational Contacts context');
assert.equal(cockpitEntryAllowed({
contextMode: 'flights',
contextModeChanging: false,
flightsEnabled: true,
militaryEnabled: false,
}), false, 'the military contact feed is required');
assert.equal(cockpitEntryAllowed({
contextMode: 'flights',
contextModeChanging: false,
flightsEnabled: false,
militaryEnabled: true,
}), false, 'the civilian contact feed is required');
});
test('context teardown guard restores coordination state after success and failure', async () => {
const idleOwner = { _contextModeChanging: false };
await runWithContextModeChanging(idleOwner, async () => {
assert.equal(idleOwner._contextModeChanging, true);
});
assert.equal(idleOwner._contextModeChanging, false);
const activeOwner = { _contextModeChanging: true };
await assert.rejects(
runWithContextModeChanging(activeOwner, async () => {
assert.equal(activeOwner._contextModeChanging, true);
throw new Error('restore failed');
}),
/restore failed/,
);
assert.equal(activeOwner._contextModeChanging, true);
});
test('leaving a Context transaction re-publishes the settled state to the funnel', () => {
// Settle-gated consumers (the Contacts detection override) no-op while a
// transaction is in flight, so dropping the flag without re-running the
// funnel leaves them holding the pre-transaction state forever.
const syncs = [];
const owner = {
_contextModeChanging: true,
_syncContextModeButtons() { syncs.push(this._contextModeChanging); },
};
settleContextModeChange(owner);
assert.equal(owner._contextModeChanging, false);
assert.deepEqual(syncs, [false], 'the funnel runs AFTER the flag clears');
// Already settled: nothing to re-publish.
settleContextModeChange(owner);
assert.deepEqual(syncs, [false]);
// A nested scope handing back to a still-running outer transaction has
// settled nothing.
const nested = {
_contextModeChanging: true,
_syncContextModeButtons() { syncs.push('nested'); },
};
settleContextModeChange(nested, true);
assert.equal(nested._contextModeChanging, true);
assert.deepEqual(syncs, [false]);
settleContextModeChange(null); // no owner, no throw
});
test('the teardown guard re-publishes on the way out, so a mid-flight exit is not stranded', async () => {
// The field case: destroying a Contacts dependency layer exits the session
// inside the guard — the sync it calls there is gated out — and only the
// guard's own settle can let a settle-gated consumer see mode === null.
const observed = [];
const owner = {
_contextModeChanging: false,
_contextMode: 'flights',
_syncContextModeButtons() {
observed.push({ mode: this._contextMode, changing: this._contextModeChanging });
},
};
await runWithContextModeChanging(owner, async () => {
owner._contextMode = null;
owner._syncContextModeButtons(); // gated: changing is still true
});
assert.deepEqual(observed, [
{ mode: null, changing: true },
{ mode: null, changing: false },
], 'the exit is re-published once the transaction settles');
});
test('the session-starting entry layer is never bookkept as user-added (chip ON→OFF restores exactly)', () => {
const snapshot = { enabledLayerIds: new Set(['flights', 'ais-live-vessels']), userAdded: new Set() };
// The exact left-panel regression: the rockets enable event lands while the
// mode is still being entered (or even null); it must not survive its own exit.
for (const effectiveContextMode of ['space-missions', null]) {
recordContextSessionUserChange({
snapshot,
change: { type: 'visibility', layerId: 'rocket-launches', enabled: true, origin: 'user' },
effectiveContextMode,
});
assert.equal(snapshot.userAdded.has('rocket-launches'), false);
}
recordContextSessionUserChange({
snapshot,
change: { type: 'visibility', layerId: 'military-awareness', enabled: true, origin: 'user' },
effectiveContextMode: null,
});
assert.equal(snapshot.userAdded.size, 0);
assert.deepEqual(
[...contextRestoreLayerIds(snapshot)].sort(),
['ais-live-vessels', 'flights'],
);
});
test('mid-session user additions are recorded, removed on disable, and judged against the effective mode', () => {
const snapshot = { enabledLayerIds: new Set(['flights']), userAdded: new Set() };
recordContextSessionUserChange({
snapshot,
change: { type: 'visibility', layerId: 'cctv', enabled: true, origin: 'user' },
effectiveContextMode: 'flights',
});
assert.equal(snapshot.userAdded.has('cctv'), true);
// A dependency of the mode being ENTERED is not a user addition.
recordContextSessionUserChange({
snapshot,
change: { type: 'visibility', layerId: 'satellites', enabled: true, origin: 'user' },
effectiveContextMode: 'space-missions',
});
assert.equal(snapshot.userAdded.has('satellites'), false);
// Disabling a recorded addition removes it before any exit restore reads it.
recordContextSessionUserChange({
snapshot,
change: { type: 'visibility', layerId: 'cctv', enabled: false, origin: 'user' },
effectiveContextMode: 'flights',
});
assert.equal(snapshot.userAdded.has('cctv'), false);
});
test('a selected Context dependency becomes user-owned for exit restoration', () => {
const snapshot = { enabledLayerIds: new Set(), userAdded: new Set() };
assert.equal(recordContextSessionUserChange({
snapshot,
change: {
type: 'visibility',
layerId: 'flights',
enabled: true,
origin: 'user',
adoptedFromSelection: true,
},
effectiveContextMode: 'flights',
}), true);
assert.equal(snapshot.userAdded.has('flights'), true);
assert.equal(contextRestoreLayerIds(snapshot).has('flights'), true);
});
test('session bookkeeping ignores programmatic origins, non-visibility events, and a missing snapshot', () => {
const snapshot = { enabledLayerIds: new Set(), userAdded: new Set() };
assert.equal(recordContextSessionUserChange({
snapshot,
change: { type: 'visibility', layerId: 'cctv', enabled: true, origin: 'programmatic' },
effectiveContextMode: null,
}), false);
assert.equal(recordContextSessionUserChange({
snapshot,
change: { type: 'visibility-will-change', layerId: 'cctv', enabled: true, origin: 'user' },
effectiveContextMode: null,
}), false);
assert.equal(recordContextSessionUserChange({
snapshot: null,
change: { type: 'visibility', layerId: 'cctv', enabled: true, origin: 'user' },
effectiveContextMode: null,
}), false);
assert.equal(snapshot.userAdded.size, 0);
});