mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
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
458 lines
15 KiB
JavaScript
458 lines
15 KiB
JavaScript
// THE LAST-RESORT PLACE SEARCH ROUTE — what it is allowed to send upstream.
|
|
//
|
|
// The public instance permits one request per second. One request per second
|
|
// and an unbounded queue are incompatible: a burst of searches would keep the
|
|
// upstream busy long after everyone who asked has given up. These cases pin the
|
|
// queue bound, the shared answer for identical searches, the cancellation path,
|
|
// and the chain order that only reaches here when nothing else answered.
|
|
//
|
|
// 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',
|
|
lon: '-79.0384',
|
|
display_name: 'Niagara Falls, Ontario, Canada',
|
|
name: 'Niagara Falls',
|
|
addresstype: 'city',
|
|
boundingbox: ['43.05', '43.15', '-79.15', '-79.0'],
|
|
},
|
|
];
|
|
|
|
/** Mount the geocode route the way the dev server does. */
|
|
let clientCounter = 0;
|
|
|
|
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({
|
|
middlewares: {
|
|
use(route, handler) {
|
|
routes.set(route, handler);
|
|
},
|
|
},
|
|
});
|
|
return async function request(path) {
|
|
const requested = new URL(path, 'http://localhost');
|
|
const handler = routes.get('/api/geocode');
|
|
if (!handler) throw new Error('no /api/geocode route');
|
|
// connect strips the mount prefix before the handler sees the request.
|
|
const rest =
|
|
requested.pathname.slice('/api/geocode'.length) + requested.search;
|
|
const req = {
|
|
method: 'GET',
|
|
url: rest[0] === '/' ? rest : `/${rest}`,
|
|
headers: {},
|
|
socket: { remoteAddress },
|
|
on() {},
|
|
};
|
|
return await new Promise((resolve, reject) => {
|
|
let status = 200;
|
|
let headers = {};
|
|
const res = {
|
|
writableEnded: false,
|
|
on() {},
|
|
writeHead(code, sent) {
|
|
status = code;
|
|
headers = sent || {};
|
|
return res;
|
|
},
|
|
end(body = '') {
|
|
res.writableEnded = true;
|
|
let parsed = null;
|
|
try {
|
|
parsed = body ? JSON.parse(String(body)) : null;
|
|
} catch {
|
|
parsed = String(body);
|
|
}
|
|
resolve({ status, headers, body: parsed });
|
|
},
|
|
};
|
|
Promise.resolve(handler(req, res)).catch(reject);
|
|
});
|
|
};
|
|
}
|
|
|
|
/** 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 = 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;
|
|
}
|
|
}
|
|
|
|
const jsonResponse = (value) =>
|
|
new Response(JSON.stringify(value), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
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 () => {
|
|
const request = mountGeocode();
|
|
await withUpstream(
|
|
async () => jsonResponse(HIT),
|
|
async (calls) => {
|
|
const query = `niagara-${Date.now()}`;
|
|
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, {
|
|
lat: 43.1731,
|
|
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']),
|
|
/gods-eye-view\/\d/,
|
|
'the policy asks for a User-Agent identifying the application',
|
|
);
|
|
assert.ok(headers.Referer, 'a Referer is sent alongside it');
|
|
assert.equal(
|
|
calls[0].options.redirect,
|
|
'error',
|
|
'a fixed API endpoint may not be redirected elsewhere',
|
|
);
|
|
|
|
// 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(
|
|
calls.length,
|
|
1,
|
|
'the second identical search is served from cache',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('identical searches in flight share one upstream call', async () => {
|
|
const request = mountGeocode();
|
|
const query = `coalesce-${Date.now()}`;
|
|
await withUpstream(
|
|
async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
return jsonResponse(HIT);
|
|
},
|
|
async (calls) => {
|
|
const answers = await Promise.all([
|
|
request(`/api/geocode?q=${query}`),
|
|
request(`/api/geocode?q=${query}`),
|
|
request(`/api/geocode?q=${query}`),
|
|
]);
|
|
for (const answer of answers) assert.equal(answer.status, 200);
|
|
assert.equal(calls.length, 1, 'one upstream call serves all three');
|
|
},
|
|
);
|
|
});
|
|
|
|
test('a burst past the queue bound is refused rather than queued for minutes', async () => {
|
|
const request = mountGeocode();
|
|
const stamp = Date.now();
|
|
await withUpstream(
|
|
async () => jsonResponse([]),
|
|
async (calls) => {
|
|
const answers = await Promise.all(
|
|
Array.from({ length: 20 }, (_unused, index) =>
|
|
request(`/api/geocode?q=burst-${stamp}-${index}`),
|
|
),
|
|
);
|
|
const refused = answers.filter((answer) => answer.status === 429);
|
|
assert.ok(
|
|
refused.length > 0,
|
|
'a burst this size must be refused somewhere',
|
|
);
|
|
for (const answer of refused)
|
|
assert.equal(answer.headers['Retry-After'], '5');
|
|
assert.ok(
|
|
calls.length <= NOMINATIM_MAX_PENDING + 1,
|
|
`the upstream saw ${calls.length} of 20 searches; the queue bound is ${NOMINATIM_MAX_PENDING}`,
|
|
);
|
|
assert.equal(
|
|
answers.length,
|
|
20,
|
|
'every caller is answered, none is left hanging',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('a query that is missing or too long never reaches the upstream', async () => {
|
|
const request = mountGeocode();
|
|
await withUpstream(
|
|
async () => jsonResponse(HIT),
|
|
async (calls) => {
|
|
for (const path of [
|
|
'/api/geocode',
|
|
'/api/geocode?q=',
|
|
'/api/geocode?q=%20%20',
|
|
`/api/geocode?q=${'x'.repeat(201)}`,
|
|
]) {
|
|
const answer = await request(path);
|
|
assert.equal(answer.status, 400, path);
|
|
}
|
|
assert.deepEqual(calls, []);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('an upstream failure is a service answer, not a crash', async () => {
|
|
const request = mountGeocode();
|
|
await withUpstream(
|
|
async () => new Response('upstream down', { status: 502 }),
|
|
async () => {
|
|
const answer = await request(`/api/geocode?q=down-${Date.now()}`);
|
|
assert.equal(answer.status, 503);
|
|
assert.equal(answer.headers['Cache-Control'], 'no-store');
|
|
assert.match(String(answer.body.error), /temporarily unavailable/);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('a search with no hit is a clean empty answer', async () => {
|
|
const request = mountGeocode();
|
|
await withUpstream(
|
|
async () => jsonResponse([]),
|
|
async () => {
|
|
const answer = await request(`/api/geocode?q=nowhere-${Date.now()}`);
|
|
assert.equal(answer.status, 200);
|
|
assert.equal(answer.body.status, 'ZERO_RESULTS');
|
|
assert.deepEqual(answer.body.results, []);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('the chain reaches this route only when Google and Photon do not answer', async () => {
|
|
const asked = [];
|
|
const request = mountGeocode();
|
|
await withUpstream(
|
|
async (url) => {
|
|
if (url.includes('photon.komoot.io')) {
|
|
asked.push('photon');
|
|
return new Response('photon is down', { status: 503 });
|
|
}
|
|
asked.push('nominatim');
|
|
return jsonResponse(HIT);
|
|
},
|
|
async () => {
|
|
const search = createPlaceSearch({
|
|
providers: [
|
|
createGoogleGeocoder({
|
|
request() {
|
|
asked.push('google');
|
|
// An unconfigured provider contributes no verdict.
|
|
return null;
|
|
},
|
|
}),
|
|
createPhotonGeocoder({
|
|
fetchImpl: (...args) => globalThis.fetch(...args),
|
|
}),
|
|
createGoogleGeocoder({
|
|
request(query) {
|
|
return request(
|
|
`/api/geocode?q=${encodeURIComponent(query)}`,
|
|
).then(
|
|
(answer) =>
|
|
new Response(JSON.stringify(answer.body), {
|
|
status: answer.status,
|
|
}),
|
|
);
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
const outcome = await search.geocode(`chain-${Date.now()}`);
|
|
assert.deepEqual(
|
|
asked,
|
|
['google', 'photon', 'nominatim'],
|
|
`Google first, then Photon, then this route (saw ${asked.join(' → ')})`,
|
|
);
|
|
assert.equal(outcome.place.lat, 43.1731);
|
|
assert.equal(outcome.fallbackUsed, true);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('a search whose caller gave up is dropped before it costs a slot', async () => {
|
|
const abandoned = new AbortController();
|
|
abandoned.abort();
|
|
await withUpstream(
|
|
async () => jsonResponse(HIT),
|
|
async (calls) => {
|
|
await assert.rejects(
|
|
fetchNominatimSearch(`cancelled-${Date.now()}`, null, {
|
|
signal: abandoned.signal,
|
|
}),
|
|
(error) => error.code === 'NOMINATIM_ABANDONED',
|
|
);
|
|
assert.deepEqual(calls, [], 'a search nobody is waiting for is not sent');
|
|
},
|
|
);
|
|
});
|
|
|
|
test('the upstream deadline covers the body, not just the headers', async () => {
|
|
// A response whose headers arrive and whose body then never does. Before the
|
|
// deadline covered the body, the abort timer was already cleared by the time
|
|
// the first byte was due, so this read had no deadline at all.
|
|
const stalled = () => {
|
|
let cancelled = false;
|
|
const stream = new ReadableStream({
|
|
pull() {
|
|
return new Promise(() => {});
|
|
},
|
|
cancel() {
|
|
cancelled = true;
|
|
},
|
|
});
|
|
return {
|
|
response: new Response(stream, { status: 200 }),
|
|
wasCancelled: () => cancelled,
|
|
};
|
|
};
|
|
|
|
const previous = globalThis.fetch;
|
|
const { response, wasCancelled } = stalled();
|
|
globalThis.fetch = async (_url, options) => {
|
|
// The same signal must reach the body read, or aborting stops nothing.
|
|
options.signal.addEventListener('abort', () => {
|
|
void response.body?.cancel().catch(() => {});
|
|
});
|
|
return response;
|
|
};
|
|
try {
|
|
const started = Date.now();
|
|
await assert.rejects(
|
|
fetchRegionalJson('https://example.test/stalls', { timeoutMs: 300 }),
|
|
'a stalled body must fail rather than hang',
|
|
);
|
|
const elapsed = Date.now() - started;
|
|
assert.ok(elapsed < 5000, `gave up after ${elapsed} ms`);
|
|
assert.ok(wasCancelled(), 'the abort reached the body read');
|
|
} finally {
|
|
globalThis.fetch = previous;
|
|
}
|
|
});
|