Merge branch 'main' into codex/overpass-admission-review-fixes

This commit is contained in:
Said Turksever authored and GitHub committed 2026-11-03 15:07:08 +00:00
commit 34906c9abb
1407 files changed
+118138 -22562

No files matched your search

+17 -12
View File
@@ -1,22 +1,27 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { expandApplicationHtml, APPLICATION_TEMPLATES } from '../../build/application-html.js';
import { APPLICATION_VIEWS } from '../ui/views/index.js';
import { readApplicationHtml } from '../testSupport/applicationHtml.mjs';
test('the standalone document expands every component once and preserves unique element ids', () => {
const source = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
const html = expandApplicationHtml(source);
assert.equal([...source.matchAll(/gev:template /g)].length, APPLICATION_TEMPLATES.length);
assert.doesNotMatch(html, /gev:template/);
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map(match => match[1]);
const source = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
test('the standalone document hosts every view once and keeps element ids unique', async () => {
const hosts = [...source.matchAll(/data-gev-view="([^"]+)"/g)].map((match) => match[1]);
// Order is stacking order for fixed elements that share a z-index.
assert.deepEqual(hosts, ['scene-chrome', 'cockpit', 'display-controls', 'command-dock', 'layer-panels', 'context', 'welcome', 'provider-settings', 'intel-hud']);
assert.deepEqual([...hosts].sort(), Object.keys(APPLICATION_VIEWS).sort());
const html = await readApplicationHtml();
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
assert.equal(new Set(ids).size, ids.length);
assert.match(html, /id="cesiumContainer"/);
assert.match(html, /type="module" src="\/src\/main.js"/);
});
test('component selection includes only requested markup and refuses filesystem traversal', () => {
const html = expandApplicationHtml('<!-- gev:template welcome -->\n');
assert.match(html, /id="first-run-launcher"/);
assert.doesNotMatch(html, /id="cesiumContainer"/);
assert.throws(() => expandApplicationHtml('<!-- gev:template ../../.env -->'), /Unknown application template/);
test('the loading screen is static markup, so it paints before any script runs', () => {
const loading = source.indexOf('id="loading-screen"');
assert.ok(loading > 0);
assert.ok(loading < source.indexOf('type="module" src="/src/main.js"'));
assert.match(source, /class="loader-status" data-i18n>Initializing photorealistic world\.\.\.</);
assert.doesNotMatch(source, /gev:template/);
});
+160 -1
View File
@@ -6,6 +6,12 @@ import { tomtomProxy } from 'gods-eye-view/server/providers/traffic';
import { firmsProxy } from 'gods-eye-view/server/providers/firms';
import { gbfsProxy } from 'gods-eye-view/server/providers/gbfs';
import { localProviderPlugins } from '../../server/providers/local.js';
import { makeFixtureRoot } from './fixtureRoot.mjs';
const nominatimStorageDir = await makeFixtureRoot(
'gev-environment-composition-',
);
test.after(() => fsp.rm(nominatimStorageDir, { recursive: true, force: true }));
function install(plugin, hook = 'configureServer') {
const routes = new Map();
@@ -88,7 +94,7 @@ test('standalone composition mounts every extracted provider exactly once withou
t.mock.method(globalThis, 'fetch', () => {
throw Error('construction must not fetch');
});
const plugins = localProviderPlugins();
const plugins = localProviderPlugins({ nominatimStorageDir });
for (const factory of [
terrainHeightsProxy,
tomtomProxy,
@@ -411,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');
});
+13 -40
View File
@@ -9,6 +9,8 @@ import {
} from '../../server/providers/space/launch-library.js';
import { keySetupStatus } from '../keySetupCore.mjs';
import { initKeySetup } from '../keySetup.js';
import { createPinia } from 'pinia';
import { useProviderSettingsStore } from '../ui/stores/providerSettings.js';
test('LL2 requests from the same UTC minute share a cache key without mutating callers', () => {
const first = new Date('2026-10-17T16:20:00.001Z');
@@ -57,60 +59,31 @@ test('LL2 headers identify the app and preserve optional server-only authorizati
});
test('actual setup rendering distinguishes optional paid LL2 from unrelated free keys', async () => {
const created = [];
const node = () => {
const value = {
dataset: {},
children: [],
append(...children) {
this.children.push(...children);
},
querySelector() {
return null;
},
querySelectorAll() {
return [];
},
addEventListener() {},
removeEventListener() {},
setAttribute() {},
remove() {},
};
created.push(value);
return value;
};
const rows = node();
const root = node();
root.querySelector = (selector) =>
selector === '[data-key-setup-rows]' ? rows : null;
const chip = node();
const store = useProviderSettingsStore(createPinia());
const status = keySetupStatus({});
status.keys = status.keys.filter((key) =>
['launch-library', 'mapillary'].includes(key.id),
);
const setup = await initKeySetup({
documentRef: {
getElementById: (id) => (id === 'key-setup-chip' ? chip : root),
createElement: node,
getElementById: (id) => (id === 'key-setup' ? { dataset: {} } : null),
addEventListener() {},
removeEventListener() {},
},
store,
fetchImpl: async () => ({ ok: true, json: async () => status }),
});
assert.ok(setup);
try {
const ll2 = rows.children.find(
(row) => row.dataset.keyId === 'launch-library',
const row = (id) => store.view.rows.find((key) => key.id === id);
const ll2 = row('launch-library');
assert.match(ll2.tierTitle, /optional paid token/i);
assert.match(ll2.unlocks, /leave blank/i);
assert.match(ll2.unlocks, /no key/i);
assert.equal(
row('mapillary').tierTitle,
'Free key — register, paste, done',
);
const free = rows.children.find((row) => row.dataset.keyId === 'mapillary');
const tier = (row) =>
row.children[0].children.find(
(child) => child.className === 'key-setup-tier',
);
assert.match(tier(ll2).title, /optional paid token/i);
assert.match(ll2.children[1].textContent, /leave blank/i);
assert.match(ll2.children[1].textContent, /no key/i);
assert.equal(tier(free).title, 'Free key — register, paste, done');
const serialized = JSON.stringify(status);
assert.ok(!serialized.includes('owned-fixture-token'));
} finally {
+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);
+5 -1
View File
@@ -6,6 +6,10 @@ import path from 'node:path';
import { cctvProxy } from '../../server/providers/cctv.js';
import { radioBrowserProxy } from '../../server/providers/radio.js';
import { localProviderPlugins } from '../../server/providers/local.js';
import { makeFixtureRoot } from './fixtureRoot.mjs';
const nominatimStorageDir = await makeFixtureRoot('gev-media-composition-');
test.after(() => rmSync(nominatimStorageDir, { recursive: true, force: true }));
function install(plugin, hook = 'configureServer') {
let handler;
@@ -97,7 +101,7 @@ test('CCTV instances resolve their own application source root and isolate catal
test('composition creates exactly one CCTV and radio provider without acquisition', (t) => {
isolate(t);
const plugins = localProviderPlugins();
const plugins = localProviderPlugins({ nominatimStorageDir });
for (const factory of [cctvProxy, radioBrowserProxy]) {
assert.equal(plugins.filter((p) => p.name === factory().name).length, 1);
}
@@ -0,0 +1,994 @@
// Review regressions for the Nominatim gate: state that survives restarts and
// sweeps, several processes sharing one count, long refusals, stalled bodies,
// request-equivalent cache keys, and stricter outline selection.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import {
createGateStateStore,
createNominatimGate,
resolveNominatimSettings,
PUBLIC_NOMINATIM_SEARCH,
} from '../../server/providers/regional/nominatimGate.js';
import { createNominatimCache } from '../../server/providers/regional/nominatimCache.js';
import {
outlineBias,
selectOutlineResult,
} from '../../server/providers/regional/outlineSelect.js';
import {
createNominatimOutlineProvider,
createNominatimSearchProvider,
geocodeProxy,
} from '../../server/providers/regional/place.js';
const PUBLIC = {
endpoint: PUBLIC_NOMINATIM_SEARCH,
isPublic: true,
dailyCap: 50,
};
const LOCAL = { ...PUBLIC, isPublic: false };
const tempDir = (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-nominatim-'));
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
return dir;
};
const quick = { sleep: async () => {}, minSpacingMs: 0 };
const json = (value) => Response.json(value);
const square = (lon, lat, d) => [
[
[lon, lat],
[lon + d, lat],
[lon + d, lat + d],
[lon, lat + d],
[lon, lat],
],
];
function mount(options) {
let handler;
geocodeProxy(options).configureServer({
middlewares: { use: (_route, fn) => (handler = fn) },
});
return (url) =>
new Promise((resolve, reject) => {
const res = {
writableEnded: false,
on() {},
writeHead(status) {
res.status = status;
},
end(body) {
res.writableEnded = true;
resolve({ status: res.status, body: JSON.parse(body) });
},
};
Promise.resolve(
handler(
{
method: 'GET',
url,
headers: {},
socket: { remoteAddress: `10.9.${Math.random()}` },
on() {},
},
res,
),
).catch(reject);
});
}
test('the daily count survives cache sweeps and a restart of the real composition', async (t) => {
const storageDir = tempDir(t);
const fetchImpl = async () => json([]);
const first = createNominatimGate({ settings: PUBLIC, fetchImpl, ...quick });
const request = mount({ storageDir, gate: first });
for (let i = 0; i < 3; i++)
assert.equal((await request(`/?q=place-${i}`)).status, 200);
assert.equal(first.stats().usedToday, 3);
// A foreign file in the answers directory and a full sweep leave the
// state alone and the stranger untouched.
const answers = path.join(storageDir, 'answers');
fs.writeFileSync(path.join(answers, 'notes.json'), '{}');
await createNominatimCache({ dir: answers, sweepEvery: 1 }).sweep();
assert.ok(fs.existsSync(path.join(answers, 'notes.json')));
assert.ok(fs.existsSync(path.join(storageDir, 'state.json')));
// Restart: a new gate composed on the same directory, which sweeps at start.
const second = createNominatimGate({
settings: { ...PUBLIC, dailyCap: 4 },
fetchImpl,
...quick,
});
const again = mount({ storageDir, gate: second });
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(second.stats().usedToday, 3, 'the count came back');
assert.equal((await again('/?q=place-new')).status, 200);
const capped = await again('/?q=place-over');
assert.equal(
capped.status,
429,
'the cap still counts yesterday-restart usage',
);
assert.equal(capped.body.code, 'NOMINATIM_DAILY_CAP');
});
test('a sweep removes expired and corrupt entries, never ones it could not read', async (t) => {
const dir = tempDir(t);
const file = (n) => path.join(dir, `${String(n).repeat(40)}.json`);
const live = {
key: 'k',
payload: null,
storedAt: 1,
expiresAt: Date.now() + 60_000,
};
fs.writeFileSync(file(1), JSON.stringify(live));
fs.writeFileSync(file(2), JSON.stringify({ ...live, expiresAt: 1 }));
fs.writeFileSync(file(3), '{not json');
const cache = createNominatimCache({ dir });
// Reads failing (a busy disk, a test double) must not empty the cache.
const fsp = (await import('node:fs/promises')).default;
const readFile = t.mock.method(fsp, 'readFile', async () => {
throw Object.assign(new Error('EMFILE'), { code: 'EMFILE' });
});
await cache.sweep();
assert.deepEqual(
[1, 2, 3].map((n) => fs.existsSync(file(n))),
[true, true, true],
);
readFile.mock.restore();
await cache.sweep();
assert.deepEqual(
[1, 2, 3].map((n) => fs.existsSync(file(n))),
[true, false, false],
);
});
test('processes sharing state space public sends and cap atomically', async (t) => {
const dir = tempDir(t);
const module = new URL(
'../../server/providers/regional/nominatimGate.js',
import.meta.url,
).href;
const run = (file, dailyCap) => {
const script = `
const { createGateStateStore } = await import(${JSON.stringify(module)});
const store = createGateStateStore({ file: ${JSON.stringify(file)} });
const latestStartAt = Date.now() + 10000;
let sentAt = null;
for (;;) {
const result = store.dispatch(
'2026-09-29',
{ clock: Date.now, dailyCap: ${dailyCap}, minSpacingMs: 300, latestStartAt },
() => {
sentAt = Date.now();
return Promise.resolve();
},
);
if (result.status === 'wait') {
await new Promise((resolve) => setTimeout(resolve, result.waitMs));
continue;
}
console.log(JSON.stringify({ status: result.status, sentAt }));
break;
}
`;
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [
'--input-type=module',
'-e',
script,
]);
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => (stdout += chunk));
child.stderr.on('data', (chunk) => (stderr += chunk));
child.on('exit', (code) =>
code === 0
? resolve(JSON.parse(stdout))
: reject(new Error(`exit ${code}: ${stderr}`)),
);
});
};
const spacedFile = path.join(dir, 'spaced.json');
const sends = await Promise.all([
run(spacedFile, 3),
run(spacedFile, 3),
run(spacedFile, 3),
]);
assert.deepEqual(
sends.map((result) => result.status),
['started', 'started', 'started'],
);
// Spacing is measured between actual transport invocations.
const sentAt = sends.map((result) => result.sentAt).sort((a, b) => a - b);
for (let i = 1; i < sentAt.length; i += 1)
assert.ok(
sentAt[i] - sentAt[i - 1] >= 300,
`${sentAt[i] - sentAt[i - 1]}ms apart`,
);
assert.equal(
createGateStateStore({ file: spacedFile }).count('2026-09-29'),
3,
);
const cappedFile = path.join(dir, 'capped.json');
const capped = await Promise.all([run(cappedFile, 1), run(cappedFile, 1)]);
assert.deepEqual(capped.map((result) => result.status).sort(), [
'cap',
'started',
]);
assert.equal(
createGateStateStore({ file: cappedFile }).count('2026-09-29'),
1,
);
});
test('independent public gates sharing state enforce spacing and cap', async (t) => {
const dir = tempDir(t);
const starts = [];
const fetchImpl = async () => {
starts.push(Date.now());
return json([]);
};
const file = path.join(dir, 'spacing.json');
const make = (options = {}) =>
createNominatimGate({
settings: PUBLIC,
fetchImpl,
usage: createGateStateStore({ file: options.file || file }),
minSpacingMs: options.minSpacingMs || 40,
});
await Promise.all([
createNominatimSearchProvider({ gate: make() })('one', null),
createNominatimSearchProvider({ gate: make() })('two', null),
]);
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 = () =>
createNominatimGate({
settings: { ...PUBLIC, dailyCap: 1 },
fetchImpl: async () => {
sent += 1;
return json([]);
},
usage: createGateStateStore({ file: capFile }),
minSpacingMs: 1,
});
const results = await Promise.allSettled([
createNominatimSearchProvider({ gate: capGate() })('three', null),
createNominatimSearchProvider({ gate: capGate() })('four', null),
]);
assert.equal(
results.filter((result) => result.status === 'fulfilled').length,
1,
);
assert.equal(
results.find((result) => result.status === 'rejected').reason.code,
'NOMINATIM_DAILY_CAP',
);
assert.equal(sent, 1);
});
test('a persistence failure is reported and never lowers the count', (t) => {
const dir = tempDir(t);
const blocker = path.join(dir, 'not-a-dir');
fs.writeFileSync(blocker, 'x');
const errors = [];
const store = createGateStateStore({
file: path.join(blocker, 'state.json'),
onError: (error) => errors.push(error),
});
store.increment('2026-09-29');
store.increment('2026-09-29');
assert.equal(store.count('2026-09-29'), 2);
assert.equal(store.status().persisted, false);
assert.ok(store.status().error);
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);
// Long past any request's timeout, the next lock owner reclaims it.
clock += 10 * 60_000;
const resumed = make(async (url) => {
calls.push(url);
return json([]);
});
assert.deepEqual(
await resumed.requestJson((endpoint) => `${endpoint}?q=z`),
[],
);
assert.equal(calls.length, 2);
assert.deepEqual(markers(), []);
assert.equal(createGateStateStore({ file }).count('2026-09-29'), 2);
});
test('a dispatch marker that names no start keeps refusing', async (t) => {
const file = path.join(tempDir(t), 'state.json');
fs.writeFileSync(`${file}.dispatch.123.456`, '');
const gate = createNominatimGate({
settings: PUBLIC,
usage: createGateStateStore({ file, onError: () => {} }),
now: () => Date.UTC(2026, 8, 29, 12),
sleep: async () => {},
fetchImpl: async () =>
assert.fail('no request while a dispatch is unfinished'),
});
await assert.rejects(
gate.requestJson((endpoint) => `${endpoint}?q=x`),
{
code: 'NOMINATIM_STATE_UNAVAILABLE',
},
);
});
test('parseable but invalid public usage state fails closed', async (t) => {
const dir = tempDir(t);
const file = path.join(dir, 'state.json');
fs.writeFileSync(
file,
JSON.stringify({
day: '2026-09-30',
count: '50',
pausedUntil: '9999999999999',
}),
);
let sent = 0;
const gate = createNominatimGate({
settings: PUBLIC,
fetchImpl: async () => {
sent += 1;
return json([]);
},
usage: createGateStateStore({ file, onError: () => {} }),
...quick,
});
await assert.rejects(
gate.requestJson((endpoint) => `${endpoint}?q=x`),
{ code: 'NOMINATIM_STATE_UNAVAILABLE' },
);
assert.equal(sent, 0);
fs.writeFileSync(
file,
JSON.stringify({ day: '2026-09-30', count: 1, pausedUntil: 0 }),
);
const migrated = createGateStateStore({ file });
assert.equal(
migrated.dispatch(
'2026-09-30',
{ clock: () => 1000, dailyCap: 50, minSpacingMs: 1100 },
() => Promise.resolve(),
).status,
'started',
'the original state format without nextStartAt remains supported',
);
});
test('a long Retry-After is honoured in full and survives a restart', async (t) => {
const file = path.join(tempDir(t), 'state.json');
let clock = Date.UTC(2026, 8, 29, 12);
const now = () => clock;
const refused = createNominatimGate({
settings: PUBLIC,
fetchImpl: async () =>
new Response('', { status: 429, headers: { 'Retry-After': '86400' } }),
usage: createGateStateStore({ file }),
now,
...quick,
});
await assert.rejects(
createNominatimSearchProvider({ gate: refused })('x', null),
{
code: 'NOMINATIM_REFUSED',
},
);
const calls = [];
const restarted = createNominatimGate({
settings: PUBLIC,
fetchImpl: async (url) => {
calls.push(url);
return json([]);
},
usage: createGateStateStore({ file }),
now,
...quick,
});
const search = createNominatimSearchProvider({ gate: restarted });
clock += 7 * 3_600_000; // past the old six-hour ceiling
await assert.rejects(search('y', null), (error) => {
assert.equal(error.code, 'NOMINATIM_PAUSED');
assert.equal(error.retryAfterMs, 17 * 3_600_000);
return true;
});
assert.equal(calls.length, 0);
clock += 17 * 3_600_000 + 1000;
await search('y', null);
assert.equal(calls.length, 1);
});
test('an operator refusal pauses only the endpoint that refused', async () => {
let active = {
endpoint: 'https://a.example.test/search',
isPublic: false,
dailyCap: 50,
};
const calls = [];
const gate = createNominatimGate({
settings: () => active,
fetchImpl: async (url) => {
calls.push(new URL(url).host);
return calls.length === 1
? new Response('', {
status: 429,
headers: { 'Retry-After': '60' },
})
: json([]);
},
...quick,
});
const request = () => gate.requestJson((endpoint) => `${endpoint}?q=x`);
await assert.rejects(request(), { code: 'NOMINATIM_REFUSED' });
active = { ...active, endpoint: 'https://b.example.test/search' };
assert.deepEqual(await request(), []);
active = { ...active, endpoint: 'https://a.example.test/search' };
await assert.rejects(request(), { code: 'NOMINATIM_PAUSED' });
assert.deepEqual(calls, ['a.example.test', 'b.example.test']);
});
test('a stalled body times out instead of holding the shared queue', async () => {
let cancelled = 0;
const stalled = (signal) => {
const body = new ReadableStream({
pull: () => new Promise(() => {}),
cancel: () => {
cancelled += 1;
},
});
signal.addEventListener('abort', () => body.cancel().catch(() => {}));
return new Response(body, { status: 200 });
};
let n = 0;
const gate = createNominatimGate({
settings: { ...PUBLIC, isPublic: false },
fetchImpl: async (_url, { signal }) =>
++n <= 2 ? stalled(signal) : json([]),
timeoutMs: 150,
});
const search = createNominatimSearchProvider({ gate });
const started = Date.now();
await assert.rejects(search('stall', null), { code: 'NOMINATIM_TRANSPORT' });
assert.ok(Date.now() - started < 3000);
assert.ok(cancelled >= 1, 'the body read was cancelled');
});
test('cache keys are request-equivalent: a miss for "Paris???" never answers "Paris"', async () => {
const calls = [];
const gate = createNominatimGate({
settings: LOCAL,
fetchImpl: async (url) => {
calls.push(new URL(url).searchParams.get('q'));
return json(
calls.length === 1
? []
: [
{
lat: '48.85',
lon: '2.35',
display_name: 'Paris, France',
name: 'Paris',
addresstype: 'city',
},
],
);
},
...quick,
});
const search = createNominatimSearchProvider({ gate });
assert.equal((await search('Paris???', null)).status, 'ZERO_RESULTS');
assert.equal((await search('Paris', null)).status, 'OK');
assert.deepEqual(calls, ['Paris???', 'Paris']);
// Whitespace-only differences are the same upstream request.
assert.equal((await search(' Paris ', null)).cached, true);
});
test('a malformed answer is a failure, not a cached "no result"', async () => {
let shape = { error: 'Unexpected' };
let calls = 0;
const gate = createNominatimGate({
settings: LOCAL,
fetchImpl: async () => {
calls += 1;
return json(shape);
},
...quick,
});
const outline = createNominatimOutlineProvider({ gate });
const search = createNominatimSearchProvider({ gate });
await assert.rejects(outline({ query: 'Zilker Park', kind: 'landmark' }), {
code: 'NOMINATIM_MALFORMED',
});
await assert.rejects(search('Zilker Park', null), {
code: 'NOMINATIM_MALFORMED',
});
shape = [];
assert.equal(
(await outline({ query: 'Zilker Park', kind: 'landmark' })).status,
'ZERO_RESULTS',
);
assert.equal(calls, 3, 'the malformed answers were not cached');
});
test('cache entries are namespaced by endpoint', async () => {
const cache = createNominatimCache();
const hits = [];
const gateFor = (endpoint) =>
createNominatimGate({
settings: { endpoint, isPublic: false, dailyCap: 50 },
fetchImpl: async (url) => {
hits.push(new URL(url).host);
return json([]);
},
});
await createNominatimSearchProvider({
gate: gateFor('https://a.example.test/search'),
cache,
})('x', null);
await createNominatimSearchProvider({
gate: gateFor('https://b.example.test/search'),
cache,
})('x', null);
assert.deepEqual(hits, ['a.example.test', 'b.example.test']);
});
test('public-host detection ignores a trailing DNS dot', () => {
assert.equal(
resolveNominatimSettings({
NOMINATIM_URL: 'https://nominatim.openstreetmap.org./search',
}).isPublic,
true,
);
});
test('missing coordinates are no bias, never (0, 0)', () => {
for (const [lat, lon] of [
[null, null],
['', ''],
[undefined, 2],
[48.8, null],
])
assert.equal(outlineBias('city', lat, lon), null, `${lat},${lon}`);
assert.ok(outlineBias('city', 0, 0), 'a real (0, 0) is still a position');
});
const boundary = (extra) => ({
category: 'boundary',
type: 'administrative',
lat: '1',
lon: '1',
geojson: { type: 'Polygon', coordinates: square(1, 1, 0.1) },
...extra,
});
test('administrative boundaries must be at the asked level', () => {
const county = boundary({
name: 'Travis County',
addresstype: 'county',
place_rank: 12,
});
const city = boundary({
name: 'Austin',
addresstype: 'city',
place_rank: 16,
});
const state = boundary({
name: 'Texas',
addresstype: 'state',
place_rank: 8,
});
assert.equal(
selectOutlineResult([state], { kind: 'city', query: 'Texas' }).outline,
null,
);
assert.equal(
selectOutlineResult([city], { kind: 'admin', query: 'Austin' }).outline,
null,
);
assert.equal(
selectOutlineResult(
[boundary({ name: 'Travis', addresstype: 'county', place_rank: 12 })],
{ kind: 'city', query: 'Travis' },
).outline,
null,
);
assert.equal(
selectOutlineResult(
[boundary({ name: 'Austin', addresstype: 'city', place_rank: 12 })],
{ kind: 'admin', query: 'Austin' },
).outline,
null,
);
assert.equal(
selectOutlineResult([state], { kind: 'neighborhood', query: 'Texas' })
.outline,
null,
);
assert.ok(
selectOutlineResult([county], { kind: 'admin', query: 'Travis County' })
.outline,
);
assert.equal(
selectOutlineResult([boundary({ name: 'Somewhere', place_rank: 16 })], {
kind: 'city',
query: 'Somewhere',
}).outline,
null,
'rank alone is not administrative identity',
);
assert.equal(
selectOutlineResult(
[
boundary({
name: 'Somewhere',
addresstype: 'locality',
place_rank: 16,
}),
],
{ kind: 'city', query: 'Somewhere' },
).outline,
null,
'rank cannot replace an unrecognized address type',
);
assert.equal(
selectOutlineResult([boundary({ name: 'Somewhere' })], {
kind: 'city',
query: 'Somewhere',
}).outline,
null,
'no level evidence at all',
);
// An unsuitable early boundary does not block a suitable later one.
const picked = selectOutlineResult(
[
state,
boundary({
name: 'Texas',
addresstype: 'city',
osm_type: 'relation',
osm_id: 9,
}),
],
{
kind: 'city',
query: 'Texas',
},
);
assert.equal(picked.outline.osm, 'relation/9');
});
test('outline selection does not publish child-owned admin geometry hooks', () => {
for (const [name, addresstype] of [
['Nepal', 'country', 'country'],
['Bagmati Province', 'province', 'admin1'],
['Province of Bagmati', 'province', 'admin1'],
['Texas', 'state', 'admin1'],
['Travis County', 'county', 'admin2'],
['County of Travis', 'county', 'admin2'],
['Kathmandu District', 'state_district', 'admin2'],
]) {
const { outline } = selectOutlineResult([boundary({ name, addresstype })], {
kind: 'admin',
query: name,
});
assert.equal('adminArea' in outline, false);
assert.equal('adminLevel' in outline, false);
}
for (const row of [
boundary({ name: 'Northern Region', addresstype: 'region', place_rank: 8 }),
boundary({
name: 'Central District',
addresstype: 'district',
place_rank: 10,
}),
boundary({ name: 'Example State', addresstype: 'county' }),
boundary({ name: 'County of Example', addresstype: 'state' }),
boundary({ name: 'Province of Example', addresstype: 'county' }),
boundary({ name: 'Example County', addresstype: 'state' }),
boundary({ name: 'Example Province', addresstype: 'county' }),
boundary({ name: 'County of Example Province', addresstype: 'county' }),
]) {
const { outline } = selectOutlineResult([row], {
kind: 'admin',
query: row.name,
});
assert.ok(outline, row.name);
assert.equal(outline.adminArea, undefined, row.name);
assert.equal(outline.adminLevel, undefined, row.name);
}
assert.equal(
selectOutlineResult(
[boundary({ name: 'Mystery Province', place_rank: 8 })],
{
kind: 'admin',
query: 'Mystery Province',
},
).outline,
null,
'rank-only metadata never creates an exact administrative outline',
);
const retired = selectOutlineResult(
[boundary({ name: 'Bagmati Zone', addresstype: 'state' })],
{ kind: 'admin', query: 'Bagmati Zone' },
).outline;
assert.equal('adminArea' in retired, false);
assert.equal('adminLevel' in retired, false);
const retiredPrefix = selectOutlineResult(
[boundary({ name: 'Zone of Bagmati', addresstype: 'state' })],
{ kind: 'admin', query: 'Zone of Bagmati' },
).outline;
assert.equal('adminArea' in retiredPrefix, false);
assert.equal('adminLevel' in retiredPrefix, false);
});
test('long admin names do not publish child-owned geometry hooks', () => {
const longValidName = `Province of ${'Example '.repeat(24)}`.trim();
const valid = selectOutlineResult(
[boundary({ name: longValidName, addresstype: 'province' })],
{ kind: 'admin', query: longValidName },
).outline;
assert.equal('adminArea' in valid, false);
assert.equal('adminLevel' in valid, false);
const longContradictoryName = `${'Very Long Administrative Name '.repeat(7)}County`;
assert.ok(longContradictoryName.length > 160);
const contradictory = selectOutlineResult(
[boundary({ name: longContradictoryName, addresstype: 'state' })],
{ kind: 'admin', query: longContradictoryName },
).outline;
assert.ok(contradictory);
assert.equal(contradictory.adminArea, undefined);
assert.equal(contradictory.adminLevel, undefined);
});
test('non-administrative outline kinds do not publish admin geometry hooks', () => {
const city = selectOutlineResult(
[
boundary({
name: 'County of Austin',
addresstype: 'city',
place_rank: 16,
}),
],
{ kind: 'city', query: 'County of Austin' },
).outline;
const park = selectOutlineResult(
[
{
category: 'leisure',
type: 'park',
name: 'Zilker Park',
lat: '1',
lon: '1',
geojson: { type: 'Polygon', coordinates: square(1, 1, 0.01) },
},
],
{ kind: 'landmark', query: 'Zilker Park' },
).outline;
for (const outline of [city, park]) {
assert.equal(outline.adminArea, undefined);
assert.equal(outline.adminLevel, undefined);
}
});
test('building asks need building evidence', () => {
const park = {
category: 'tourism',
type: 'theme_park',
name: 'Big Top',
lat: '1',
lon: '1',
geojson: { type: 'Polygon', coordinates: square(1, 1, 0.0001) },
};
const camp = { ...park, type: 'camp_site' };
const campus = { ...park, category: 'amenity', type: 'university' };
for (const row of [park, camp, campus])
assert.equal(
selectOutlineResult([row], { kind: 'building', query: 'Big Top' })
.outline,
null,
row.type,
);
const hugeOffice = {
...park,
category: 'office',
type: 'government',
geojson: { type: 'Polygon', coordinates: square(1, 1, 0.01) },
};
assert.equal(
selectOutlineResult([hugeOffice], { kind: 'building', query: 'Big Top' })
.outline,
null,
);
const office = {
...hugeOffice,
geojson: { type: 'Polygon', coordinates: square(1, 1, 0.001) },
};
assert.ok(
selectOutlineResult([office], { kind: 'building', query: 'Big Top' })
.outline,
);
const hall = { ...hugeOffice, category: 'building', type: 'yes' };
assert.ok(
selectOutlineResult([hall], { kind: 'building', query: 'Big Top' }).outline,
);
});
test('names match on whole words', () => {
const row = (name) => ({
category: 'leisure',
type: 'park',
name,
lat: '1',
lon: '1',
geojson: { type: 'Polygon', coordinates: square(1, 1, 0.01) },
});
assert.equal(
selectOutlineResult([row('Yorkshire Park')], {
kind: 'landmark',
query: 'York',
}).outline,
null,
);
assert.ok(
selectOutlineResult([row('York Park')], { kind: 'landmark', query: 'York' })
.outline,
);
assert.ok(
selectOutlineResult([row('Zilker Metropolitan Park')], {
kind: 'landmark',
query: 'Zilker Park',
}).outline,
);
assert.ok(
selectOutlineResult(
[{ ...row('新宿御苑'), namedetails: { name: '新宿御苑' } }],
{ kind: 'landmark', query: '新宿御苑' },
).outline,
);
assert.equal(
selectOutlineResult([row('कुल मंदिर')], {
kind: 'landmark',
query: 'काली मंदिर',
}).outline,
null,
);
assert.ok(
selectOutlineResult([row('काली मंदिर')], {
kind: 'landmark',
query: 'काली मंदिर',
}).outline,
);
});
+241
View File
@@ -0,0 +1,241 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { createGateStateStore } from '../../server/providers/regional/nominatimGate.js';
const moduleUrl = new URL(
'../../server/providers/regional/nominatimGate.js',
import.meta.url,
).href;
function startModule(script) {
const child = spawn(process.execPath, ['--input-type=module', '-e', script], {
stdio: ['pipe', 'pipe', 'pipe'],
});
const lines = [];
const waiters = [];
let stdout = '';
let stderr = '';
const publish = (line) => {
lines.push(line);
for (const waiter of [...waiters]) {
if (!line.startsWith(waiter.prefix)) continue;
waiters.splice(waiters.indexOf(waiter), 1);
clearTimeout(waiter.timer);
waiter.resolve(line);
}
};
child.stdout.on('data', (chunk) => {
stdout += chunk;
const parts = stdout.split('\n');
stdout = parts.pop();
for (const line of parts) publish(line);
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
});
return {
child,
lines,
waitFor(prefix, timeoutMs = 3000) {
const existing = lines.find((line) => line.startsWith(prefix));
if (existing) return Promise.resolve(existing);
return new Promise((resolve, reject) => {
const waiter = { prefix, resolve, reject, timer: null };
waiter.timer = setTimeout(() => {
const index = waiters.indexOf(waiter);
if (index >= 0) waiters.splice(index, 1);
reject(
new Error(
`timed out waiting for ${prefix}; stderr: ${stderr.trim()}`,
),
);
}, timeoutMs);
waiters.push(waiter);
});
},
exited: new Promise((resolve, reject) => {
child.once('error', reject);
child.once('exit', (code, signal) => resolve({ code, signal }));
}),
};
}
function holderScript(lockFile) {
return `
import { DatabaseSync } from 'node:sqlite';
const database = new DatabaseSync(${JSON.stringify(lockFile)});
database.exec('BEGIN IMMEDIATE');
process.stdout.write('LOCKED\\n');
process.stdin.once('data', () => {
database.exec('ROLLBACK');
database.close();
process.stdout.write('RELEASED\\n');
});
process.stdin.resume();
`;
}
function contenderScript(stateFile, lockWaitMs) {
return `
const { createGateStateStore } = await import(${JSON.stringify(moduleUrl)});
const store = createGateStateStore({
file: ${JSON.stringify(stateFile)},
lockWaitMs: ${lockWaitMs},
onError: () => {},
});
process.stdout.write('ATTEMPTING\\n', () => {
const result = store.dispatch(
'2026-10-01',
{ clock: () => 1000, dailyCap: 50, minSpacingMs: 1100 },
() => Promise.resolve(),
);
delete result.value;
process.stdout.write('RESULT:' + JSON.stringify(result) + '\\n');
});
`;
}
test('a crashed lock owner cannot delete or bypass a successor lock', async (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-nominatim-lock-'));
const stateFile = path.join(dir, 'state.json');
const lockFile = `${stateFile}.lock.sqlite`;
const children = [];
t.after(() => {
for (const process of children) {
if (process.exitCode == null && process.signalCode == null)
process.kill();
}
fs.rmSync(dir, { recursive: true, force: true });
});
const crashedOwner = startModule(holderScript(lockFile));
children.push(crashedOwner.child);
await crashedOwner.waitFor('LOCKED');
const originalLock = fs.statSync(lockFile);
const blockedByCrashedOwner = startModule(contenderScript(stateFile, 0));
children.push(blockedByCrashedOwner.child);
const blockedResult = JSON.parse(
(await blockedByCrashedOwner.waitFor('RESULT:')).slice('RESULT:'.length),
);
assert.equal(blockedResult.status, 'unavailable');
assert.equal((await blockedByCrashedOwner.exited).code, 0);
assert.equal(fs.existsSync(stateFile), false);
crashedOwner.child.kill();
await crashedOwner.exited;
const successorOwner = startModule(holderScript(lockFile));
children.push(successorOwner.child);
await successorOwner.waitFor('LOCKED');
assert.equal(fs.statSync(lockFile).ino, originalLock.ino);
const refused = startModule(contenderScript(stateFile, 0));
children.push(refused.child);
const refusedResult = JSON.parse(
(await refused.waitFor('RESULT:')).slice('RESULT:'.length),
);
assert.equal(refusedResult.status, 'unavailable');
assert.equal((await refused.exited).code, 0);
assert.equal(
createGateStateStore({ file: stateFile }).count('2026-10-01'),
0,
'a timed-out contender fails closed without spending a public slot',
);
assert.equal(fs.statSync(lockFile).ino, originalLock.ino);
successorOwner.child.stdin.end('release\n');
await successorOwner.waitFor('RELEASED');
assert.equal((await successorOwner.exited).code, 0);
const released = createGateStateStore({ file: stateFile });
const afterRelease = released.dispatch(
'2026-10-01',
{ clock: () => 2200, dailyCap: 50, minSpacingMs: 1100 },
() => Promise.resolve(),
);
assert.equal(afterRelease.status, 'started');
assert.equal(released.count('2026-10-01'), 1);
assert.equal(fs.statSync(lockFile).ino, originalLock.ino);
});
test('a public pause stays authoritative while another process owns the lock', async (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-nominatim-pause-'));
const stateFile = path.join(dir, 'state.json');
const lockFile = `${stateFile}.lock.sqlite`;
const children = [];
t.after(() => {
for (const process of children) {
if (process.exitCode == null && process.signalCode == null)
process.kill();
}
fs.rmSync(dir, { recursive: true, force: true });
});
fs.writeFileSync(
stateFile,
JSON.stringify({
day: '2026-10-01',
count: 1,
pausedUntil: 0,
nextStartAt: 0,
}),
);
const owner = startModule(holderScript(lockFile));
children.push(owner.child);
await owner.waitFor('LOCKED');
const errors = [];
const blockedWriter = createGateStateStore({
file: stateFile,
lockWaitMs: 0,
onError: (error) => errors.push(error),
});
blockedWriter.pauseUntil(9000);
assert.equal(errors.length, 1, 'the blocked JSON consolidation is reported');
assert.equal(
JSON.parse(fs.readFileSync(stateFile, 'utf8')).pausedUntil,
0,
'the lock owner prevents an unsafe JSON overwrite',
);
owner.child.stdin.end('release\n');
await owner.waitFor('RELEASED');
assert.equal((await owner.exited).code, 0);
const successor = createGateStateStore({ file: stateFile });
let sent = 0;
const send = () => {
sent += 1;
return Promise.resolve();
};
assert.deepEqual(
successor.dispatch(
'2026-10-01',
{ clock: () => 1000, dailyCap: 50, minSpacingMs: 1100 },
send,
),
{ status: 'paused', retryAfterMs: 8000 },
'a fresh process observes the pending refusal pause',
);
assert.equal(sent, 0, 'a paused dispatch never starts the transport');
const resumed = successor.dispatch(
'2026-10-01',
{ clock: () => 9001, dailyCap: 50, minSpacingMs: 1100 },
send,
);
assert.equal(resumed.status, 'started');
assert.equal(sent, 1);
assert.equal(successor.count('2026-10-01'), 2);
assert.equal(
fs.readdirSync(dir).some((name) => name.startsWith('state.json.pause.')),
false,
'the next successful transaction consolidates and removes the marker',
);
});
File diff suppressed because it is too large. Load diff
+97 -8
View File
@@ -9,16 +9,50 @@
// Run with: npm test
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
fetchNominatimSearch,
geocodeProxy,
NOMINATIM_MAX_PENDING,
} from '../../server/providers/regional/place.js';
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';
import { standaloneConfig } from '../../server/standalone/vite.config.js';
import { defaultSourceRoot } from '../../server/providers/common/source-root.js';
import { createPlaceSearch, createGoogleGeocoder } from '../search/index.js';
import { createPhotonGeocoder } from '../keylessGeocoder.js';
// Each case starts with a fresh shared gate: a pause after one case's
// simulated outage must not decide the next case.
// Pin the public-policy fixture independently of operator endpoint settings.
// Each case's transport fails closed outside its active withUpstream block.
let activeUpstream;
test.beforeEach(() => {
const transport = { fetch: null };
activeUpstream = transport;
setSharedNominatimGate(
createNominatimGate({
settings: resolveNominatimSettings({
NOMINATIM_URL: PUBLIC_NOMINATIM_SEARCH,
}),
fetchImpl: (...args) => {
if (!transport.fetch) throw new Error('No active test upstream');
return transport.fetch(...args);
},
}),
);
});
const storageRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-search-route-'));
test.after(() => fs.rmSync(storageRoot, { recursive: true, force: true }));
const HIT = [
{
lat: '43.1731',
@@ -33,13 +67,16 @@ const HIT = [
/** Mount the geocode route the way the dev server does. */
let clientCounter = 0;
function mountGeocode(
install = (server) => geocodeProxy().configureServer(server),
) {
function mountGeocode(install = null) {
// A fresh client address per mount: the per-client rate limiter is module
// state shared by every case in this file, and one case's burst must not
// decide another case's outcome.
clientCounter += 1;
if (!install)
install = (server) =>
geocodeProxy({
storageDir: path.join(storageRoot, `case-${clientCounter}`),
}).configureServer(server);
const remoteAddress = `10.0.0.${clientCounter % 250}`;
const routes = new Map();
install({
@@ -93,14 +130,17 @@ function mountGeocode(
/** Answer the upstream from a stub, counting and optionally stalling calls. */
async function withUpstream(respond, run) {
const previous = globalThis.fetch;
const transport = activeUpstream;
const previousUpstream = transport.fetch;
const calls = [];
globalThis.fetch = async (url, options) => {
globalThis.fetch = transport.fetch = async (url, options) => {
calls.push({ url: String(url), options });
return await respond(String(url), options, calls.length);
};
try {
return await run(calls);
} finally {
transport.fetch = previousUpstream;
globalThis.fetch = previous;
}
}
@@ -111,13 +151,55 @@ const jsonResponse = (value) =>
headers: { 'Content-Type': 'application/json' },
});
test('the route is part of the application on both dev and preview', () => {
const plugin = localProviderPlugins().find(
test('the real composition binds durable storage before dev or preview installation, independent of CWD', async () => {
const cwd = process.cwd();
const expected = path.join(defaultSourceRoot, '.gev-cache', 'nominatim');
try {
for (const suffix of ['one', 'two']) {
const dir = path.join(storageRoot, `cwd-${suffix}`);
fs.mkdirSync(dir);
process.chdir(dir);
const { NOMINATIM_CACHE_DIR } = await import(
`../../server/providers/regional/place.js?cwd-${suffix}`
);
assert.equal(NOMINATIM_CACHE_DIR, expected);
assert.equal(fs.existsSync(path.join(dir, '.gev-cache')), false);
}
} finally {
process.chdir(cwd);
}
const bound = [];
setSharedNominatimGate({
persistUsage: (file) => bound.push(file),
});
const storage = path.join(storageRoot, 'composition');
const plugin = localProviderPlugins({ nominatimStorageDir: storage }).find(
(entry) => entry.name === 'geocode-proxy',
);
assert.ok(plugin, 'the application must compose the place-search route');
assert.equal(typeof plugin.configureServer, 'function');
assert.equal(typeof plugin.configurePreviewServer, 'function');
assert.deepEqual(bound, [path.join(storage, 'state.json')]);
const standaloneStorage = path.join(storageRoot, 'standalone');
const config = standaloneConfig(
{ command: 'serve', mode: 'test' },
{ nominatimStorageDir: standaloneStorage },
);
assert.ok(config.plugins.some((entry) => entry.name === 'geocode-proxy'));
assert.deepEqual(bound, [
path.join(storage, 'state.json'),
path.join(standaloneStorage, 'state.json'),
]);
for (const value of [null, '', 'relative'])
assert.throws(
() => localProviderPlugins({ nominatimStorageDir: value }),
/absolute directory/,
);
assert.equal(
bound.length,
2,
'invalid bindings never construct policy state',
);
});
test('a search reaches the upstream identified, and the answer is cached', async () => {
@@ -126,7 +208,9 @@ test('a search reaches the upstream identified, and the answer is cached', async
async () => jsonResponse(HIT),
async (calls) => {
const query = `niagara-${Date.now()}`;
const first = await request(`/api/geocode?q=${query}`);
const first = await request(
`/api/geocode?q=${query}&bounds=${encodeURIComponent('-20,170|-10,-171')}`,
);
assert.equal(first.status, 200);
assert.equal(first.body.status, 'OK');
assert.deepEqual(first.body.results[0].geometry.location, {
@@ -134,6 +218,11 @@ test('a search reaches the upstream identified, and the answer is cached', async
lng: -79.0384,
});
assert.equal(calls.length, 1);
assert.equal(
new URL(calls[0].url).searchParams.has('viewbox'),
false,
'a wrapped hint must not be coarsened into a Greenwich viewbox',
);
const headers = calls[0].options.headers;
assert.match(
String(headers['User-Agent']),
@@ -147,7 +236,7 @@ test('a search reaches the upstream identified, and the answer is cached', async
'a fixed API endpoint may not be redirected elsewhere',
);
// The policy asks that results be cached; a repeat costs no request.
// Dropping the invalid hint also shares the unbiased cache entry.
const repeat = await request(`/api/geocode?q=${query}`);
assert.equal(repeat.status, 200);
assert.equal(
+6 -3
View File
@@ -77,13 +77,15 @@ test('the documented outcomes hold: default, stored open, stored shut, and a sha
// against the code that decides them.
const { PanelPositionControls } =
await import('../ui/panelPositionControls.js');
const { presentPanelLocally } = await import('../testSupport/panelState.mjs');
const { readPanelState } = await import('../ui/rail/panelState.js');
const html = await readFile(
path.join(root, 'src/ui/templates/layer-panels.html'),
path.join(root, 'src/ui/views/panels/CctvPanel.vue'),
'utf8',
);
assert.match(
html,
/<div id="cctv-panel" class="panel-collapsible collapsed"/,
/<RailPanel id="cctv-panel"[^>]*\bstart-collapsed\b/,
'the CCTV panel starts collapsed in the markup',
);
@@ -110,7 +112,8 @@ test('the documented outcomes hold: default, stored open, stored shut, and a sha
};
try {
const controls = new PanelPositionControls({
syncPanelCollapseButton: () => {},
presentPanel: presentPanelLocally,
shownPanel: readPanelState,
layoutRightPanels: () => {},
syncCctvPanelViewport: () => {},
showToast: () => {},
+10 -2
View File
@@ -7,10 +7,14 @@ import { localProviderPlugins } from '../../server/providers/local.js';
import { apiNotFoundPlugin } from '../../server/standalone/api-not-found.js';
import { makeFixtureRoot } from './fixtureRoot.mjs';
const nominatimStorageDir = await makeFixtureRoot('gev-preview-composition-');
test.after(() => rm(nominatimStorageDir, { recursive: true, force: true }));
test('data providers have both hooks; credential editing stays development-only', () => {
const plugins = localProviderPlugins();
const plugins = localProviderPlugins({ nominatimStorageDir });
assert.ok(plugins.some(({ name }) => name === 'fire-perimeters'));
assert.ok(plugins.some(({ name }) => name === 'gnss-integrity'));
assert.ok(plugins.some(({ name }) => name === 'ghostmaps'));
for (const plugin of plugins) {
if (plugin.name === 'gev-key-setup') {
assert.equal(plugin.configurePreviewServer, undefined);
@@ -88,7 +92,10 @@ test('real dev and built-preview servers serve provider JSON and terminate unkno
for (const isPreview of [false, true]) {
const config = {
...base,
plugins: [...localProviderPlugins(), apiNotFoundPlugin()],
plugins: [
...localProviderPlugins({ nominatimStorageDir }),
apiNotFoundPlugin(),
],
server: { host: '127.0.0.1', port: 0, hmr: false },
preview: { host: '127.0.0.1', port: 0 },
};
@@ -105,6 +112,7 @@ test('real dev and built-preview servers serve provider JSON and terminate unkno
['/api/firms/status', 200],
['/api/fire-perimeters/inciweb/publication/invalid', 400],
['/api/gnss-integrity?lat=invalid', 400],
['/api/ghostmaps/unknown', 404],
['/api/terrain/heights?points=invalid', 400],
['/api/overpass', 405],
['/api/overpass/status', 200],
+2 -1
View File
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';
import {
CUBE_FACES,
@@ -24,7 +25,7 @@ test('painted-out planets leave no disc brighter than the sky around them', asyn
}));
for (const face of CUBE_FACES) {
const { data, info } = await sharp(
new URL(`milkyway_${FACE_FILE_SUFFIX[face]}.webp`, SKY).pathname,
fileURLToPath(new URL(`milkyway_${FACE_FILE_SUFFIX[face]}.webp`, SKY)),
)
.raw()
.toBuffer({ resolveWithObject: true });
+27 -20
View File
@@ -1,3 +1,4 @@
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
@@ -8,7 +9,8 @@ import {
detectOn,
} from '../../scripts/qa-transit-controls.mjs';
import { createStyleParameters } from '../ui/styleParameters.js';
import { bindDisplayControls } from '../ui/displayControls.js';
import { createPinia } from 'pinia';
import { useDisplayStore } from '../ui/stores/display.js';
import { thermalShader } from '../styles/thermal.js';
import {
cycleMode,
@@ -18,6 +20,9 @@ import {
} from '../data/detection.js';
const pause = async () => {};
// The page fixture swaps the globals for stand-ins; generated rows render
// into a real element, so keep the registered document.
const realDocument = globalThis.document;
class Element extends EventTarget {
constructor() {
super();
@@ -67,8 +72,7 @@ function pageFixture(t) {
const label = selector.match(/aria-label="([^"]+)"/)[1];
return elements
.get('param-sliders')
.children.flatMap((row) => row.children)
.find((el) => el.getAttribute('aria-label') === label);
.querySelector(`input.param-slider[aria-label="${label}"]`);
}
return elements.get(selector.slice(1));
};
@@ -128,8 +132,7 @@ test('live visual cities follow local operating hours in summer and winter', ()
test('thermal controls wait for activation and use the real generated parameter rows', async (t) => {
const { page, document, elements } = pageFixture(t);
const container = new Element();
container.ownerDocument = document;
const container = realDocument.createElement('div');
elements.set('param-sliders', container);
const parameters = createStyleParameters({ container });
t.after(() => parameters.destroy());
@@ -159,9 +162,7 @@ test('thermal controls wait for activation and use the real generated parameter
page.$eval = async (selector, fn, ...args) => {
if (selector.startsWith('.style-btn')) return fn(new Element(), ...args);
const label = selector.match(/aria-label="([^"]+)"/)[1];
const slider = container.children
.flatMap((row) => row.children)
.find((el) => el.getAttribute('aria-label') === label);
const slider = container.querySelector(`[aria-label="${label}"]`);
assert.ok(slider);
return fn(slider, ...args);
};
@@ -189,26 +190,32 @@ test('DETECT uses the real OFF/restore button and density input to reach DENSE',
setMode('BALANCED');
setMode('OFF');
const modes = [];
const controls = bindDisplayControls({
elements: { detectionButton: button, densitySlider: slider },
actions: {
cycleDetection() {
cycleMode();
modes.push(getMode());
},
setDensity(value) {
setDetectionTuning({ densityPct: Number(value) });
},
// As DisplayPanel does: the toggle and the slider call the Display
// controls' actions through the display store.
const store = useDisplayStore(createPinia());
const token = store.connect({
cycleDetection() {
cycleMode();
modes.push(getMode());
},
setDensity(value) {
setDetectionTuning({ densityPct: Number(value) });
},
});
const onClick = () => store.act('cycleDetection');
const onInput = () => store.act('setDensity', slider.value);
button.addEventListener('click', onClick);
slider.addEventListener('input', onInput);
t.after(() => {
controls.destroy();
button.removeEventListener('click', onClick);
slider.removeEventListener('input', onInput);
store.detach(token);
setMode('OFF');
});
assert.equal(await detectOn(page, pause), 'DENSE');
assert.deepEqual(modes, ['BALANCED', 'OFF', 'DENSE']);
const template = readFileSync(
new URL('../ui/templates/display-controls.html', import.meta.url),
new URL('../ui/views/panels/DisplayPanel.vue', import.meta.url),
'utf8',
);
assert.match(template, /id="detection-density-slider"/);
+49 -3
View File
@@ -16,6 +16,9 @@ import * as providers from '../../server/providers/local.js';
import { apiNotFoundPlugin } from '../../server/standalone/api-not-found.js';
import { makeFixtureRoot } from './fixtureRoot.mjs';
const nominatimStorageDir = await makeFixtureRoot('gev-build-composition-');
test.after(() => rm(nominatimStorageDir, { recursive: true, force: true }));
test('explicit build inputs preserve browser-only defines, plugin order and loopback protections', () => {
const plugin = { name: 'fixture-provider' };
const config = createBrowserViteConfig({
@@ -29,6 +32,7 @@ test('explicit build inputs preserve browser-only defines, plugin order and loop
assert.equal(config.plugins[0].enforce, 'pre');
assert.equal(config.plugins[1].name, 'host-check');
assert.equal(config.plugins[1].enforce, 'pre');
assert.equal(config.plugins[3].name, 'vite:vue');
assert.equal(config.plugins[4], plugin);
assert.equal(config.server.host, 'localhost');
assert.equal(config.server.port, 4173);
@@ -64,10 +68,34 @@ test('explicit build inputs preserve browser-only defines, plugin order and loop
assert.ok(!csp.includes("script-src 'self' 'unsafe-inline'"));
assert.deepEqual(config.preview.headers, config.server.headers);
assert.deepEqual(config.define, {
__VUE_I18N_FULL_INSTALL__: 'false',
__VUE_I18N_LEGACY_API__: 'false',
__INTLIFY_PROD_DEVTOOLS__: 'false',
__INTLIFY_DROP_MESSAGE_COMPILER__: 'true',
'import.meta.env.GOOGLE_MAPS_API_KEY': '"browser-fixture"',
'import.meta.env.CESIUM_ION_TOKEN': '"ion-fixture"',
'import.meta.env.MAPILLARY_CLIENT_TOKEN': '""',
});
assert.equal(
createBrowserViteConfig({ photonUrl: '' }).define[
'import.meta.env.PHOTON_URL'
],
'null',
'an empty PHOTON_URL reaches the browser only as the disabled sentinel',
);
assert.equal(
createBrowserViteConfig({ photonUrl: 'https://u:[email protected]/api/' })
.define['import.meta.env.PHOTON_URL'],
'null',
'credentials are never serialized into browser assets',
);
assert.equal(
createBrowserViteConfig({ photonUrl: 'http://localhost:2322/api/' }).define[
'import.meta.env.PHOTON_URL'
],
'null',
'plain http would be blocked by connect-src, so it is refused here',
);
assert.equal(
createBrowserViteConfig({ mapillaryToken: 'MLY|1|abc' }).define[
'import.meta.env.MAPILLARY_CLIENT_TOKEN'
@@ -115,7 +143,7 @@ test('build helper does not discover environment values or construct local provi
test('root config retains existing named exports and standalone provider order', () => {
for (const [name, value] of Object.entries(providers))
assert.equal(compatibility[name], value, name);
const config = standaloneConfig({ mode: 'test' });
const config = standaloneConfig({ mode: 'test' }, { nominatimStorageDir });
const standaloneProviders = config.plugins
.slice(4, -6)
.map((plugin) => plugin.name);
@@ -129,7 +157,9 @@ test('root config retains existing named exports and standalone provider order',
);
assert.deepEqual(
standaloneProviders.filter((name) => name !== 'gemini-live-proxy'),
providers.localProviderPlugins().map((plugin) => plugin.name),
providers
.localProviderPlugins({ nominatimStorageDir })
.map((plugin) => plugin.name),
);
assert.equal(config.plugins.at(-7).name, 'gev-key-setup');
// Token fallback follows real providers and local MCP before the API 404.
@@ -142,10 +172,26 @@ test('root config retains existing named exports and standalone provider order',
assert.equal(config.plugins.at(-1).name, 'panel-build');
});
test('standalone config passes a validated Photon setting into the browser define', () => {
const previous = process.env.PHOTON_URL;
process.env.PHOTON_URL = 'https://photon.example.test/api/';
try {
assert.equal(
standaloneConfig({ mode: 'test' }, { nominatimStorageDir }).define[
'import.meta.env.PHOTON_URL'
],
'"https://photon.example.test/api/"',
);
} finally {
if (previous === undefined) delete process.env.PHOTON_URL;
else process.env.PHOTON_URL = previous;
}
});
test('directories the server writes while it runs are kept out of the dev watcher', () => {
// Providers write cache files and logs here while the app runs; none of it is
// source, so the dev watcher has no reason to stat it.
const config = standaloneConfig({ mode: 'test' });
const config = standaloneConfig({ mode: 'test' }, { nominatimStorageDir });
const plugin = config.plugins.find(
({ name }) => name === 'gev-unwatched-runtime-dirs',
);
+111
View File
@@ -0,0 +1,111 @@
import assert from 'node:assert/strict';
import {
chmodSync,
mkdirSync,
mkdtempSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
cachedVueModule,
compileVueModule,
ownCacheDirectory,
vueCacheDirectory,
vueCacheKey,
} from '../../build/vueLoader.js';
const SOURCE =
'<script setup>\nconst label = "Hi";\n</script>\n<template><b>{{ label }}</b></template>\n';
const FILE = '/virtual/Greeting.vue';
test('a component compiles once and later processes read the cached module', (t) => {
const directory = mkdtempSync(path.join(tmpdir(), 'gev-vue-cache-test-'));
t.after(() => rmSync(directory, { recursive: true, force: true }));
const compiled = cachedVueModule(SOURCE, FILE, directory);
assert.equal(compiled, compileVueModule(SOURCE, FILE));
const entries = readdirSync(directory);
assert.deepEqual(entries, [`${vueCacheKey(SOURCE, FILE)}.mjs`]);
// A cached entry is returned as stored, without compiling again.
writeFileSync(path.join(directory, entries[0]), 'export default "cached";');
assert.equal(
cachedVueModule(SOURCE, FILE, directory),
'export default "cached";',
);
});
test('an edited component or another path is a new cache entry', () => {
const key = vueCacheKey(SOURCE, FILE);
assert.notEqual(vueCacheKey(`${SOURCE}\n`, FILE), key);
assert.notEqual(vueCacheKey(SOURCE, '/virtual/Other.vue'), key);
assert.equal(vueCacheKey(SOURCE, FILE), key);
});
test('GEV_VUE_CACHE=0 compiles every time; otherwise the cache sits in a per-user directory', () => {
assert.equal(vueCacheDirectory({ GEV_VUE_CACHE: '0' }), null);
if (process.platform !== 'win32') {
const directory = vueCacheDirectory({});
assert.equal(path.dirname(directory), tmpdir());
assert.match(path.basename(directory), /^gev-vue-sfc-/);
}
// Windows temp directories may be shared, so the cache lives in the
// user's profile, or not at all.
assert.equal(
vueCacheDirectory(
{ LOCALAPPDATA: 'C:\\Users\\a\\AppData\\Local' },
'win32',
),
path.join('C:\\Users\\a\\AppData\\Local', 'gev-vue-sfc'),
);
assert.equal(vueCacheDirectory({}, 'win32'), null);
assert.equal(
cachedVueModule(SOURCE, FILE, null),
compileVueModule(SOURCE, FILE),
);
});
test('a cache directory that cannot be written still returns the compiled module', (t) => {
const directory = mkdtempSync(path.join(tmpdir(), 'gev-vue-cache-test-'));
t.after(() => rmSync(directory, { recursive: true, force: true }));
// A file where the cache directory should be makes every write fail.
const blocked = path.join(directory, 'blocked');
writeFileSync(blocked, '');
assert.equal(
cachedVueModule(SOURCE, FILE, blocked),
compileVueModule(SOURCE, FILE),
);
});
test(
'a cache directory others can write to, or a link to one, is never read',
{
skip: process.platform === 'win32',
},
(t) => {
const root = mkdtempSync(path.join(tmpdir(), 'gev-vue-cache-test-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const shared = path.join(root, 'shared');
mkdirSync(shared);
chmodSync(shared, 0o777);
// A module planted where the cache would look must not run.
writeFileSync(
path.join(shared, `${vueCacheKey(SOURCE, FILE)}.mjs`),
'export default "planted";',
);
assert.equal(ownCacheDirectory(shared), false);
assert.equal(
cachedVueModule(SOURCE, FILE, shared),
compileVueModule(SOURCE, FILE),
);
const own = path.join(root, 'own');
mkdirSync(own, { mode: 0o700 });
const link = path.join(root, 'link');
symlinkSync(own, link);
assert.equal(ownCacheDirectory(link), false);
assert.equal(ownCacheDirectory(own), true);
},
);