mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
merge main f13c60c into feat/voice-outlines
reconcile voice outlines with main f13c60c, retaining German/Japanese,
Operator HUD retirement and Context/Radio cleanup, named-building
containment, courtyard-hole, non-Latin identity, attribution, cancellation
and antimeridian repairs
keep outline callouts above the open voice preview when an above-panel
slot fits without colliding with another callout,
and clip the compact dock location label on narrow screens
This commit is contained in:
commit
578ebdc030
449 files changed
+30400
-3429
No files matched your search
@@ -417,3 +417,156 @@ test('terrain middleware retains successful chunks around a failure and retries
|
||||
);
|
||||
assert.equal(requests.length, 4);
|
||||
});
|
||||
|
||||
test('after a 429 the terrain proxy leaves the upstream alone and logs once', async (t) => {
|
||||
isolate(t);
|
||||
const warnings = [];
|
||||
t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' ')));
|
||||
let calls = 0;
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls += 1;
|
||||
return new Response('', { status: 429 });
|
||||
});
|
||||
t.mock.timers.enable({ apis: ['Date', 'setTimeout'], now: 1_000_000 });
|
||||
const request = install(terrainHeightsProxy());
|
||||
// The retries' backoff runs on mocked time.
|
||||
let settled = false;
|
||||
const first = request('/?points=1,2').then(() => {
|
||||
settled = true;
|
||||
});
|
||||
for (let step = 0; step < 60 && !settled; step += 1) {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
t.mock.timers.tick(1_000);
|
||||
}
|
||||
assert.ok(settled, 'the first request settles within its retry budget');
|
||||
await first;
|
||||
const afterFirst = calls;
|
||||
assert.ok(afterFirst >= 1);
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.match(warnings[0], /HTTP 429; pausing upstream for 60 s/);
|
||||
// New points during the pause: no upstream call, no new log line.
|
||||
const paused = await request('/?points=3,4');
|
||||
assert.equal(calls, afterFirst);
|
||||
assert.equal(paused.status, 502);
|
||||
assert.equal(warnings.length, 1);
|
||||
});
|
||||
|
||||
test('a far-off Retry-After pauses the terrain upstream for at most 10 minutes', async (t) => {
|
||||
isolate(t);
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
t.mock.timers.enable({ apis: ['Date', 'setTimeout'], now: 1_000_000 });
|
||||
let limited = true;
|
||||
t.mock.method(globalThis, 'fetch', async () =>
|
||||
limited
|
||||
? new Response('', { status: 429, headers: { 'retry-after': '86400' } })
|
||||
: Response.json({ results: [{ ellipsoid: 100 }] }),
|
||||
);
|
||||
const request = install(terrainHeightsProxy());
|
||||
// The first request spends its retry budget on 429s, then the pause begins.
|
||||
let settled = false;
|
||||
const first = request('/?points=1,2').then((res) => {
|
||||
settled = true;
|
||||
return res;
|
||||
});
|
||||
// Bounded: the retry budget is 10 s, so 60 simulated seconds is plenty.
|
||||
for (let step = 0; step < 60 && !settled; step += 1) {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
t.mock.timers.tick(1_000);
|
||||
}
|
||||
assert.ok(settled, 'the first request settles within its retry budget');
|
||||
assert.equal((await first).status, 502);
|
||||
limited = false;
|
||||
const pausedAt = Date.now();
|
||||
t.mock.timers.setTime(pausedAt + 9 * 60_000);
|
||||
assert.equal((await request('/?points=3,4')).status, 502, 'still paused');
|
||||
t.mock.timers.setTime(pausedAt + 10 * 60_000 + 1);
|
||||
assert.equal((await request('/?points=3,4')).status, 200, 'pause over');
|
||||
});
|
||||
|
||||
/** Drive mocked timers until `promise` settles; the retry budget is 10 s. */
|
||||
async function settleOnMockedTime(t, promise) {
|
||||
let settled = false;
|
||||
const tracked = promise.finally(() => {
|
||||
settled = true;
|
||||
});
|
||||
for (let step = 0; step < 60 && !settled; step += 1) {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
t.mock.timers.tick(1_000);
|
||||
}
|
||||
assert.ok(settled, 'the request settles within its retry budget');
|
||||
return tracked;
|
||||
}
|
||||
|
||||
test('during a terrain 429 pause fully cached points, even stale ones, are still served', async (t) => {
|
||||
isolate(t);
|
||||
t.mock.timers.enable({ apis: ['Date', 'setTimeout'], now: 1_000_000 });
|
||||
let calls = 0;
|
||||
let limited = false;
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls += 1;
|
||||
return limited
|
||||
? new Response('', { status: 429, headers: { 'retry-after': '300' } })
|
||||
: Response.json({ results: [{ ellipsoid: 100 }] });
|
||||
});
|
||||
const request = install(terrainHeightsProxy());
|
||||
assert.equal((await request('/?points=5,6')).status, 200);
|
||||
// Past the 30-day TTL, so 5,6 is stale and would normally be refreshed.
|
||||
t.mock.timers.setTime(Date.now() + 31 * 24 * 3600_000);
|
||||
limited = true;
|
||||
const first = await settleOnMockedTime(t, request('/?points=1,2'));
|
||||
assert.equal(first.status, 502);
|
||||
assert.equal(calls, 2, 'a Retry-After past the budget is not retried');
|
||||
const stale = await request('/?points=5,6');
|
||||
assert.equal(stale.status, 200);
|
||||
assert.deepEqual(json(stale).results, [{ ellipsoid: 100 }]);
|
||||
assert.equal((await request('/?points=5,6;7,8')).status, 502);
|
||||
assert.equal(calls, 2, 'no upstream call during the pause');
|
||||
});
|
||||
|
||||
test('a terrain upstream failure other than 429 does not pause later requests', async (t) => {
|
||||
isolate(t);
|
||||
t.mock.timers.enable({ apis: ['Date', 'setTimeout'], now: 1_000_000 });
|
||||
let calls = 0;
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls += 1;
|
||||
return new Response('', { status: 503 });
|
||||
});
|
||||
const request = install(terrainHeightsProxy());
|
||||
const first = await settleOnMockedTime(t, request('/?points=1,2'));
|
||||
assert.equal(first.status, 502);
|
||||
const afterFirst = calls;
|
||||
const second = await settleOnMockedTime(t, request('/?points=3,4'));
|
||||
assert.equal(second.status, 502);
|
||||
assert.ok(calls > afterFirst, 'new points still go upstream after a 503');
|
||||
});
|
||||
|
||||
test('a shorter concurrent Retry-After never cuts a terrain 429 pause short', async (t) => {
|
||||
isolate(t);
|
||||
t.mock.timers.enable({ apis: ['Date', 'setTimeout'], now: 1_000_000 });
|
||||
let calls = 0;
|
||||
t.mock.method(globalThis, 'fetch', async (raw) => {
|
||||
calls += 1;
|
||||
const points = new URL(raw).searchParams.get('points');
|
||||
if (points.startsWith('0.'))
|
||||
return Response.json({ results: [{ ellipsoid: 1 }] });
|
||||
const retryAfter = points.startsWith('1.') ? '300' : '30';
|
||||
return new Response('', {
|
||||
status: 429,
|
||||
headers: { 'retry-after': retryAfter },
|
||||
});
|
||||
});
|
||||
const request = install(terrainHeightsProxy());
|
||||
// Load the disk cache first so both requests below reach the upstream in
|
||||
// order: the 300 s answer lands first, the 30 s answer after it.
|
||||
assert.equal((await request('/?points=0,0')).status, 200);
|
||||
const [long, short] = await settleOnMockedTime(
|
||||
t,
|
||||
Promise.all([request('/?points=1,2'), request('/?points=3,4')]),
|
||||
);
|
||||
assert.equal(long.status, 502);
|
||||
assert.equal(short.status, 502);
|
||||
const afterBoth = calls;
|
||||
t.mock.timers.setTime(Date.now() + 120_000);
|
||||
assert.equal((await request('/?points=5,6')).status, 502);
|
||||
assert.equal(calls, afterBoth, 'still paused by the 300 s Retry-After');
|
||||
});
|
||||
@@ -729,6 +729,29 @@ test('Realtime service configuration selects compatible endpoint/model without f
|
||||
assert.doesNotMatch(response.body, /server-fixture|voice\.example/);
|
||||
});
|
||||
|
||||
test('the HUD summary status says whether a summary is configured, without calling OpenAI', async (t) => {
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
throw new Error('the status probe must not reach OpenAI');
|
||||
});
|
||||
const handler = install(openAiRealtimeProxy()).get('/api/openai/hud-summary');
|
||||
for (const [key, configured] of [
|
||||
[undefined, false],
|
||||
[' ', false],
|
||||
['fixture-key', true],
|
||||
]) {
|
||||
env(t, 'OPENAI_API_KEY', key);
|
||||
const response = await request(handler, { url: '/status' });
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(response.json(), { configured });
|
||||
assert.equal(response.headers['cache-control'], 'no-store');
|
||||
}
|
||||
assert.equal(
|
||||
(await request(handler, { url: '/' })).status,
|
||||
405,
|
||||
'only the status path answers GET',
|
||||
);
|
||||
});
|
||||
|
||||
test('OpenAI routes answer generically when the upstream or the request fails', async (t) => {
|
||||
env(t, 'OPENAI_API_KEY', 'fixture-upstream-secret');
|
||||
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
|
||||
|
||||
@@ -35,7 +35,7 @@ const tempDir = (t) => {
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
return dir;
|
||||
};
|
||||
const quick = { sleep: async () => {} };
|
||||
const quick = { sleep: async () => {}, minSpacingMs: 0 };
|
||||
const json = (value) => Response.json(value);
|
||||
const square = (lon, lat, d) => [
|
||||
[
|
||||
@@ -235,6 +235,49 @@ test('independent public gates sharing state enforce spacing and cap', async (t)
|
||||
starts.sort((a, b) => a - b);
|
||||
assert.ok(starts[1] - starts[0] >= 35, `${starts[1] - starts[0]}ms apart`);
|
||||
|
||||
// A slow durable write must move the next admission relative to the actual
|
||||
// fetch invocation, not the timestamp reserved before that write.
|
||||
const stalledFile = path.join(dir, 'stalled.json');
|
||||
let clock = Date.UTC(2026, 8, 29, 12);
|
||||
const actualStarts = [];
|
||||
const renameSync = fs.renameSync;
|
||||
let stalled = false;
|
||||
const rename = t.mock.method(fs, 'renameSync', (...args) => {
|
||||
if (args[1] === stalledFile && !stalled) {
|
||||
stalled = true;
|
||||
clock += 1500;
|
||||
}
|
||||
return renameSync(...args);
|
||||
});
|
||||
const delayedGate = () =>
|
||||
createNominatimGate({
|
||||
settings: PUBLIC,
|
||||
usage: createGateStateStore({ file: stalledFile }),
|
||||
now: () => clock,
|
||||
sleep: async (ms) => {
|
||||
clock += ms;
|
||||
},
|
||||
minSpacingMs: 1100,
|
||||
fetchImpl: async () => {
|
||||
actualStarts.push(clock);
|
||||
return json([]);
|
||||
},
|
||||
});
|
||||
try {
|
||||
await Promise.all([
|
||||
delayedGate().requestJson((endpoint) => `${endpoint}?q=delayed-one`),
|
||||
delayedGate().requestJson((endpoint) => `${endpoint}?q=delayed-two`),
|
||||
]);
|
||||
} finally {
|
||||
rename.mock.restore();
|
||||
}
|
||||
assert.equal(actualStarts.length, 2);
|
||||
assert.ok(actualStarts[1] - actualStarts[0] >= 1100);
|
||||
assert.equal(
|
||||
createGateStateStore({ file: stalledFile }).count('2026-09-29'),
|
||||
2,
|
||||
);
|
||||
|
||||
const capFile = path.join(dir, 'cap.json');
|
||||
let sent = 0;
|
||||
const capGate = () =>
|
||||
@@ -279,6 +322,78 @@ test('a persistence failure is reported and never lowers the count', (t) => {
|
||||
assert.equal(errors.length, 1, 'reported once, not per request');
|
||||
});
|
||||
|
||||
test('a persistence failure after fetch invocation aborts it and blocks restarts', async (t) => {
|
||||
const file = path.join(tempDir(t), 'state.json');
|
||||
let clock = Date.UTC(2026, 8, 29, 12);
|
||||
const markers = () =>
|
||||
fs
|
||||
.readdirSync(path.dirname(file))
|
||||
.filter((name) => name.startsWith('state.json.dispatch.'));
|
||||
const calls = [];
|
||||
const renameSync = fs.renameSync;
|
||||
let failStateWrite = false;
|
||||
const rename = t.mock.method(fs, 'renameSync', (...args) => {
|
||||
if (failStateWrite && args[1] === file) {
|
||||
failStateWrite = false;
|
||||
throw Object.assign(new Error('EIO'), { code: 'EIO' });
|
||||
}
|
||||
return renameSync(...args);
|
||||
});
|
||||
const make = (fetchImpl) =>
|
||||
createNominatimGate({
|
||||
settings: PUBLIC,
|
||||
usage: createGateStateStore({ file, onError: () => {} }),
|
||||
now: () => clock,
|
||||
sleep: async (ms) => {
|
||||
clock += ms;
|
||||
},
|
||||
minSpacingMs: 1100,
|
||||
fetchImpl,
|
||||
});
|
||||
let durableAtInvocation;
|
||||
let requestSignal;
|
||||
const gate = make((url, { signal }) => {
|
||||
calls.push(url);
|
||||
durableAtInvocation = JSON.parse(fs.readFileSync(file, 'utf8')).count;
|
||||
requestSignal = signal;
|
||||
failStateWrite = true;
|
||||
return new Promise((_, reject) =>
|
||||
signal.addEventListener('abort', () => reject(signal.reason), {
|
||||
once: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
try {
|
||||
await assert.rejects(
|
||||
gate.requestJson((endpoint) => `${endpoint}?q=x`),
|
||||
{
|
||||
code: 'NOMINATIM_STATE_UNAVAILABLE',
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
rename.mock.restore();
|
||||
}
|
||||
assert.equal(durableAtInvocation, 1, 'the count persisted before invocation');
|
||||
assert.ok(requestSignal.aborted);
|
||||
const orphan = markers();
|
||||
assert.equal(orphan.length, 1);
|
||||
assert.equal(gate.stats().persistence.dispatchPending, true);
|
||||
clock += 60_000;
|
||||
const restarted = make(async (url) => {
|
||||
calls.push(url);
|
||||
return json([]);
|
||||
});
|
||||
await assert.rejects(
|
||||
restarted.requestJson((endpoint) => `${endpoint}?q=y`),
|
||||
{
|
||||
code: 'NOMINATIM_STATE_UNAVAILABLE',
|
||||
},
|
||||
);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.deepEqual(markers(), orphan);
|
||||
assert.equal(createGateStateStore({ file }).count('2026-09-29'), 1);
|
||||
});
|
||||
|
||||
test('parseable but invalid public usage state fails closed', async (t) => {
|
||||
const dir = tempDir(t);
|
||||
const file = path.join(dir, 'state.json');
|
||||
|
||||
@@ -163,10 +163,10 @@ const emptyCache = () => ({
|
||||
|
||||
// ---------------------------------------------------------------- settings
|
||||
|
||||
test('endpoint settings: unset is public, empty disables, operator URLs are not public', () => {
|
||||
test('endpoint settings: unset and empty disable, operator URLs are explicit', () => {
|
||||
assert.deepEqual(resolveNominatimSettings({}), {
|
||||
endpoint: PUBLIC_NOMINATIM_SEARCH,
|
||||
isPublic: true,
|
||||
endpoint: null,
|
||||
isPublic: false,
|
||||
dailyCap: 50,
|
||||
});
|
||||
for (const off of ['', ' ', 'off', 'none', 'disabled'])
|
||||
@@ -860,6 +860,41 @@ test('the first polygon-bearing result of the right class is taken (Mumbai)', ()
|
||||
});
|
||||
|
||||
test('a result whose name does not answer the ask is passed over', () => {
|
||||
const city = (name, namedetails) =>
|
||||
row({
|
||||
category: 'place',
|
||||
type: 'city',
|
||||
addresstype: 'city',
|
||||
name,
|
||||
namedetails,
|
||||
});
|
||||
const york = { kind: 'city', query: 'York' };
|
||||
assert.equal(selectOutlineResult([city('New York')], york).outline, null);
|
||||
assert.ok(selectOutlineResult([city('York')], york).outline);
|
||||
assert.equal(
|
||||
selectOutlineResult([city('Le Havre')], { kind: 'city', query: 'Havre' })
|
||||
.outline,
|
||||
null,
|
||||
);
|
||||
assert.ok(
|
||||
selectOutlineResult([city('Le Havre')], { kind: 'city', query: 'Le Havre' })
|
||||
.outline,
|
||||
);
|
||||
assert.ok(
|
||||
selectOutlineResult([city('City of York', { short_name: 'York' })], york)
|
||||
.outline,
|
||||
);
|
||||
assert.equal(
|
||||
selectOutlineResult([city('新東京')], { kind: 'city', query: '東京' })
|
||||
.outline,
|
||||
null,
|
||||
);
|
||||
const tokyo = { kind: 'city', query: '東京' };
|
||||
assert.equal(selectOutlineResult([city('東京都')], tokyo).outline, null);
|
||||
assert.ok(
|
||||
selectOutlineResult([city('東京都', { short_name: '東京' })], tokyo)
|
||||
.outline,
|
||||
);
|
||||
const other = row({ category: 'leisure', type: 'park', name: 'Pease Park' });
|
||||
assert.equal(
|
||||
selectOutlineResult([other], { kind: 'landmark', query: 'Zilker Park' })
|
||||
|
||||
@@ -21,6 +21,7 @@ import { fetchRegionalJson } from '../../server/providers/regional/http.js';
|
||||
import {
|
||||
createNominatimGate,
|
||||
resolveNominatimSettings,
|
||||
PUBLIC_NOMINATIM_SEARCH,
|
||||
setSharedNominatimGate,
|
||||
} from '../../server/providers/regional/nominatimGate.js';
|
||||
import { localProviderPlugins } from '../../server/providers/local.js';
|
||||
@@ -39,7 +40,9 @@ test.beforeEach(() => {
|
||||
activeUpstream = transport;
|
||||
setSharedNominatimGate(
|
||||
createNominatimGate({
|
||||
settings: resolveNominatimSettings({}),
|
||||
settings: resolveNominatimSettings({
|
||||
NOMINATIM_URL: PUBLIC_NOMINATIM_SEARCH,
|
||||
}),
|
||||
fetchImpl: (...args) => {
|
||||
if (!transport.fetch) throw new Error('No active test upstream');
|
||||
return transport.fetch(...args);
|
||||
|
||||
Reference in new issue
Block a user