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:
manjunath committed 2026-10-30 19:02:47 +05:30
commit 578ebdc030
449 files changed
+30400 -3429

No files matched your search

+153
View File
@@ -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');
});
+23
View File
@@ -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);
+116 -1
View File
@@ -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');
+38 -3
View File
@@ -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' })
+4 -1
View File
@@ -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);