mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
- i18n scanner: lastError sink, no false sinks on defaults and ternaries - shared src/i18n/marker.js for modules that only mark strings - wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js - drop review IDs, change history and stale references from comments and docs Co-Authored-By: Claude Opus 5.5 <[email protected]>
270 lines
10 KiB
JavaScript
270 lines
10 KiB
JavaScript
// Camera-ownership policy for user-issued destinations. The ORDER is the
|
|
// contract: cockpit refuses before anything is released, the release happens
|
|
// before the flight, and a deferred flight retires the moment ANY newer
|
|
// navigation intent claims the camera.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
announceNavigationAuthority,
|
|
beginDeferredNavigation,
|
|
NAVIGATION_AUTHORITY_EVENT,
|
|
reassertNavigationHandoff,
|
|
runExplicitNavigation,
|
|
stampInitialShareGesture,
|
|
} from './navigationPolicy.js';
|
|
|
|
test('layer authority announcements distinguish passive autofocus from direct intent', () => {
|
|
const events = [];
|
|
const eventTarget = new EventTarget();
|
|
eventTarget.addEventListener(NAVIGATION_AUTHORITY_EVENT, (event) => events.push(event.detail));
|
|
announceNavigationAuthority('context-vessel-autofocus', {
|
|
eventTarget,
|
|
cancelPendingSelection: false,
|
|
});
|
|
announceNavigationAuthority('context-vessel-focus', { eventTarget });
|
|
assert.deepEqual(events, [
|
|
{ reason: 'context-vessel-autofocus', cancelPendingSelection: false },
|
|
{ reason: 'context-vessel-focus', cancelPendingSelection: true },
|
|
]);
|
|
});
|
|
|
|
test('an initial globe gesture cancels the passive shared Follow selection', () => {
|
|
const calls = [];
|
|
const result = stampInitialShareGesture((options) => {
|
|
calls.push(options);
|
|
return 7;
|
|
});
|
|
assert.equal(result, 7);
|
|
assert.deepEqual(calls, [{ cancelPendingSelection: true }]);
|
|
assert.equal(stampInitialShareGesture(null), undefined);
|
|
});
|
|
|
|
/** Records every policy side effect in the order it happened. */
|
|
function spy(overrides = {}) {
|
|
const log = [];
|
|
return {
|
|
log,
|
|
showToast: (text) => log.push(`toast:${text}`),
|
|
stamp: () => log.push('stamp'),
|
|
release: () => log.push('release'),
|
|
navigate: () => { log.push('navigate'); return 'flew'; },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The wiring StyleManager applies: one generation counter shared by every
|
|
* explicit navigation intent AND by cockpit taking the camera. Deferred
|
|
* flights capture their stamp and recheck it before flying.
|
|
*/
|
|
function navigator() {
|
|
const state = { generation: 0, cockpitActive: false, log: [] };
|
|
const stamp = () => { state.generation += 1; return state.generation; };
|
|
const release = () => state.log.push('release');
|
|
const showToast = (text) => state.log.push(`toast:${text}`);
|
|
return {
|
|
state,
|
|
/** One explicit intent that flies immediately. Returns its stamp, or false. */
|
|
navigate(noun) {
|
|
return runExplicitNavigation({
|
|
cockpitActive: state.cockpitActive,
|
|
noun,
|
|
showToast,
|
|
stamp,
|
|
release,
|
|
navigate: (generation) => {
|
|
state.log.push(`fly:${noun}`);
|
|
return generation;
|
|
},
|
|
});
|
|
},
|
|
/** An intent whose flight resolves later (the geocoded search). */
|
|
startDeferred(noun) {
|
|
return beginDeferredNavigation({
|
|
cockpitActive: state.cockpitActive,
|
|
noun,
|
|
showToast,
|
|
stamp,
|
|
});
|
|
},
|
|
/** The deferred flight finally resolving. */
|
|
resolveDeferred(generation, label) {
|
|
const cleared = reassertNavigationHandoff({
|
|
generation,
|
|
currentGeneration: state.generation,
|
|
cockpitActive: state.cockpitActive,
|
|
showToast,
|
|
release,
|
|
});
|
|
if (cleared) state.log.push(`fly:${label}`);
|
|
return cleared;
|
|
},
|
|
enterCockpit() {
|
|
// Mirrors CockpitViewController's onCameraTakeover.
|
|
stamp();
|
|
state.cockpitActive = true;
|
|
},
|
|
exitCockpit() { state.cockpitActive = false; },
|
|
};
|
|
}
|
|
|
|
test('a free camera is stamped, released, then flown — in that order', () => {
|
|
const s = spy();
|
|
const result = runExplicitNavigation({ cockpitActive: false, noun: 'location', ...s });
|
|
assert.equal(result, 'flew');
|
|
assert.deepEqual(s.log, ['stamp', 'release', 'navigate']);
|
|
});
|
|
|
|
test('the accepted intent hands its stamp to the flight', () => {
|
|
let seen = null;
|
|
runExplicitNavigation({ stamp: () => 42, navigate: (generation) => { seen = generation; } });
|
|
assert.equal(seen, 42, 'a deferred flight needs its stamp to recheck later');
|
|
});
|
|
|
|
test('cockpit refuses without stamping or releasing anything', () => {
|
|
// Releasing under cockpit destroys its hidden aircraft entity and the rig
|
|
// silently exits on the next update — the refusal must come first.
|
|
for (const noun of ['location', 'camera', 'vessel', 'fire']) {
|
|
const s = spy();
|
|
const result = runExplicitNavigation({ cockpitActive: true, noun, ...s });
|
|
assert.equal(result, false);
|
|
assert.deepEqual(s.log, [`toast:Exit cockpit to fly to a ${noun}`]);
|
|
}
|
|
});
|
|
|
|
test('weather layers name their destination in the Cockpit refusal', () => {
|
|
const expected = {
|
|
weather: 'Exit cockpit to view weather coverage',
|
|
cyclone: 'Exit cockpit to fly to a cyclone',
|
|
};
|
|
for (const [noun, text] of Object.entries(expected)) {
|
|
const s = spy();
|
|
runExplicitNavigation({ cockpitActive: true, noun, ...s });
|
|
assert.deepEqual(s.log, [`toast:${text}`]);
|
|
}
|
|
});
|
|
|
|
test('disposed navigation is inert before any camera or UI mutation', () => {
|
|
const s = spy();
|
|
const result = runExplicitNavigation({ disposed: true, cockpitActive: true, ...s });
|
|
assert.equal(result, false);
|
|
assert.deepEqual(s.log, []);
|
|
});
|
|
|
|
test('the refusal is a strict false, distinguishable from a flight result', () => {
|
|
const refused = runExplicitNavigation({ cockpitActive: true, showToast() {} });
|
|
assert.strictEqual(refused, false);
|
|
// A navigate() that legitimately returns undefined is not a refusal.
|
|
assert.strictEqual(runExplicitNavigation({ navigate: () => undefined }), undefined);
|
|
});
|
|
|
|
test('deferred handoff: the current request re-releases, then proceeds', () => {
|
|
const s = spy();
|
|
const ok = reassertNavigationHandoff({ generation: 4, currentGeneration: 4, ...s });
|
|
assert.equal(ok, true);
|
|
assert.deepEqual(s.log, ['release']);
|
|
});
|
|
|
|
test('deferred intent stamps without releasing a camera owner', () => {
|
|
const s = spy({ stamp: () => { s.log.push('stamp'); return 7; } });
|
|
assert.equal(beginDeferredNavigation({ ...s, noun: 'location' }), 7);
|
|
assert.deepEqual(s.log, ['stamp']);
|
|
});
|
|
|
|
test('disposed deferred intent is inert before stamp or UI mutation', () => {
|
|
const s = spy();
|
|
assert.equal(beginDeferredNavigation({ disposed: true, cockpitActive: true, ...s }), false);
|
|
assert.deepEqual(s.log, []);
|
|
});
|
|
|
|
test('disposed deferred work is inert before release', () => {
|
|
const s = spy();
|
|
assert.equal(reassertNavigationHandoff({
|
|
generation: 4,
|
|
currentGeneration: 4,
|
|
disposed: true,
|
|
...s,
|
|
}), false);
|
|
assert.deepEqual(s.log, []);
|
|
});
|
|
|
|
test('deferred handoff: a superseded request neither flies nor releases', () => {
|
|
// The newer intent owns the camera now — releasing here would yank it.
|
|
const s = spy();
|
|
const ok = reassertNavigationHandoff({ generation: 3, currentGeneration: 4, ...s });
|
|
assert.equal(ok, false);
|
|
assert.deepEqual(s.log, [], 'a stale flight must be completely inert');
|
|
});
|
|
|
|
test('deferred handoff: cockpit taken mid-flight refuses and explains', () => {
|
|
const s = spy();
|
|
const ok = reassertNavigationHandoff({
|
|
generation: 4, currentGeneration: 4, cockpitActive: true, ...s,
|
|
});
|
|
assert.equal(ok, false);
|
|
assert.deepEqual(s.log, ['toast:Exit cockpit to fly to a location']);
|
|
});
|
|
|
|
test('deferred handoff: supersession is checked before cockpit, so it stays silent', () => {
|
|
const s = spy();
|
|
reassertNavigationHandoff({ generation: 3, currentGeneration: 4, cockpitActive: true, ...s });
|
|
assert.deepEqual(s.log, [], 'a stale request must not toast on the user');
|
|
});
|
|
|
|
// Interleavings: the generation advances on EVERY explicit intent, not just on
|
|
// another search. A search-only token left all of these open — the stale search
|
|
// still held the current token and flew over the newer destination.
|
|
test('interleaving: a canned destination during a search retires the search', () => {
|
|
const nav = navigator();
|
|
const searchGeneration = nav.startDeferred('location');
|
|
nav.navigate('location'); // user clicks a city pill while the geocode runs
|
|
assert.equal(nav.resolveDeferred(searchGeneration, 'search'), false);
|
|
assert.deepEqual(nav.state.log, ['release', 'fly:location']);
|
|
assert.ok(!nav.state.log.includes('fly:search'), 'the stale search must not fly');
|
|
});
|
|
|
|
test('interleaving: a clicked vessel during a search retires the search', () => {
|
|
const nav = navigator();
|
|
const searchGeneration = nav.startDeferred('location');
|
|
nav.navigate('vessel');
|
|
assert.equal(nav.resolveDeferred(searchGeneration, 'search'), false);
|
|
assert.deepEqual(nav.state.log, ['release', 'fly:vessel']);
|
|
});
|
|
|
|
test('interleaving: a CCTV focus during a search retires the search', () => {
|
|
const nav = navigator();
|
|
const searchGeneration = nav.startDeferred('location');
|
|
nav.navigate('camera');
|
|
assert.equal(nav.resolveDeferred(searchGeneration, 'search'), false);
|
|
assert.ok(!nav.state.log.includes('fly:search'));
|
|
});
|
|
|
|
test('interleaving: cockpit entered AND exited during a search still retires it', () => {
|
|
// The cockpit flag is back to false by the time the flight resolves, so only
|
|
// the generation stamped by the takeover can catch this one.
|
|
const nav = navigator();
|
|
const searchGeneration = nav.startDeferred('location');
|
|
nav.enterCockpit();
|
|
nav.exitCockpit();
|
|
assert.equal(nav.state.cockpitActive, false);
|
|
assert.equal(nav.resolveDeferred(searchGeneration, 'search'), false);
|
|
assert.deepEqual(nav.state.log, [], 'the deferred search never released');
|
|
});
|
|
|
|
test('interleaving: an uninterrupted search still flies', () => {
|
|
const nav = navigator();
|
|
const searchGeneration = nav.startDeferred('location');
|
|
assert.equal(nav.resolveDeferred(searchGeneration, 'search'), true);
|
|
assert.deepEqual(nav.state.log, ['release', 'fly:search']);
|
|
});
|
|
|
|
test('interleaving: the newest of two searches wins', () => {
|
|
const nav = navigator();
|
|
const first = nav.startDeferred('location');
|
|
const second = nav.startDeferred('location');
|
|
assert.equal(nav.resolveDeferred(first, 'first'), false);
|
|
assert.equal(nav.resolveDeferred(second, 'second'), true);
|
|
assert.ok(!nav.state.log.includes('fly:first'));
|
|
assert.ok(nav.state.log.includes('fly:second'));
|
|
});
|