mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Both stack parents (#837, #842) are on main, so the voice geometry branch now builds on main directly. Conflict resolutions keep main's behaviour and add the geometry work on top: osm_query gating on every provider, restored admin identity for Nominatim outlines, merged analyst follow-up memory, and the cancelled Globe View reset instruction ahead of the generic cancellation.
407 lines
14 KiB
JavaScript
407 lines
14 KiB
JavaScript
// OVERPASS PROXY — which upstream answers count as an answer.
|
|
//
|
|
// One predicate governs cache reads, writes, and stale fallback. A configured
|
|
// upstream's refusal is never data, and only operator-supplied alternatives
|
|
// may be tried. These cases use no live providers.
|
|
//
|
|
// Run with: npm test
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdir, readFile, writeFile, unlink } from 'node:fs/promises';
|
|
import { createHash, randomUUID } from 'node:crypto';
|
|
import path from 'node:path';
|
|
import { Readable } from 'node:stream';
|
|
import createViteConfig, { fetchOverpassPayload, overpassPayloadIsData, readOverpassDisk } from '../vite.config.js';
|
|
import { resolveOverpassUpstreamTag } from '../server/providers/overpass/constants.js';
|
|
|
|
const DATA = { status: 200, body: '{"elements":[]}' };
|
|
|
|
test('disk cache rejects old refusals for fresh and stale reads but preserves last-good data', async () => {
|
|
const key = `overpass-cache-regression-${randomUUID()}`;
|
|
const directory = path.join(process.cwd(), '.gev-cache', 'overpass');
|
|
const file = path.join(
|
|
directory,
|
|
`${createHash('sha1').update(key).digest('hex')}.json`,
|
|
);
|
|
await mkdir(directory, { recursive: true });
|
|
try {
|
|
for (const refusal of [
|
|
{ status: 406 },
|
|
{ status: 429 },
|
|
{ status: 503 },
|
|
{ status: 200, rateLimited: true },
|
|
{ status: 200, runtimeError: true },
|
|
]) {
|
|
await writeFile(
|
|
file,
|
|
JSON.stringify({ ...DATA, cachedAt: Date.now(), ...refusal }),
|
|
);
|
|
assert.equal(
|
|
await readOverpassDisk(key, 60000),
|
|
null,
|
|
`fresh ${JSON.stringify(refusal)}`,
|
|
);
|
|
assert.equal(
|
|
await readOverpassDisk(key, Infinity),
|
|
null,
|
|
`stale ${JSON.stringify(refusal)}`,
|
|
);
|
|
}
|
|
const good = { ...DATA, cachedAt: Date.now() - 120000 };
|
|
await writeFile(file, JSON.stringify(good));
|
|
assert.equal(
|
|
await readOverpassDisk(key, 60000),
|
|
null,
|
|
'expired good data misses normal TTL',
|
|
);
|
|
assert.deepEqual(
|
|
await readOverpassDisk(key, Infinity),
|
|
good,
|
|
'last-good data survives an outage',
|
|
);
|
|
await writeFile(file, '{invalid');
|
|
assert.equal(
|
|
await readOverpassDisk(key, Infinity),
|
|
null,
|
|
'corrupt cache is ignored',
|
|
);
|
|
} finally {
|
|
await unlink(file);
|
|
}
|
|
});
|
|
|
|
// ── The predicate ────────────────────────────────────────────────────────────
|
|
|
|
test('only a 2xx that is neither rate-limited nor a runtime error is data', () => {
|
|
assert.equal(overpassPayloadIsData({ status: 200 }), true);
|
|
assert.equal(overpassPayloadIsData({ status: 204 }), true);
|
|
|
|
// The measured refusal, and its neighbours. `< 500` admitted every one.
|
|
for (const status of [400, 403, 406, 410, 429]) {
|
|
assert.equal(
|
|
overpassPayloadIsData({ status }),
|
|
false,
|
|
`${status} is not data`,
|
|
);
|
|
}
|
|
assert.equal(overpassPayloadIsData({ status: 502 }), false);
|
|
// A 200 can still not be data: Overpass reports runtime failures in the body.
|
|
assert.equal(
|
|
overpassPayloadIsData({ status: 200, runtimeError: true }),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
overpassPayloadIsData({ status: 200, rateLimited: true }),
|
|
false,
|
|
);
|
|
assert.equal(overpassPayloadIsData({}), false);
|
|
assert.equal(overpassPayloadIsData(null), false);
|
|
});
|
|
|
|
// Configured endpoints replace defaults; refusals are sanitized and cooled down.
|
|
test('configured failover accepts empty data and keeps the application identity', async () => {
|
|
const endpoints = [
|
|
'https://first.example/query?secret=one',
|
|
'http://localhost:12345/api',
|
|
];
|
|
const seen = [];
|
|
const payload = await fetchOverpassPayload('data=x', 1024, {
|
|
endpoints,
|
|
fetchImpl: async (url, options) => {
|
|
seen.push(url);
|
|
assert.equal(
|
|
options.headers['User-Agent'],
|
|
'gods-eye-view/0.1 (+https://github.com/bilawalsidhu/gods-eye-view)',
|
|
);
|
|
assert.equal(options.redirect, 'error');
|
|
return new Response(seen.length === 1 ? url : DATA.body, {
|
|
status: seen.length === 1 ? 406 : 200,
|
|
});
|
|
},
|
|
});
|
|
assert.deepEqual(seen, endpoints);
|
|
assert.equal(payload.body, DATA.body);
|
|
assert.equal(payload.endpoint, 'configured');
|
|
});
|
|
|
|
test('406 and 429 honor Retry-After across different queries without more egress', async () => {
|
|
for (const status of [406, 429]) {
|
|
const endpoint = `https://cooldown-${status}.example/private?token=secret`;
|
|
let count = 0,
|
|
now = 1_000_000;
|
|
const options = {
|
|
endpoints: [endpoint],
|
|
now: () => now,
|
|
fetchImpl: async () => {
|
|
count++;
|
|
return new Response(`Refused ${endpoint}`, {
|
|
status,
|
|
headers: { 'Retry-After': '90' },
|
|
});
|
|
},
|
|
};
|
|
const first = await fetchOverpassPayload('first', 1024, options);
|
|
assert.equal(first.status, status);
|
|
assert.equal(first.retryAfterMs, 90_000);
|
|
assert.doesNotMatch(JSON.stringify(first), /secret|https:/);
|
|
now += 30_000;
|
|
const next = await fetchOverpassPayload('different', 1024, options);
|
|
assert.equal(count, 1);
|
|
assert.equal(next.retryAfterMs, 60_000);
|
|
now += 60_001;
|
|
await fetchOverpassPayload('third', 1024, options);
|
|
assert.equal(count, 2);
|
|
}
|
|
});
|
|
|
|
test('HTTP-date Retry-After is respected, and absent delays use bounded backoff', async () => {
|
|
let now = Date.parse('2026-09-23T12:00:00Z');
|
|
const endpoint = 'https://dated.example/api';
|
|
const payload = await fetchOverpassPayload('x', 1024, {
|
|
endpoints: [endpoint],
|
|
now: () => now,
|
|
fetchImpl: async () =>
|
|
new Response('busy', {
|
|
status: 429,
|
|
headers: { 'Retry-After': 'Wed, 23 Sep 2026 12:02:00 GMT' },
|
|
}),
|
|
});
|
|
assert.equal(payload.retryAfterMs, 120_000);
|
|
const options = {
|
|
endpoints: ['https://bounded.example'],
|
|
now: () => now,
|
|
fetchImpl: async () => {
|
|
throw new Error('https://bounded.example/secret');
|
|
},
|
|
};
|
|
for (let i = 0; i < 8; i++) {
|
|
const reply = await fetchOverpassPayload('x', 1024, options);
|
|
assert.ok(reply.retryAfterMs <= 300_000);
|
|
assert.doesNotMatch(reply.body, /secret|https:/);
|
|
now += reply.retryAfterMs + 1;
|
|
}
|
|
});
|
|
|
|
test('production reader rejects oversized, malformed and runtime-failure bodies', async () => {
|
|
for (const [i, body] of [
|
|
'x'.repeat(200),
|
|
'{"remark":"runtime error: timed out","elements":[]}',
|
|
'rate_limited',
|
|
'{}',
|
|
'not json',
|
|
].entries()) {
|
|
const endpoints = [
|
|
`https://bad-${i}.example/`,
|
|
`https://good-${i}.example/`,
|
|
];
|
|
const seen = [];
|
|
const payload = await fetchOverpassPayload('data=x', 100, {
|
|
endpoints,
|
|
fetchImpl: async (url) => {
|
|
seen.push(url);
|
|
return new Response(seen.length === 1 ? body : DATA.body);
|
|
},
|
|
});
|
|
assert.equal(payload.body, DATA.body);
|
|
assert.deepEqual(seen, endpoints);
|
|
}
|
|
});
|
|
|
|
test('element data that mentions a failure phrase is data, and sets no cooldown', async () => {
|
|
const names = ['Timed Out Escape Rooms', 'Runtime Error Bar', 'Out of Memory Lane', 'Too Many Requests Cafe', 'rate_limited'];
|
|
for (const [i, name] of names.entries()) {
|
|
const body = JSON.stringify({ elements: [{ type: 'node', id: 1, lat: 1, lon: 2, tags: { name } }] });
|
|
let count = 0;
|
|
const options = { endpoints: [`https://phrase-${i}.example/`], fetchImpl: async () => {
|
|
count++; return new Response(body);
|
|
} };
|
|
const payload = await fetchOverpassPayload('first', 1024, options);
|
|
assert.equal(payload.status, 200, name);
|
|
assert.ok(overpassPayloadIsData(payload), name);
|
|
assert.ok(payload.body.includes(name), name);
|
|
await fetchOverpassPayload('different', 1024, options);
|
|
assert.equal(count, 2, name);
|
|
}
|
|
const remark = JSON.stringify({ elements: [], remark: 'runtime error: rate_limited' });
|
|
const limited = await fetchOverpassPayload('x', 1024, { endpoints: ['https://remark.example/'],
|
|
fetchImpl: async () => new Response(remark, { headers: { 'Retry-After': '90' } }) });
|
|
assert.equal(limited.status, 429);
|
|
assert.equal(limited.retryAfterMs, 90_000);
|
|
const error = await fetchOverpassPayload('x', 1024, { endpoints: ['https://error.example/'],
|
|
fetchImpl: async () => new Response('{"error":"rate_limited"}', { headers: { 'Retry-After': '120' } }) });
|
|
assert.equal(error.status, 429);
|
|
assert.equal(error.retryAfterMs, 120_000);
|
|
});
|
|
|
|
test('caller cancellation aborts transport without cooling down the upstream', async () => {
|
|
const endpoint = `https://cancelled-${randomUUID()}.example/api`;
|
|
const controller = new AbortController();
|
|
const entered = Promise.withResolvers();
|
|
const first = fetchOverpassPayload('data=x', 1024, {
|
|
endpoints: [endpoint],
|
|
signal: controller.signal,
|
|
fetchImpl: async (_url, { signal }) => {
|
|
entered.resolve();
|
|
return new Promise((_, reject) => {
|
|
signal.addEventListener('abort', () => reject(signal.reason), {
|
|
once: true,
|
|
});
|
|
});
|
|
},
|
|
});
|
|
await entered.promise;
|
|
controller.abort();
|
|
await assert.rejects(first);
|
|
|
|
let retried = 0;
|
|
const next = await fetchOverpassPayload('data=y', 1024, {
|
|
endpoints: [endpoint],
|
|
fetchImpl: async () => {
|
|
retried += 1;
|
|
return new Response(DATA.body, { status: 200 });
|
|
},
|
|
});
|
|
assert.equal(
|
|
retried,
|
|
1,
|
|
'the next live caller is not held behind a cooldown',
|
|
);
|
|
assert.equal(next.body, DATA.body);
|
|
});
|
|
|
|
function proxyHandler() {
|
|
const plugin = createViteConfig({ mode: 'test' }).plugins.find(
|
|
(p) => p.name === 'overpass-proxy',
|
|
);
|
|
const routes = new Map();
|
|
plugin.configureServer({
|
|
middlewares: { use: (route, handler) => routes.set(route, handler) },
|
|
});
|
|
return routes.get('/api/overpass');
|
|
}
|
|
|
|
function invoke(handler, body) {
|
|
const req = Readable.from([Buffer.from(body)]);
|
|
Object.assign(req, {
|
|
method: 'POST',
|
|
headers: {},
|
|
socket: { remoteAddress: '127.0.0.1' },
|
|
});
|
|
return new Promise((resolve, reject) => {
|
|
const res = {
|
|
writeHead(status, headers) {
|
|
this.status = status;
|
|
this.headers = headers;
|
|
},
|
|
end(body) {
|
|
resolve({ status: this.status, headers: this.headers, body });
|
|
},
|
|
};
|
|
Promise.resolve(handler(req, res)).catch(reject);
|
|
});
|
|
}
|
|
|
|
test('coalesced outage callers both receive last-good data, never a cached refusal', async (t) => {
|
|
const handler = proxyHandler();
|
|
const prior = process.env.OVERPASS_UPSTREAMS;
|
|
t.after(() => {
|
|
if (prior === undefined) delete process.env.OVERPASS_UPSTREAMS;
|
|
else process.env.OVERPASS_UPSTREAMS = prior;
|
|
});
|
|
for (const status of [406, 503, 429]) {
|
|
process.env.OVERPASS_UPSTREAMS = `https://outage-${status}.example/api`;
|
|
const query = `[out:json][timeout:12];node(around:10,30.27,-97.74)["name"="${randomUUID()}"];out;`;
|
|
const body = `data=${encodeURIComponent(query)}`;
|
|
const directory = path.join(process.cwd(), '.gev-cache', 'overpass');
|
|
const file = path.join(
|
|
directory,
|
|
`${createHash('sha1').update(body).digest('hex')}.json`,
|
|
);
|
|
await mkdir(directory, { recursive: true });
|
|
// Last-good data was fetched from the instance configured above, so it
|
|
// carries that instance's tag the way the transport now writes it.
|
|
const stale = {
|
|
...DATA,
|
|
cachedAt: Date.now() - 40 * 86400000,
|
|
upstreams: resolveOverpassUpstreamTag(),
|
|
};
|
|
await writeFile(file, JSON.stringify(stale));
|
|
const entered = Promise.withResolvers();
|
|
const release = Promise.withResolvers();
|
|
let fetches = 0;
|
|
const mock = t.mock.method(globalThis, 'fetch', async () => {
|
|
fetches++;
|
|
entered.resolve();
|
|
await release.promise;
|
|
return new Response('upstream unavailable', { status });
|
|
});
|
|
try {
|
|
const first = invoke(handler, body);
|
|
await entered.promise;
|
|
const second = invoke(handler, body);
|
|
// The second request consumes its in-memory stream and joins the pending
|
|
// promise before releasing upstream. No network or elapsed-time sleep.
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
release.resolve();
|
|
for (const response of await Promise.all([first, second])) {
|
|
assert.equal(
|
|
response.status,
|
|
200,
|
|
`${status}: both callers use last-good data`,
|
|
);
|
|
assert.equal(response.body, DATA.body);
|
|
assert.equal(response.headers['X-Overpass-Cache'], 'STALE');
|
|
}
|
|
assert.equal(fetches, 1, 'one shared configured request');
|
|
assert.deepEqual(JSON.parse(await readFile(file, 'utf8')), stale);
|
|
} finally {
|
|
release.resolve();
|
|
mock.mock.restore();
|
|
await unlink(file);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('a disk entry is not served after OVERPASS_UPSTREAMS is repointed', async (t) => {
|
|
const prior = process.env.OVERPASS_UPSTREAMS;
|
|
t.after(() => {
|
|
if (prior === undefined) delete process.env.OVERPASS_UPSTREAMS;
|
|
else process.env.OVERPASS_UPSTREAMS = prior;
|
|
});
|
|
const key = `overpass-upstream-switch-${randomUUID()}`;
|
|
const directory = path.join(process.cwd(), '.gev-cache', 'overpass');
|
|
const file = path.join(
|
|
directory,
|
|
`${createHash('sha1').update(key).digest('hex')}.json`,
|
|
);
|
|
await mkdir(directory, { recursive: true });
|
|
try {
|
|
// Written while one instance was configured...
|
|
process.env.OVERPASS_UPSTREAMS = 'https://first.example/api/interpreter';
|
|
const first = resolveOverpassUpstreamTag();
|
|
await writeFile(
|
|
file,
|
|
JSON.stringify({ ...DATA, cachedAt: Date.now(), upstreams: first }),
|
|
);
|
|
assert.ok(await readOverpassDisk(key, 60000), 'its own entry is a hit');
|
|
|
|
// ...is not an answer once the operator points at a different one, on the
|
|
// stale path either: the key is the query alone, so it would otherwise hit.
|
|
process.env.OVERPASS_UPSTREAMS = 'https://second.example/api/interpreter';
|
|
assert.notEqual(resolveOverpassUpstreamTag(), first, 'tag follows the env');
|
|
assert.equal(await readOverpassDisk(key, 60000), null, 'fresh read refused');
|
|
assert.equal(
|
|
await readOverpassDisk(key, Infinity),
|
|
null,
|
|
'stale read refused',
|
|
);
|
|
|
|
// With nothing configured there is no instance to contradict, and serving
|
|
// last-good data is what the not-configured path exists for.
|
|
delete process.env.OVERPASS_UPSTREAMS;
|
|
assert.ok(await readOverpassDisk(key, Infinity), 'unconfigured still serves');
|
|
} finally {
|
|
await unlink(file);
|
|
}
|
|
});
|