feat(outlines): restore voice outlines without public Overpass

use bundled place boundaries and shared OpenFreeMap tiles before a guarded Nominatim fallback

preserve multipart shapes, holes and cancellation,
and keep pending annotations visible while ground sampling finishes across frames

keep non-Latin name distinctions when selecting provider results,
and retain an outline anchor only when it lies in filled geometry
This commit is contained in:
manjunath committed 2026-10-27 13:24:02 +00:00
commit d4434aa6d7
1489 files changed
+196442 -12353

No files matched your search

+319
View File
@@ -0,0 +1,319 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as fs from 'node:fs/promises';
import {
mkdtemp,
mkdir,
writeFile,
readFile,
readdir,
rm,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
FRAGMENT_DIR,
STAGING_DIR,
assembleChangelog,
assembleFragments,
orderFragmentNames,
recoverInterruptedBatch,
} from '../../scripts/assemble-changelog.mjs';
const EXISTING =
'# Changelog\n\n- An entry that was already released (someone, #1).\n';
const repositoryRoot = fileURLToPath(new URL('../..', import.meta.url));
async function fixture(t, fragments = {}) {
const root = await mkdtemp(path.join(tmpdir(), 'gev-changelog-'));
t.after(() => rm(root, { recursive: true, force: true }));
await mkdir(path.join(root, FRAGMENT_DIR));
await writeFile(
path.join(root, FRAGMENT_DIR, 'README.md'),
'# How to add one\n',
);
await writeFile(path.join(root, 'CHANGELOG.md'), EXISTING);
for (const [name, body] of Object.entries(fragments)) {
await writeFile(path.join(root, FRAGMENT_DIR, name), body);
}
return root;
}
test('the batch reads newest pull request first', () => {
assert.deepEqual(orderFragmentNames(['7-b.md', '112-c.md', '19-a.md']), [
'112-c.md',
'19-a.md',
'7-b.md',
]);
});
test('the directory README is documentation, never an entry', () => {
assert.deepEqual(orderFragmentNames(['README.md', '5-a.md']), ['5-a.md']);
});
test('two fragments claiming one pull request fail loudly, not silently', () => {
assert.throws(
() => orderFragmentNames(['5-first.md', '5-second.md']),
/Two fragments claim pull request 5/,
);
});
test('a name that is not <pull request>-<slug>.md is refused', () => {
for (const name of ['notes.md', '5_underscore.md', '5-Caps.md', '5-a.txt']) {
assert.throws(() => orderFragmentNames([name]), /Unexpected file/, name);
}
});
test('fragment text is spliced in unchanged, above the existing entries', () => {
// Wrapped, indented and punctuated the way a real entry is: the continuation
// indent and the blank line inside the entry are the parts a reflow would eat.
const entry = [
'- A new thing, with `code` and a — dash. The second line is indented the',
' way the surrounding entries wrap, and it keeps its two spaces.',
'',
' A second paragraph, because some entries have one (someone, #9).',
].join('\n');
const result = assembleChangelog(EXISTING, [`${entry}\n\n\n`]);
assert.equal(
result,
`# Changelog\n\n${entry}\n\n- An entry that was already released (someone, #1).\n`,
);
// The entry survives byte for byte: drawTool.test.mjs reads this file as
// prose and slices a fixed window, so reflowing would move text across it.
assert.ok(result.includes(entry));
});
test('a CHANGELOG without the heading is refused rather than prepended to', () => {
assert.throws(
() => assembleChangelog('- orphan entry\n', ['- new\n']),
/must start with "# Changelog"/,
);
});
test('assembling consumes the fragments and leaves the README', async (t) => {
const root = await fixture(t, {
'20-second.md': '- Second (someone, #20).',
'100-first.md': '- First (someone, #100).',
});
const { consumed } = await assembleFragments(root, '--write');
assert.deepEqual(consumed, ['100-first.md', '20-second.md']);
const changelog = await readFile(path.join(root, 'CHANGELOG.md'), 'utf8');
assert.match(
changelog,
/# Changelog\n\n- First \(someone, #100\)\.\n\n- Second \(someone, #20\)\.\n\n- An entry that was already released/,
);
assert.deepEqual(await readdir(path.join(root, FRAGMENT_DIR)), ['README.md']);
});
test('a rejected batch leaves the directory and the file untouched', async (t) => {
const root = await fixture(t, {
'8-first.md': '- First (someone, #8).',
'8-again.md': '- Again (someone, #8).',
});
await assert.rejects(
assembleFragments(root, '--write'),
/Two fragments claim pull request 8/,
);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
assert.deepEqual((await readdir(path.join(root, FRAGMENT_DIR))).sort(), [
'8-again.md',
'8-first.md',
'README.md',
]);
});
test('check mode changes nothing on disk', async (t) => {
const root = await fixture(t, { '3-a.md': '- Something (someone, #3).' });
const { consumed } = await assembleFragments(root, '--check');
assert.deepEqual(consumed, ['3-a.md']);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
assert.deepEqual((await readdir(path.join(root, FRAGMENT_DIR))).sort(), [
'3-a.md',
'README.md',
]);
});
test('an empty directory is a no-op, so a release can always run it', async (t) => {
const root = await fixture(t);
const { consumed } = await assembleFragments(root, '--write');
assert.deepEqual(consumed, []);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
});
test('a mode that is neither check nor write is refused', async (t) => {
const root = await fixture(t);
await assert.rejects(assembleFragments(root, '--force'), /--check\|--write/);
});
// Failure injection. Every filesystem call the write path makes is a point
// where a release can die: a full disk, a permissions error, a killed process.
const IO_CALLS = ['readdir', 'readFile', 'mkdir', 'writeFile', 'rename', 'rm'];
const BATCH = {
'20-second.md': '- Second (someone, #20).',
'100-first.md': '- First (someone, #100).',
'7-third.md': '- Third (someone, #7).',
};
/**
* The real filesystem, except that `call` throws the `nth` time it runs. With
* `crash`, every later call throws as well, the way a killed process makes no
* further calls: the in-process rollback cannot run either, so the next
* invocation is the only thing that can recover.
*/
function failing(call, nth, { crash = false } = {}) {
let seen = 0;
let dead = false;
const io = { ...fs };
for (const name of IO_CALLS) {
io[name] = async (...args) => {
if (dead) throw new Error('process already gone');
if (name === call && ++seen === nth) {
dead = crash;
throw new Error(`injected ${call} failure #${nth}`);
}
return fs[name](...args);
};
}
return io;
}
/** How many times one uninterrupted `--write` makes each filesystem call. */
async function countCalls(t) {
const root = await fixture(t, BATCH);
const counts = Object.fromEntries(IO_CALLS.map((name) => [name, 0]));
const io = { ...fs };
for (const name of IO_CALLS) {
io[name] = (...args) => {
counts[name] += 1;
return fs[name](...args);
};
}
await assembleFragments(root, '--write', io);
return {
counts,
expected: await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
};
}
test('a write that fails at any call never lets a retry duplicate or lose an entry', async (t) => {
const { counts, expected } = await countCalls(t);
// The write path itself: one mkdir, one writeFile, a rename per fragment plus
// the commit rename, and the cleanup rm. Fewer would mean a step went missing.
assert.ok(counts.rename >= Object.keys(BATCH).length + 1);
for (const crash of [false, true]) {
for (const call of IO_CALLS) {
for (let nth = 1; nth <= counts[call]; nth += 1) {
const label = `${crash ? 'crash' : 'error'} at ${call} #${nth}`;
const root = await fixture(t, BATCH);
await assert.rejects(
assembleFragments(root, '--write', failing(call, nth, { crash })),
/injected|already gone/,
label,
);
await assembleFragments(root, '--write');
const changelog = await readFile(
path.join(root, 'CHANGELOG.md'),
'utf8',
);
assert.equal(changelog, expected, label);
for (const pr of [100, 20, 7, 1]) {
assert.equal(
changelog.split(`(someone, #${pr}).`).length - 1,
1,
`${label}: #${pr} appears exactly once`,
);
}
assert.deepEqual(
await readdir(path.join(root, FRAGMENT_DIR)),
['README.md'],
label,
);
}
}
}
});
test('an error before the commit rolls the batch back in the same run', async (t) => {
const root = await fixture(t, BATCH);
// The second rename has already moved one fragment into the staging directory.
await assert.rejects(
assembleFragments(root, '--write', failing('rename', 2)),
/injected rename failure #2/,
);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
assert.deepEqual(
(await readdir(path.join(root, FRAGMENT_DIR))).sort(),
[...Object.keys(BATCH), 'README.md'].sort(),
);
});
test('check mode reports a batch an interrupted release left staged', async (t) => {
const root = await fixture(t, BATCH);
await assert.rejects(
assembleFragments(root, '--write', failing('rename', 2, { crash: true })),
);
await assert.rejects(
assembleFragments(root, '--check'),
new RegExp(`${STAGING_DIR}/ holds a batch an interrupted release`),
);
assert.equal(await recoverInterruptedBatch(root), 'rolled-back');
assert.equal(await recoverInterruptedBatch(root), 'none');
});
test('recovery refuses to roll a fragment back over a file of the same name', async (t) => {
const root = await fixture(t, BATCH);
await assert.rejects(
assembleFragments(root, '--write', failing('rename', 2, { crash: true })),
);
await writeFile(
path.join(root, FRAGMENT_DIR, '100-first.md'),
'- Rewritten.',
);
await assert.rejects(
assembleFragments(root, '--write'),
/Cannot roll back .*100-first\.md/,
);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
});
// The two below run against the repository itself, so a fragment that the
// release could not assemble fails `npm test` in the pull request that adds it
// rather than at release time, when its author has moved on.
test('every pending fragment in the repository can be assembled', async () => {
const names = await readdir(path.join(repositoryRoot, FRAGMENT_DIR));
assert.ok(
names.includes('README.md'),
`${FRAGMENT_DIR}/README.md documents the convention and keeps the directory tracked`,
);
// Throws on a name that is not <pull request>-<slug>.md, and on two fragments
// claiming one pull request number. Every other file is an entry.
assert.equal(orderFragmentNames(names).length, names.length - 1);
});
test('CHANGELOG.md still carries the heading a release splices under', async () => {
const changelog = await readFile(
path.join(repositoryRoot, 'CHANGELOG.md'),
'utf8',
);
assert.ok(changelog.startsWith('# Changelog\n'));
});
+150 -28
View File
@@ -6,10 +6,17 @@ 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';
function install(plugin) {
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();
plugin.configureServer({
assert.equal(typeof plugin[hook], 'function', `${hook} is available`);
plugin[hook]({
middlewares: {
use(route, handler) {
routes.set(route, handler);
@@ -17,9 +24,16 @@ function install(plugin) {
},
});
assert.equal(routes.size, 1);
return async (url = '/', method = 'GET') => {
return async (url = '/', method = 'GET', headers = {}) => {
const res = {
headersSent: false,
headers: {},
set statusCode(status) {
this.status = status;
},
setHeader(name, value) {
this.headers[name.toLowerCase()] = value;
},
writeHead(status, headers) {
Object.assign(this, { status, headers, headersSent: true });
},
@@ -27,7 +41,7 @@ function install(plugin) {
this.body = body;
},
};
await [...routes.values()][0]({ url, method }, res);
await [...routes.values()][0]({ url, method, headers, socket: {} }, res);
return res;
};
}
@@ -53,11 +67,34 @@ function isolate(t, env = {}) {
}
const json = (res) => JSON.parse(res.body);
/** A response that streams 1 MiB chunks of `text` well past every cap, then ends. */
function oversizedResponse(text) {
const state = { pulled: 0, cancelled: false };
const chunk = new TextEncoder().encode(
text.repeat(Math.ceil((1 << 20) / text.length)),
);
const response = new Response(
new ReadableStream(
{
pull(controller) {
if (++state.pulled > 40) controller.close();
else controller.enqueue(chunk);
},
cancel() {
state.cancelled = true;
},
},
{ highWaterMark: 0 },
),
);
return { response, state };
}
test('standalone composition mounts every extracted provider exactly once without acquisition', (t) => {
t.mock.method(globalThis, 'fetch', () => {
throw Error('construction must not fetch');
});
const plugins = localProviderPlugins();
const plugins = localProviderPlugins({ nominatimStorageDir });
for (const factory of [
terrainHeightsProxy,
tomtomProxy,
@@ -67,7 +104,7 @@ test('standalone composition mounts every extracted provider exactly once withou
assert.equal(plugins.filter((p) => p.name === factory().name).length, 1);
});
test('terrain middleware chunks missing points and reconstructs repeated/reordered requests from cache', async (t) => {
test('terrain middleware bounds missing points and reconstructs repeated/reordered requests from cache', async (t) => {
isolate(t);
let calls = 0;
const sizes = [];
@@ -85,25 +122,23 @@ test('terrain middleware chunks missing points and reconstructs repeated/reorder
});
});
const request = install(terrainHeightsProxy());
const points = Array.from({ length: 257 }, (_, i) => `${i / 100},1`);
const points = Array.from({ length: 64 }, (_, i) => `${i / 100},1`);
const res = await request('/?points=' + points.join(';'));
assert.equal(res.status, 200);
// Tracks UPSTREAM_CHUNK in server/providers/terrain.js (64): 257 points
// split into four full chunks and a remainder.
assert.deepEqual(sizes, [64, 64, 64, 64, 1]);
const reordered = await request('/?points=2.56,1;0,1;2.56,1');
assert.deepEqual(sizes, [64]);
const reordered = await request('/?points=0.63,1;0,1;0.63,1');
assert.deepEqual(json(reordered), {
results: [{ ellipsoid: 102.56 }, { ellipsoid: 100 }, { ellipsoid: 102.56 }],
results: [{ ellipsoid: 100.63 }, { ellipsoid: 100 }, { ellipsoid: 100.63 }],
});
// Five chunks for the first batch; the reordered request is fully cached.
assert.equal(calls, 5);
// One bounded batch; the reordered request is fully cached.
assert.equal(calls, 1);
assert.equal((await request('/?points=invalid')).status, 400);
assert.equal(
(await request('/?points=' + Array(2001).fill('0,1').join(';'))).status,
500,
413,
);
// Rejected requests add no upstream calls: still the five from the first batch.
assert.equal(calls, 5);
// Rejected requests add no upstream calls.
assert.equal(calls, 1);
});
test('terrain middleware migrates valid legacy disk points without fabricating omitted heights', async (t) => {
@@ -129,6 +164,7 @@ test('terrain middleware migrates valid legacy disk points without fabricating o
test('traffic middleware preserves keyless mode, caching, stale budget fallback and UTC rollover', async (t) => {
isolate(t, { TOMTOM_API_KEY: '', TOMTOM_DAILY_TILE_BUDGET: '1' });
let now = Date.UTC(2026, 8, 12, 12);
const fetchedAt = String(now);
t.mock.method(Date, 'now', () => now);
let calls = 0;
t.mock.method(globalThis, 'fetch', async (raw) => {
@@ -153,10 +189,10 @@ test('traffic middleware preserves keyless mode, caching, stale budget fallback
assert.equal(json(await request('/status')).dailyCount, 1);
assert.equal(calls, 1);
now += 120001;
assert.equal(
(await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],
'STALE-BUDGET',
);
const staleTile = await request('/flow/8/1/1.pbf');
assert.equal(staleTile.headers['x-tomtom-cache'], 'STALE-BUDGET');
// The stale copy still says when TomTom produced it.
assert.equal(staleTile.headers['x-tomtom-fetched-at'], fetchedAt);
assert.equal((await request('/flow/8/2/1.pbf')).status, 429);
assert.equal(calls, 1);
now += 86400000;
@@ -168,6 +204,87 @@ test('traffic middleware preserves keyless mode, caching, stale budget fallback
assert.equal(calls, 2);
});
test('traffic refuses cross-site requests in dev and preview before spending the TomTom budget', async (t) => {
isolate(t, { TOMTOM_API_KEY: 'fixture-key', TOMTOM_DAILY_TILE_BUDGET: '3' });
let calls = 0;
t.mock.method(globalThis, 'fetch', async () => {
calls++;
return new Response(new Uint8Array([1, 2, 3]));
});
const host = 'localhost:5173';
const refused = [
{ host, 'sec-fetch-site': 'cross-site' }, // <img src> from any web page
{ host, 'sec-fetch-site': 'same-site' },
{ host, origin: 'https://evil.example' }, // fetch from another page
{ host, origin: 'null' }, // sandboxed frame
{ host, 'x-forwarded-for': '203.0.113.9' }, // through a proxy
];
for (const [mode, hook] of [
['dev', 'configureServer'],
['preview', 'configurePreviewServer'],
]) {
const request = install(tomtomProxy(), hook);
// Each refused shape asks for distinct uncached tiles: without the gate
// every one would be a billed attempt counted against the daily budget.
let x = 0;
for (const headers of refused)
for (let i = 0; i < 2; i++) {
const res = await request(`/flow/16/${x++}/0.pbf`, 'GET', headers);
assert.equal(res.status, 403, `${mode}: ${JSON.stringify(headers)}`);
}
assert.equal(
(await request('/status', 'GET', refused[0])).status,
403,
`${mode}: status is gated too`,
);
assert.equal(
calls,
mode === 'dev' ? 0 : 2,
`${mode}: no refused upstream work`,
);
const app = { host, 'sec-fetch-site': 'same-origin' };
assert.equal(json(await request('/status', 'GET', app)).dailyCount, 0);
// The app itself and loopback tools (no browser headers) still get tiles.
const tile = await request('/flow/8/1/1.pbf', 'GET', app);
assert.equal(tile.status, 200);
assert.equal(tile.headers['x-tomtom-cache'], 'MISS');
assert.equal((await request('/flow/8/2/1.pbf')).status, 200);
assert.equal(
(
await request('/flow/8/1/1.pbf', 'GET', {
host,
origin: `http://${host}`,
})
).headers['x-tomtom-cache'],
'HIT',
);
assert.equal(calls, mode === 'dev' ? 2 : 4);
}
});
test('traffic middleware answers 502 to an oversized tile without caching it', async (t) => {
isolate(t, { TOMTOM_API_KEY: 'fixture-key', TOMTOM_DAILY_TILE_BUDGET: '' });
const bodies = [];
t.mock.method(globalThis, 'fetch', async () => {
const oversized = oversizedResponse('\x01');
bodies.push(oversized.state);
return oversized.response;
});
const request = install(tomtomProxy());
const first = await request('/flow/8/1/1.pbf');
assert.equal(first.status, 502);
assert.equal(bodies[0].cancelled, true);
assert.ok(bodies[0].pulled <= 9, 'reading stops at the 8 MiB cap');
assert.ok(
!fsp.writeFile.mock.calls.some(({ arguments: [file] }) =>
String(file).endsWith('.pbf'),
),
'no tile reached the disk cache',
);
assert.equal((await request('/flow/8/1/1.pbf')).status, 502);
assert.equal(bodies.length, 2, 'nothing was cached in memory');
});
test('FIRMS retains a large successful source during partial failure and filters stale data at serve time', async (t) => {
isolate(t, { FIRMS_MAP_KEY: '' });
let now = Date.UTC(2026, 8, 12, 12);
@@ -276,21 +393,26 @@ test('terrain middleware retains successful chunks around a failure and retries
});
const request = install(terrainHeightsProxy());
const points = Array.from({ length: 130 }, (_, i) => `${i / 100},1`);
const url = '/?points=' + points.join(';');
assert.equal((await request(url)).status, 502);
const urls = [0, 64, 128].map(
(start) => '/?points=' + points.slice(start, start + 64).join(';'),
);
assert.deepEqual(
requests.map((p) => p.length),
[64, 64, 2],
await Promise.all(urls.map(async (url) => (await request(url)).status)),
[200, 502, 200],
);
assert.deepEqual(
requests.map((p) => p.length).sort((a, b) => a - b),
[2, 64, 64],
);
fail = false;
const recovered = await request(url);
assert.equal(recovered.status, 200);
const recovered = await Promise.all(urls.map((url) => request(url)));
assert.ok(recovered.every((response) => response.status === 200));
assert.deepEqual(
requests[3],
Array.from({ length: 64 }, (_, i) => [(i + 64) / 100, 1]),
);
assert.deepEqual(
json(recovered).results,
recovered.flatMap((response) => json(response).results),
Array.from({ length: 130 }, (_, i) => ({ ellipsoid: i / 100 + 100 })),
);
assert.equal(requests.length, 4);
@@ -0,0 +1,94 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { once } from 'node:events';
import { createGoogleTokenSource } from '../maps/googleTokens.js';
import { googleTokenUnavailablePlugin } from '../../server/standalone/google-token-unavailable.js';
async function withServer({ beforeFallback } = {}, check) {
const middlewares = [];
const app = {
middlewares: {
use: (handler) => middlewares.push(handler),
},
};
if (beforeFallback) app.middlewares.use(beforeFallback);
googleTokenUnavailablePlugin().configureServer(app);
app.middlewares.use((_req, res) => {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unknown API route' }));
});
const server = createServer((req, res) => {
let index = 0;
const next = () => middlewares[index++]?.(req, res, next);
next();
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const address = server.address();
const url = 'http://127.0.0.1:' + address.port;
try {
await check(url);
} finally {
server.close();
await once(server, 'close');
}
}
test('keyless GET and HEAD return uncached 204 instead of 404', async () => {
await withServer({}, async (url) => {
for (const method of ['GET', 'HEAD']) {
const response = await fetch(url + '/api/google/tiles-token?probe=1', {
method,
});
assert.equal(response.status, 204);
assert.equal(response.headers.get('cache-control'), 'no-store');
assert.equal(await response.text(), '');
}
});
});
test('the existing client treats unavailable tokens as null', async () => {
await withServer({}, async (url) => {
const source = createGoogleTokenSource({
fetchImpl: (path, options) => fetch(new URL(path, url), options),
});
assert.equal(await source.token(), null);
});
});
test('unrelated API paths and unsupported methods remain 404', async () => {
await withServer({}, async (url) => {
for (const [path, method] of [
['/api/google/tiles-token/extra', 'GET'],
['/api/not-a-token', 'GET'],
['/api/google/tiles-token', 'POST'],
]) {
const response = await fetch(new URL(path, url), { method });
assert.equal(response.status, 404);
}
});
});
test('a real token provider registered first still wins', async () => {
await withServer(
{
beforeFallback: (req, res, next) => {
if (req.url !== '/api/google/tiles-token') return next();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
accessToken: 'server-token',
expiresAt: 10000000000000,
}),
);
},
},
async (url) => {
const response = await fetch(url + '/api/google/tiles-token');
assert.equal(response.status, 200);
assert.equal((await response.json()).accessToken, 'server-token');
},
);
});
+7 -2
View File
@@ -156,13 +156,18 @@ test('self package imports cannot evade portable ownership', (t) => {
assert.throws(() => checkImportDirections(root), /Source imports rendering/);
});
test('compatibility exceptions are limited to the two existing composition entries', (t) => {
test('the compatibility exception is limited to the existing composition entry', (t) => {
const { root, write } = fixture(t);
write('src/ui.js', "import './standalone/catalog.js';");
write('src/standalone/catalog.js');
write('server/providers/local.js', "import '../standalone/key-setup.js';");
write('server/standalone/key-setup.js');
assert.doesNotThrow(() => checkImportDirections(root));
write('src/ui.js', "import './standalone/catalog.js';");
assert.throws(
() => checkImportDirections(root),
/Reusable module imports standalone/,
);
write('src/ui.js');
write('src/ui/new.js', "import '../standalone/catalog.js';");
assert.throws(
() => checkImportDirections(root),
+384 -34
View File
@@ -37,6 +37,30 @@ function install(plugin, preview = false) {
};
}
/** A response that streams 1 MiB chunks of `text` well past every cap, then ends. */
function oversizedResponse(text, init = {}) {
const state = { pulled: 0, cancelled: false };
const chunk = new TextEncoder().encode(
text.repeat(Math.ceil((1 << 20) / text.length)),
);
const response = new Response(
new ReadableStream(
{
pull(controller) {
if (++state.pulled > 40) controller.close();
else controller.enqueue(chunk);
},
cancel() {
state.cancelled = true;
},
},
{ highWaterMark: 0 },
),
init,
);
return { response, state };
}
function environment(t, values) {
for (const [key, value] of Object.entries(values)) {
const original = process.env[key];
@@ -93,11 +117,11 @@ test('OpenSky state and track routes share tokens, retain cache and use regional
});
const states = install(providers.openSkyProxy());
assert.equal(
(await states('/api/opensky', '?lat=30&lon=-97')).statusCode,
(await states('/api/flights', '?lat=30&lon=-97')).statusCode,
200,
);
assert.equal(
(await states('/api/opensky', '?lat=30&lon=-97')).headers[
(await states('/api/flights', '?lat=30&lon=-97')).headers[
'x-opensky-cache'
],
'HIT',
@@ -105,14 +129,14 @@ test('OpenSky state and track routes share tokens, retain cache and use regional
assert.equal(calls.length, 2);
const tracks = install(providers.trackBackfillProxies(), true);
assert.equal(
(await tracks('/api/opensky-track', '?icao24=ABC123')).statusCode,
(await tracks('/api/flights/track', '?icao24=ABC123')).statusCode,
200,
);
await tracks('/api/opensky-track', '?icao24=abc123');
await tracks('/api/flights/track', '?icao24=abc123');
assert.equal(calls.filter((call) => call.url.includes('/token')).length, 1);
assert.equal(calls.filter((call) => call.url.includes('/tracks/')).length, 1);
assert.equal(
(await tracks('/api/adsblol/trace', '?hex=invalid')).statusCode,
(await tracks('/api/military/track', '?hex=invalid')).statusCode,
400,
);
now += 130_000;
@@ -131,7 +155,7 @@ test('OpenSky state and track routes share tokens, retain cache and use regional
);
process.env.OPENSKY_AUTH_MODE = 'anon';
const fallback = await install(fresh.openSkyProxy())(
'/api/opensky',
'/api/flights',
'?lat=30&lon=-97',
);
assert.equal(fallback.statusCode, 200);
@@ -174,7 +198,7 @@ test('OpenSky global snapshot attempts are time-limited, retried once, then fall
`../../server/providers/aircraft/opensky.js?timeout=${Math.random()}`
);
const response = await install(fresh.openSkyProxy())(
'/api/opensky',
'/api/flights',
'?lat=30&lon=-97',
);
return { response, attempts };
@@ -193,6 +217,130 @@ test('OpenSky global snapshot attempts are time-limited, retried once, then fall
assert.equal(stalled.response.headers['x-flight-source'], 'adsb.lol');
});
const openSkyAnonEnv = {
OPENSKY_CLIENT_ID: undefined,
OPENSKY_CLIENT_SECRET: undefined,
OPENSKY_AUTH_MODE: 'anon',
OPENSKY_USERNAME: undefined,
OPENSKY_PASSWORD: undefined,
};
const regionalFallback = () =>
Response.json({
now: Date.now() / 1000,
ac: [{ hex: 'abc123', lat: 30, lon: -97, alt_baro: 10000 }],
});
test('OpenSky 429 starts the cooldown from headers and cancels the body unread', async (t) => {
environment(t, openSkyAnonEnv);
t.mock.method(console, 'warn', () => {});
t.mock.method(console, 'error', () => {});
let states = 0;
let limited;
t.mock.method(globalThis, 'fetch', async (url) => {
if (url.includes('/states/')) {
states++;
limited = oversizedResponse('x', {
status: 429,
headers: { 'x-rate-limit-retry-after-seconds': '600' },
});
return limited.response;
}
if (url.includes('/lat/')) return regionalFallback();
throw Error(`Unexpected URL: ${url}`);
});
const fresh = await import(
`../../server/providers/aircraft/opensky.js?ratelimit=${Math.random()}`
);
const request = install(fresh.openSkyProxy());
const first = await request('/api/flights', '?lat=30&lon=-97');
assert.equal(first.statusCode, 200);
assert.equal(first.headers['x-flight-source'], 'adsb.lol');
assert.equal(states, 1, 'a rate-limited snapshot is not retried');
assert.equal(limited.state.cancelled, true);
assert.ok(limited.state.pulled <= 1, 'the 429 body is not read');
const cooling = await request('/api/flights', '?lat=30&lon=-97');
assert.equal(cooling.statusCode, 200);
assert.equal(
cooling.headers['x-opensky-auth-reason'],
'opensky_cooldown_regional_fallback',
);
assert.equal(states, 1, 'the cooldown holds off the next upstream attempt');
});
test('OpenSky 429 without retry-after cools down for the 2 minute default', async (t) => {
environment(t, openSkyAnonEnv);
t.mock.method(console, 'warn', () => {});
t.mock.method(console, 'error', () => {});
const start = Date.now();
let now = start;
t.mock.method(Date, 'now', () => now);
let states = 0;
t.mock.method(globalThis, 'fetch', async (url) => {
if (url.includes('/states/')) {
states++;
return new Response('{}', { status: 429 });
}
if (url.includes('/lat/')) return regionalFallback();
throw Error(`Unexpected URL: ${url}`);
});
const fresh = await import(
`../../server/providers/aircraft/opensky.js?ratelimit-default=${Math.random()}`
);
const request = install(fresh.openSkyProxy());
await request('/api/flights', '?lat=30&lon=-97');
assert.equal(states, 1);
// Past the 30 s floor but inside the 2 minute default.
now = start + 90_000;
await request('/api/flights', '?lat=30&lon=-97');
assert.equal(states, 1, 'a missing header is not read as a zero wait');
});
test('OpenSky does not retry a snapshot body over the cap', async (t) => {
environment(t, openSkyAnonEnv);
t.mock.method(console, 'warn', () => {});
t.mock.method(console, 'error', () => {});
const bodies = [];
t.mock.method(globalThis, 'fetch', async (url) => {
if (url.includes('/states/')) {
const oversized = oversizedResponse(' ');
bodies.push(oversized.state);
return oversized.response;
}
if (url.includes('/lat/')) return regionalFallback();
throw Error(`Unexpected URL: ${url}`);
});
const fresh = await import(
`../../server/providers/aircraft/opensky.js?oversize=${Math.random()}`
);
const response = await install(fresh.openSkyProxy())(
'/api/flights',
'?lat=30&lon=-97',
);
assert.equal(response.statusCode, 200);
assert.equal(response.headers['x-flight-source'], 'adsb.lol');
assert.equal(bodies.length, 1, 'an oversized body is not fetched twice');
assert.equal(bodies[0].cancelled, true);
assert.ok(bodies[0].pulled <= 9, 'reading stops at the 8 MiB cap');
});
test('military aircraft route answers 502 to an oversized body without caching it', async (t) => {
t.mock.method(console, 'error', () => {});
const bodies = [];
t.mock.method(globalThis, 'fetch', async () => {
const oversized = oversizedResponse('{"ac":[]}');
bodies.push(oversized.state);
return oversized.response;
});
const request = install(providers.adsbLolProxy());
const first = await request('/api/military');
assert.equal(first.statusCode, 502);
assert.equal(bodies[0].cancelled, true);
assert.ok(bodies[0].pulled <= 9, 'reading stops at the 8 MiB cap');
const again = await request('/api/military');
assert.equal(again.statusCode, 502, 'nothing was cached');
assert.equal(bodies.length, 2);
});
test('military aircraft route preserves fresh cache and stale response after upstream failure', async (t) => {
let now = Date.now();
let calls = 0;
@@ -203,11 +351,11 @@ test('military aircraft route preserves fresh cache and stale response after ups
return Response.json({ ac: [{ hex: 'abc123' }] });
});
const request = install(providers.adsbLolProxy());
const first = await request('/api/adsblol/mil');
assert.equal((await request('/api/adsblol/mil')).body, first.body);
const first = await request('/api/military');
assert.equal((await request('/api/military')).body, first.body);
assert.equal(calls, 1);
now += 13_000;
assert.equal((await request('/api/adsblol/mil')).body, first.body);
assert.equal((await request('/api/military')).body, first.body);
assert.equal(calls, 2);
});
@@ -261,7 +409,7 @@ test('AIS preview route ingests through the socket, returns tracks and disposes
const request = install(plugin, true);
let res;
for (let i = 0; i < 100; i++) {
res = await request('/api/ais-live');
res = await request('/api/vessels');
if (JSON.parse(res.body).rows.length) break;
await delay(10);
}
@@ -269,10 +417,10 @@ test('AIS preview route ingests through the socket, returns tracks and disposes
assert.equal(data.status, 'live');
assert.equal(data.rows[0].mmsi, '123456789');
assert.equal(data.rows[0].heading, null);
const history = await request('/api/ais-live', '/track?mmsi=123456789');
const history = await request('/api/vessels', '/track?mmsi=123456789');
assert.equal(JSON.parse(history.body).samples.length, 2);
assert.equal(
(await request('/api/ais-live', '/track?mmsi=bad')).statusCode,
(await request('/api/vessels', '/track?mmsi=bad')).statusCode,
400,
);
assert.equal(sockets.length, 1);
@@ -280,7 +428,7 @@ test('AIS preview route ingests through the socket, returns tracks and disposes
for (let i = 0; i < 100 && sockets[0].readyState !== 3; i++) await delay(10);
assert.equal(sockets[0].readyState, 3);
const restarted = install(plugin);
await restarted('/api/ais-live');
await restarted('/api/vessels');
for (let i = 0; i < 100 && sockets.length < 2; i++) await delay(10);
assert.equal(sockets.length, 2);
});
@@ -299,10 +447,10 @@ test('military aircraft route serves stale cache on an upstream 429 and cools do
});
});
const request = install(providers.adsbLolProxy());
const first = await request('/api/adsblol/mil');
const first = await request('/api/military');
assert.equal(first.statusCode, 200);
now += 13_000;
const limited = await request('/api/adsblol/mil');
const limited = await request('/api/military');
assert.equal(calls, 2);
assert.equal(
limited.statusCode,
@@ -310,16 +458,16 @@ test('military aircraft route serves stale cache on an upstream 429 and cools do
'a 429 with a cached body is never relayed',
);
assert.equal(limited.body, first.body);
assert.equal(limited.headers['x-ads-b-cache'], 'STALE');
assert.equal(limited.headers['x-feed-cache'], 'STALE');
assert.equal(limited.headers['x-ads-b-upstream-status'], '429');
assert.equal(limited.headers['x-ads-b-cache-age-ms'], '13000');
assert.equal(limited.headers['x-feed-age-ms'], '13000');
now += 5_000;
const cooling = await request('/api/adsblol/mil');
const cooling = await request('/api/military');
assert.equal(calls, 2, 'no upstream call inside the Retry-After window');
assert.equal(cooling.headers['x-ads-b-cache'], 'STALE');
assert.equal(cooling.headers['x-ads-b-cache-age-ms'], '18000');
assert.equal(cooling.headers['x-feed-cache'], 'STALE');
assert.equal(cooling.headers['x-feed-age-ms'], '18000');
now += 16_000;
await request('/api/adsblol/mil');
await request('/api/military');
assert.equal(calls, 3, 'upstream is retried once Retry-After elapses');
});
@@ -331,10 +479,10 @@ test('military aircraft route relays an upstream 429 when nothing is cached', as
return new Response('{"error":"rate limited"}', { status: 429 });
});
const request = install(providers.adsbLolProxy());
const limited = await request('/api/adsblol/mil');
const limited = await request('/api/military');
assert.equal(limited.statusCode, 429);
assert.ok(limited.headers['retry-after']);
const again = await request('/api/adsblol/mil');
const again = await request('/api/military');
assert.equal(again.statusCode, 429);
assert.equal(
calls,
@@ -369,21 +517,21 @@ test('military fallback cancels a stalled 5xx body and starts cooldown at receip
);
});
const request = install(providers.adsbLolProxy());
await request('/api/adsblol/mil');
await request('/api/military');
now += 1000;
const hit = await request('/api/adsblol/mil');
assert.equal(hit.headers['x-ads-b-cache-age-ms'], '1000');
const hit = await request('/api/military');
assert.equal(hit.headers['x-feed-age-ms'], '1000');
now += 12000;
const fallback = await request('/api/adsblol/mil');
const fallback = await request('/api/military');
assert.equal(fallback.statusCode, 200);
assert.equal(cancelled, true);
assert.equal(fallback.headers['x-ads-b-cache-age-ms'], '23000');
assert.equal(fallback.headers['x-feed-age-ms'], '23000');
assert.equal(fallback.headers['x-ads-b-retry-after-seconds'], '20');
now += 19000;
await request('/api/adsblol/mil');
await request('/api/military');
assert.equal(calls, 2);
now += 2000;
await request('/api/adsblol/mil');
await request('/api/military');
assert.equal(calls, 3);
});
@@ -403,7 +551,7 @@ test('military cooldown bounds untrusted Retry-After and defaults server errors'
headers: { 'Retry-After': raw },
}),
);
const result = await install(providers.adsbLolProxy())('/api/adsblol/mil');
const result = await install(providers.adsbLolProxy())('/api/military');
assert.equal(Number(result.headers['retry-after']), seconds);
}
});
@@ -423,7 +571,7 @@ test('track backfill proxy returns 502 on an oversized upstream body and caches
};
});
const res1 = await tracks('/api/opensky-track', '?icao24=def456');
const res1 = await tracks('/api/flights/track', '?icao24=def456');
assert.equal(res1.statusCode, 502);
assert.deepEqual(JSON.parse(res1.body), {
error: 'Upstream track response too large',
@@ -431,7 +579,209 @@ test('track backfill proxy returns 502 on an oversized upstream body and caches
// Cached as a 502 (never a 200): a retry inside the window neither
// reads as an empty track nor spends OpenSky credits on another download.
const res2 = await tracks('/api/opensky-track', '?icao24=def456');
const res2 = await tracks('/api/flights/track', '?icao24=def456');
assert.equal(res2.statusCode, 502);
assert.equal(callCount, 1);
});
test('OpenSky Basic mode no longer sends username and password: it asks anonymously and warns once', async (t) => {
environment(t, {
OPENSKY_CLIENT_ID: undefined,
OPENSKY_CLIENT_SECRET: undefined,
OPENSKY_CREDENTIALS_FILE: undefined,
OPENSKY_AUTH_MODE: 'basic',
OPENSKY_USERNAME: 'fixture-user',
OPENSKY_PASSWORD: 'fixture-password',
});
const warnings = [];
t.mock.method(console, 'warn', (message) => warnings.push(message));
const authorizations = [];
t.mock.method(globalThis, 'fetch', async (url, options) => {
if (url.includes('/states/')) {
authorizations.push(options.headers.Authorization);
return Response.json({ time: Math.floor(Date.now() / 1000), states: [] });
}
throw Error(`Unexpected URL: ${url}`);
});
const fresh = await import(
`../../server/providers/aircraft/opensky.js?basic=${Math.random()}`
);
const flights = install(fresh.openSkyProxy());
const first = await flights('/api/flights');
assert.equal(first.statusCode, 200);
assert.equal(first.headers['x-opensky-auth'], 'anon');
assert.equal(first.headers['x-opensky-auth-reason'], 'basic_retired');
assert.deepEqual(authorizations, [undefined]);
assert.equal(warnings.length, 1);
assert.match(warnings[0], /retired username\/password sign-in/);
assert.ok(!warnings[0].includes('fixture-password'));
});
test('OpenSky auto mode never falls back to Basic, even when OAuth is rejected', async (t) => {
environment(t, {
OPENSKY_CLIENT_ID: 'fixture-client',
OPENSKY_CLIENT_SECRET: 'fixture-secret',
OPENSKY_CREDENTIALS_FILE: undefined,
OPENSKY_AUTH_MODE: 'auto',
OPENSKY_USERNAME: 'fixture-user',
OPENSKY_PASSWORD: 'fixture-password',
});
t.mock.method(console, 'warn', () => {});
t.mock.method(console, 'log', () => {});
t.mock.method(console, 'error', () => {});
const authorizations = [];
t.mock.method(globalThis, 'fetch', async (url, options) => {
if (url.includes('/token'))
return Response.json({ access_token: 'fixture-token', expires_in: 1800 });
if (url.includes('/states/')) {
authorizations.push(options.headers.Authorization);
return new Response('', { status: 401 });
}
if (url.includes('/lat/')) return regionalFallback();
throw Error(`Unexpected URL: ${url}`);
});
const fresh = await import(
`../../server/providers/aircraft/opensky.js?auto=${Math.random()}`
);
await install(fresh.openSkyProxy())('/api/flights', '?lat=30&lon=-97');
assert.deepEqual(authorizations, ['Bearer fixture-token']);
});
test('retired military history refuses without upstream I/O while validating identifiers', async (t) => {
t.mock.method(globalThis, 'fetch', async () => {
assert.fail('retired history must not contact the upstream');
});
const route = install(providers.trackBackfillProxies());
for (const hex of ['abc123', 'abc456']) {
const response = await route('/api/military/track', '?hex=' + hex);
assert.equal(response.statusCode, 503);
assert.equal(JSON.parse(response.body).code, 'history_unavailable');
assert.equal(response.headers['retry-after'], '1800');
assert.equal(response.headers['cache-control'], 'no-store');
}
assert.equal(
(await route('/api/military/track', '?hex=invalid')).statusCode,
400,
);
});
test('global aircraft snapshots coalesce cold and expired callers and clear rejected flights', async (t) => {
environment(t, { OPENSKY_AUTH_MODE: 'anon' });
t.mock.method(console, 'warn', () => {});
t.mock.method(console, 'error', () => {});
let now = Date.now();
t.mock.method(Date, 'now', () => now);
for (const kind of ['opensky', 'military']) {
const module =
kind === 'opensky'
? await import(
`../../server/providers/aircraft/opensky.js?coalesce=${Math.random()}`
)
: await import('../../server/providers/aircraft/adsb-lol.js');
const request = install(
kind === 'opensky' ? module.openSkyProxy() : module.adsbLolProxy(),
);
const route = kind === 'opensky' ? '/api/flights' : '/api/military';
let calls = 0;
let release;
let blocked;
let fail = false;
t.mock.method(globalThis, 'fetch', async () => {
calls++;
await blocked;
if (fail) throw new Error('fixture connection failed');
return Response.json(
{ time: now / 1000, states: [], ac: [] },
{ headers: { 'x-rate-limit-remaining': '5000' } },
);
});
for (const phase of ['cold', 'expired', 'failed', 'recovered']) {
now += 13000;
fail = phase === 'failed';
blocked = new Promise((resolve) => {
release = resolve;
});
const before = calls;
const pending = Array.from({ length: 5 }, () => request(route));
await new Promise(setImmediate);
assert.equal(calls - before, 1, `${kind} ${phase}: one refresh starts`);
release();
const responses = await Promise.all(pending);
assert.equal(calls - before, fail && kind === 'opensky' ? 2 : 1);
assert.ok(responses.every((res) => res.statusCode === 200));
assert.ok(responses.every((res) => res.body === responses[0].body));
}
}
});
test('OpenSky 5xx retains dated last-good data, cancels error bodies and cools down before recovery', async (t) => {
environment(t, { OPENSKY_AUTH_MODE: 'anon' });
let now = Date.now();
t.mock.method(Date, 'now', () => now);
for (const status of [502, 503, 504]) {
const module = await import(
`../../server/providers/aircraft/opensky.js?server-error=${status}`
);
const request = install(module.openSkyProxy());
let fail = false;
let calls = 0;
let cancelled = false;
t.mock.method(globalThis, 'fetch', async () => {
calls++;
if (fail)
return new Response(
new ReadableStream({
cancel() {
cancelled = true;
},
}),
{ status },
);
return Response.json(
{ time: now / 1000, states: [] },
{ headers: { 'x-rate-limit-remaining': '5000' } },
);
});
const good = await request('/api/flights');
now += 10000;
fail = true;
const stale = await request('/api/flights');
assert.equal(stale.statusCode, 200);
assert.equal(stale.body, good.body);
assert.equal(stale.headers['x-opensky-cache'], 'STALE');
assert.equal(
stale.headers['x-opensky-auth-reason'],
`upstream_http_${status}_serving_stale`,
);
assert.equal(stale.headers['x-opensky-stale-seconds'], '10');
assert.equal(cancelled, true);
const cooling = await request('/api/flights');
assert.equal(cooling.body, good.body);
assert.equal(
cooling.headers['x-opensky-auth-reason'],
`upstream_http_${status}_serving_stale`,
);
assert.equal(calls, 2);
now += 16000;
fail = false;
assert.equal(
(await request('/api/flights')).headers['x-opensky-cache'],
'MISS',
);
assert.equal(calls, 3);
}
const coldModule =
await import('../../server/providers/aircraft/opensky.js?cold-server-error');
let calls = 0;
t.mock.method(globalThis, 'fetch', async () => {
calls++;
return new Response('<html>failure</html>', { status: 503 });
});
const cold = install(coldModule.openSkyProxy());
for (let i = 0; i < 2; i++) {
const response = await cold('/api/flights');
assert.equal(response.statusCode, 503);
assert.match(JSON.parse(response.body).error, /OpenSky/);
}
assert.equal(calls, 1);
});
+232
View File
@@ -0,0 +1,232 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { promises as fsp } from 'node:fs';
import http from 'node:http';
import { launchLibraryRecentUrl } from '../data/spaceProviderRequests.js';
import {
launchLibraryRequestHeaders,
rocketLaunchesProxy,
} from '../../server/providers/space/launch-library.js';
import { keySetupStatus } from '../keySetupCore.mjs';
import { initKeySetup } from '../keySetup.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');
const last = new Date('2026-10-17T16:20:59.999Z');
assert.equal(
launchLibraryRecentUrl(first).href,
launchLibraryRecentUrl(last).href,
);
assert.equal(first.toISOString(), '2026-10-17T16:20:00.001Z');
assert.equal(last.toISOString(), '2026-10-17T16:20:59.999Z');
});
for (const [date, end] of [
['2026-10-17T16:20:00.001Z', '2026-10-17T16:20:00.000Z'],
['2026-10-17T16:20:59.999Z', '2026-10-17T16:20:00.000Z'],
['2026-10-17T16:21:00.000Z', '2026-10-17T16:21:00.000Z'],
['2026-01-01T00:00:00.001Z', '2026-01-01T00:00:00.000Z'],
]) {
test(`LL2 bounds retain the 30-day UTC window at ${date}`, () => {
const url = launchLibraryRecentUrl(new Date(date));
assert.equal(url.origin, 'https://ll.thespacedevs.com');
assert.equal(url.pathname, '/2.3.0/launches/');
assert.equal(url.searchParams.get('net__lte'), end);
assert.equal(
Date.parse(end) - Date.parse(url.searchParams.get('net__gte')),
30 * 86400000,
);
assert.equal(url.searchParams.get('limit'), '100');
assert.equal(url.searchParams.get('mode'), 'detailed');
});
}
test('LL2 headers identify the app and preserve optional server-only authorization', () => {
const publicHeaders = launchLibraryRequestHeaders('');
assert.match(publicHeaders['User-Agent'], /^gods-eye-view.*\/1\.0 /);
assert.ok(
publicHeaders['User-Agent'].includes(
'https://github.com/bilawalsidhu/gods-eye-view',
),
);
assert.equal(publicHeaders.Accept, 'application/json');
assert.equal(publicHeaders.Authorization, undefined);
const paidHeaders = launchLibraryRequestHeaders(' owned-fixture-token ');
assert.equal(paidHeaders.Authorization, 'Token owned-fixture-token');
assert.equal(paidHeaders['User-Agent'], publicHeaders['User-Agent']);
});
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 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,
addEventListener() {},
removeEventListener() {},
},
fetchImpl: async () => ({ ok: true, json: async () => status }),
});
assert.ok(setup);
try {
const ll2 = rows.children.find(
(row) => row.dataset.keyId === 'launch-library',
);
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 {
setup.destroy();
}
});
test('real LL2 proxy sends cacheable headers and bounds, retaining its memory cache', async (t) => {
for (const method of ['readFile', 'stat'])
t.mock.method(fsp, method, async () => {
throw new Error('owned fixture has no disk cache');
});
t.mock.method(fsp, 'mkdir', async () => {});
t.mock.method(fsp, 'writeFile', async () => {});
const calls = [];
t.mock.method(globalThis, 'fetch', async (url, options) => {
calls.push({ url: new URL(url), options });
return new Response(
JSON.stringify({ results: [{ id: 'owned-launch-fixture' }] }),
);
});
let handler;
rocketLaunchesProxy().configureServer({
middlewares: {
use(route, value) {
assert.equal(route, '/api/launches');
handler = value;
},
},
});
async function request() {
const res = {
writeHead(status, headers) {
this.status = status;
this.headers = headers;
},
end(body) {
this.body = body;
},
};
await handler({ method: 'GET' }, res);
return res;
}
const first = await request();
const second = await request();
assert.equal(first.status, 200);
assert.equal(second.body, first.body);
assert.equal(second.headers['X-GEV-Cache'], 'HIT');
assert.equal(calls.length, 1);
assert.match(calls[0].options.headers['User-Agent'], /gods-eye-view/);
assert.match(calls[0].url.searchParams.get('net__lte'), /:00\.000Z$/);
assert.match(calls[0].url.searchParams.get('net__gte'), /:00\.000Z$/);
});
for (const preview of [false, true]) {
test(`LL2 proxy sends its identifying headers over real owned HTTP in ${preview ? 'preview' : 'development'}`, async (t) => {
for (const method of ['readFile', 'stat'])
t.mock.method(fsp, method, async () => {
throw new Error('no owned cache');
});
t.mock.method(fsp, 'mkdir', async () => {});
t.mock.method(fsp, 'writeFile', async () => {});
const received = [];
const upstream = http.createServer((req, res) => {
received.push({
url: new URL(req.url, 'http://fixture.invalid'),
headers: req.headers,
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ results: [{ id: 'owned-wire-fixture' }] }));
});
await new Promise((resolve) => upstream.listen(0, '127.0.0.1', resolve));
const originalFetch = globalThis.fetch;
t.mock.method(globalThis, 'fetch', (url, options) => {
const parsed = new URL(url);
assert.equal(parsed.origin, 'https://ll.thespacedevs.com');
return originalFetch(
`http://127.0.0.1:${upstream.address().port}${parsed.pathname}${parsed.search}`,
options,
);
});
let handler;
const plugin = rocketLaunchesProxy();
plugin[preview ? 'configurePreviewServer' : 'configureServer']({
middlewares: {
use(_route, value) {
handler = value;
},
},
});
const proxy = http.createServer((req, res) => {
void handler(req, res);
});
await new Promise((resolve) => proxy.listen(0, '127.0.0.1', resolve));
try {
const result = await originalFetch(
`http://127.0.0.1:${proxy.address().port}/api/launches`,
);
assert.equal(result.status, 200);
assert.deepEqual(await result.json(), {
results: [{ id: 'owned-wire-fixture' }],
});
assert.equal(received.length, 1);
assert.match(
received[0].headers['user-agent'],
/^gods-eye-view-launch-library-proxy\/1\.0 /,
);
assert.equal(received[0].headers.authorization, undefined);
assert.match(received[0].url.searchParams.get('net__lte'), /:00\.000Z$/);
} finally {
proxy.closeAllConnections();
upstream.closeAllConnections();
await Promise.all([
new Promise((resolve) => proxy.close(resolve)),
new Promise((resolve) => upstream.close(resolve)),
]);
}
});
}
+672 -1
View File
@@ -21,6 +21,9 @@ import { openAiRealtimeProxy } from 'gods-eye-view/server/providers/openai';
import { keySetupEndpoint } from 'gods-eye-view/server/standalone/key-setup';
import { realtimeInstructions } from '../../server/providers/openai/instructions.js';
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
import { createDebugLogHandler } from '../../server/providers/openai/debug-log.js';
import { attachVoiceResult } from '../voice/speech.js';
import { sanitizeDebugValue } from '../voice/realtimeDiagnostics.js';
import { standaloneVoiceTools } from '../../server/standalone/voiceTools.js';
function install(plugin, preview = false) {
@@ -38,6 +41,7 @@ function request(
url = '/',
body = '',
origin = 'http://localhost:4173',
remoteAddress = '127.0.0.1',
} = {},
) {
return new Promise((resolve, reject) => {
@@ -50,7 +54,7 @@ function request(
origin,
'content-type': 'application/json',
},
socket: { remoteAddress: '127.0.0.1' },
socket: { remoteAddress },
});
const headers = {};
const res = {
@@ -196,6 +200,410 @@ test('Realtime handler preserves tools and default instructions, isolates suppli
assert.equal(sent[0].session.instructions, sent[2].session.instructions);
});
test('Realtime handler keeps API key as default and uses OAuth only when requested', async (t) => {
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
let oauthReads = 0;
const authorizations = [];
const routes = install(
openAiRealtimeProxy({
realtime: {
resolveApiKey: () => 'fixture-api-key',
resolveOAuthAccessToken: () => {
oauthReads += 1;
return 'fixture-oauth-token';
},
fetchImpl: async (_url, options) => {
authorizations.push(options.headers.Authorization);
return Response.json({ value: 'fixture-ephemeral' });
},
},
}),
);
const token = routes.get('/api/realtime/token');
const apiResponse = await request(token);
assert.equal(apiResponse.status, 200);
assert.equal(apiResponse.headers['x-gev-voice-auth'], 'env');
assert.equal(oauthReads, 0, 'default cloud voice does not read OAuth');
assert.equal(authorizations.at(-1), 'Bearer fixture-api-key');
const oauthResponse = await request(token, { url: '/?auth=oauth' });
assert.equal(oauthResponse.status, 200);
assert.equal(oauthResponse.headers['x-gev-voice-auth'], 'codex-oauth');
assert.equal(oauthReads, 1);
assert.equal(authorizations.at(-1), 'Bearer fixture-oauth-token');
});
test('a Codex OAuth pin refuses API-key minting without silently switching modes', async (t) => {
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
const authorizations = [];
const routes = install(
openAiRealtimeProxy({
realtime: {
environment: { GEV_PREFER_CODEX_OAUTH: 'true' },
resolveApiKey: () => assert.fail('pinned OAuth cannot read an API key'),
resolveOAuthAccessToken: () => 'fixture-oauth-token',
fetchImpl: async (_url, options) => {
authorizations.push(options.headers.Authorization);
return Response.json({ value: 'fixture-ephemeral' });
},
},
}),
);
const token = routes.get('/api/realtime/token');
const refused = await request(token);
assert.equal(refused.status, 503);
assert.equal(refused.json().code, 'CODEX_OAUTH_REQUIRED');
assert.equal(authorizations.length, 0);
assert.equal((await request(token, { url: '/?auth=oauth' })).status, 200);
assert.deepEqual(authorizations, ['Bearer fixture-oauth-token']);
});
test('OAuth status and token minting never expose local OAuth to remote clients', async () => {
let oauthReads = 0;
const routes = install(
openAiRealtimeProxy({
realtime: {
resolveOAuthAccessToken: () => {
oauthReads += 1;
return 'must-not-be-read-remotely';
},
fetchImpl: () =>
assert.fail('remote OAuth request must not reach OpenAI'),
},
}),
);
const remote = { remoteAddress: '192.168.1.20' };
assert.equal(
(await request(routes.get('/api/realtime/oauth-status'), remote)).status,
403,
);
assert.equal(
(
await request(routes.get('/api/realtime/token'), {
...remote,
url: '/?auth=oauth',
})
).status,
403,
);
assert.equal(oauthReads, 0);
});
test('OAuth status reports local sign-in availability without returning the token', async () => {
const routes = install(
openAiRealtimeProxy({
realtime: { resolveOAuthAccessToken: () => 'fixture-oauth-secret' },
}),
);
const response = await request(routes.get('/api/realtime/oauth-status'));
assert.equal(response.status, 200);
assert.deepEqual(response.json(), { available: true });
assert.equal(response.body.includes('fixture-oauth-secret'), false);
});
test('OAuth status and token minting report expired sign-in as re-auth required', async () => {
const expired = () => {
const error = new Error('ChatGPT sign-in has expired.');
error.code = 'CODEX_OAUTH_REAUTH_REQUIRED';
throw error;
};
const routes = install(
openAiRealtimeProxy({
realtime: {
resolveOAuthAccessToken: expired,
fetchImpl: () => assert.fail('expired OAuth must not reach OpenAI'),
},
}),
);
const status = await request(routes.get('/api/realtime/oauth-status'));
assert.equal(status.status, 200);
assert.deepEqual(status.json(), {
available: false,
error: 'ChatGPT sign-in has expired.',
code: 'CODEX_OAUTH_REAUTH_REQUIRED',
reauthRequired: true,
});
const token = await request(routes.get('/api/realtime/token'), {
url: '/?auth=oauth',
});
assert.equal(token.status, 503);
assert.equal(token.json().code, 'CODEX_OAUTH_REAUTH_REQUIRED');
});
test('OAuth login endpoint shares pending attempts and never exposes credentials', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
let starts = 0;
let spawnReady;
let exit;
const plugin = openAiRealtimeProxy({
realtime: {
resolveOAuthAccessToken: () => {
throw new Error('not signed in');
},
startOAuthLogin: async () => {
starts += 1;
await new Promise((resolve) => {
spawnReady = resolve;
});
return {
completion: new Promise((resolve) => {
exit = resolve;
}),
cancel() {},
};
},
},
});
t.after(plugin.closeBundle);
const routes = install(plugin);
const handler = routes.get('/api/realtime/oauth-login');
const firstRequest = request(handler, { method: 'POST' });
const concurrent = await request(handler, { method: 'POST' });
assert.equal(concurrent.status, 202);
assert.equal(concurrent.json().started, false);
assert.equal(starts, 1, 'reserve the attempt before awaiting spawn');
spawnReady();
const started = await firstRequest;
assert.equal(started.status, 202);
assert.deepEqual(started.json(), {
available: false,
started: true,
pending: true,
});
assert.equal(starts, 1);
t.mock.timers.tick(31_000);
const duplicate = await request(handler, { method: 'POST' });
assert.equal(duplicate.status, 202);
assert.equal(duplicate.json().pending, true);
assert.equal(starts, 1, 'rapid retries must not spawn another login');
const remote = await request(handler, {
method: 'POST',
remoteAddress: '192.168.1.20',
});
assert.equal(remote.status, 403);
assert.equal(starts, 1);
const crossOrigin = await request(handler, {
method: 'POST',
origin: 'https://example.com',
});
assert.equal(crossOrigin.status, 403);
assert.equal(starts, 1);
exit({ exitCode: 1 });
await Promise.resolve();
const failed = await request(routes.get('/api/realtime/oauth-status'));
assert.equal(failed.json().loginFailed, true);
assert.equal(failed.json().code, 'CODEX_OAUTH_LOGIN_FAILED');
});
test('OAuth status follows process completion, permits retry, and checks credential storage', async (t) => {
let token = null;
let exit;
let starts = 0;
const plugin = openAiRealtimeProxy({
realtime: {
resolveOAuthAccessToken: () => token,
startOAuthLogin: async () => {
starts += 1;
return {
completion: new Promise((resolve) => {
exit = resolve;
}),
cancel() {},
};
},
},
});
t.after(plugin.closeBundle);
const routes = install(plugin);
const launch = () =>
request(routes.get('/api/realtime/oauth-login'), { method: 'POST' });
const status = () => request(routes.get('/api/realtime/oauth-status'));
await launch();
exit({ exitCode: 0 });
await Promise.resolve();
assert.equal(
(await status()).json().code,
'CODEX_OAUTH_STORAGE_UNAVAILABLE',
'exit success alone is not usable sign-in',
);
await launch();
assert.equal(starts, 2, 'failed attempts can be retried immediately');
token = 'fixture-private-token';
exit({ exitCode: 0 });
await Promise.resolve();
const ready = await status();
assert.deepEqual(ready.json(), { available: true });
assert.equal(ready.body.includes(token), false);
});
test('OAuth login timeout and server shutdown cancel the owned process', async () => {
let timeout;
let cancelled = 0;
let cleared = 0;
const plugin = openAiRealtimeProxy({
realtime: {
resolveOAuthAccessToken: () => null,
startOAuthLogin: async () => ({
completion: new Promise(() => {}),
cancel() {
cancelled += 1;
},
}),
setTimer(callback, duration) {
timeout = callback;
assert.equal(duration, 120_000);
return 1;
},
clearTimer() {
cleared += 1;
},
},
});
const routes = install(plugin);
const launch = () =>
request(routes.get('/api/realtime/oauth-login'), { method: 'POST' });
await launch();
timeout();
assert.equal(cancelled, 1);
const status = await request(routes.get('/api/realtime/oauth-status'));
assert.equal(status.json().code, 'CODEX_OAUTH_LOGIN_TIMEOUT');
await launch();
plugin.closeBundle();
plugin.closeBundle();
assert.equal(cancelled, 2, 'shutdown is idempotent');
assert.equal(cleared, 2);
});
test('OAuth launch errors are safe and cross-origin status requests cannot read credentials', async () => {
let reads = 0;
const routes = install(
openAiRealtimeProxy({
realtime: {
resolveOAuthAccessToken() {
reads += 1;
return null;
},
startOAuthLogin: async () => {
throw new Error('private-token-fixture');
},
},
}),
);
const remote = await request(routes.get('/api/realtime/oauth-status'), {
origin: 'https://example.com',
});
assert.equal(remote.status, 403);
assert.equal(reads, 0);
const failed = await request(routes.get('/api/realtime/oauth-login'), {
method: 'POST',
});
assert.equal(failed.status, 503);
assert.equal(failed.json().code, 'CODEX_OAUTH_LOGIN_FAILED');
assert.equal(failed.body.includes('private-token-fixture'), false);
});
test('a login that spawns after timeout is cancelled and cannot replace a retry', async (t) => {
let timeout;
let releaseSpawn;
let cancelled = 0;
let launches = 0;
const plugin = openAiRealtimeProxy({
realtime: {
resolveOAuthAccessToken: () => null,
startOAuthLogin: async () => {
launches += 1;
if (launches === 1)
await new Promise((resolve) => {
releaseSpawn = resolve;
});
return {
completion: new Promise(() => {}),
cancel() {
cancelled += 1;
},
};
},
setTimer(callback) {
timeout = callback;
return 1;
},
clearTimer() {},
},
});
t.after(plugin.closeBundle);
const routes = install(plugin);
const launch = () =>
request(routes.get('/api/realtime/oauth-login'), { method: 'POST' });
const first = launch();
timeout();
await launch();
releaseSpawn();
await first;
assert.equal(launches, 2);
assert.equal(cancelled, 1, 'late process must not become an orphan');
const status = (
await request(routes.get('/api/realtime/oauth-status'))
).json();
assert.equal(status.pending, true);
assert.equal(
status.loginFailed,
undefined,
'the older attempt cannot fail the retry',
);
});
test('OAuth login endpoint selects an existing local sign-in without launching Codex', async () => {
const handler = install(
openAiRealtimeProxy({
realtime: {
resolveOAuthAccessToken: () => 'fixture-existing-secret',
startOAuthLogin: () =>
assert.fail('existing auth must not launch login'),
},
}),
).get('/api/realtime/oauth-login');
const response = await request(handler, { method: 'POST' });
assert.equal(response.status, 200);
assert.deepEqual(response.json(), {
available: true,
started: false,
pending: false,
});
assert.equal(response.body.includes('fixture-existing-secret'), false);
});
test('caption transcription is on by default, reported to the client meter, and can be turned off', async (t) => {
env(t, 'OPENAI_API_KEY', 'fixture-upstream-secret');
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
const sent = [];
t.mock.method(globalThis, 'fetch', async (url, options) => {
sent.push(JSON.parse(options.body));
return Response.json({ value: 'fixture-ephemeral' });
});
for (const [setting, expected] of [
[undefined, 'gpt-4o-mini-transcribe'],
['off', 'off'],
]) {
env(t, 'OPENAI_REALTIME_TRANSCRIBE_MODEL', setting);
const response = await request(
install(openAiRealtimeProxy()).get('/api/realtime/token'),
);
assert.equal(response.headers['x-gev-voice-transcribe-model'], expected);
assert.equal(
sent.at(-1).session.audio.input.transcription?.model ?? 'off',
expected,
);
}
});
test('Realtime sessions carry supplied tools, and the standalone voice adds the catalog queries', async (t) => {
env(t, 'OPENAI_API_KEY', 'fixture-upstream-secret');
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
@@ -374,6 +782,269 @@ test('OpenAI routes answer generically when the upstream or the request fails',
assert.equal(token.body.includes('fixture-upstream-secret'), false);
});
test('the debug-log sink omits what people said and tool arguments unless content logging is enabled', async (t) => {
const nestObject = (value, levels) => {
let nested = value;
for (let index = 0; index < levels; index += 1) nested = { nested };
return nested;
};
const nestArray = (value, levels) => {
let nested = value;
for (let index = 0; index < levels; index += 1) nested = [nested];
return nested;
};
const records = [
{
timestamp: '2026-10-05T00:00:00.000Z',
sessionId: 'gev-session-1',
event: 'tool.call',
status: 'active',
payload: {
name: 'analyst_query',
callId: 'call-1',
arguments: { location: '12 Elm Street', note: 'meet Alice there' },
},
},
{
timestamp: '2026-10-05T00:00:01.000Z',
sessionId: 'gev-session-1',
event: 'server.event',
status: 'active',
payload: {
type: 'response.function_call_arguments.done',
eventId: 'event-1',
responseId: 'response-1',
payload: {
type: 'response.function_call_arguments.done',
event_id: 'event-1',
response_id: 'response-1',
item_id: 'item-1',
output_index: 0,
call_id: 'call-1',
name: 'analyst_query',
arguments: '{"query":"Tokyo Station"}',
},
},
},
{
event: 'server.event',
status: 'active',
payload: {
type: 'response.audio_transcript.done',
transcript: 'my home address is 12 Elm Street',
text: 'call my sister',
delta: 'Flying to Tokyo.',
usage: { input_tokens: 40 },
},
},
{
event: 'voice.cost',
status: 'active',
payload: { delta: 17, total: 41 },
},
{
event: 'tool.call',
status: 'active',
payload: { name: 'adjust_camera_zoom', callId: 'call-2', arguments: 2 },
},
{
event: 'deep.object',
status: 'active',
payload: nestObject(
{
transcript: 'deep-object-transcript-secret',
arguments: { query: 'deep-object-argument-secret' },
},
14,
),
},
{
event: 'deep.array',
status: 'active',
payload: nestArray(
{
transcript: 'deep-array-transcript-secret',
arguments: { query: 'deep-array-argument-secret' },
},
14,
),
},
];
for (const includeContent of [false, true]) {
const sourceRoot = root(t);
const handler = createDebugLogHandler({ sourceRoot, includeContent });
for (const record of records) {
const response = await request(handler, {
method: 'POST',
body: JSON.stringify(record),
});
assert.equal(response.status, 204);
}
const logged = readFileSync(
path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl'),
'utf8',
);
assert.equal(logged.includes('Elm Street'), includeContent);
assert.equal(logged.includes('call my sister'), includeContent);
assert.equal(logged.includes('meet Alice there'), includeContent);
assert.equal(logged.includes('Tokyo Station'), includeContent);
assert.equal(logged.includes('Flying to Tokyo'), includeContent);
assert.equal(
logged.includes('deep-object-transcript-secret'),
includeContent,
);
assert.equal(
logged.includes('deep-object-argument-secret'),
includeContent,
);
assert.equal(
logged.includes('deep-array-transcript-secret'),
includeContent,
);
assert.equal(logged.includes('deep-array-argument-secret'), includeContent);
const [
toolCall,
serverEvent,
transcriptEvent,
numericDelta,
numericArguments,
deepObject,
deepArray,
] = logged
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line));
assert.equal(toolCall.timestamp, records[0].timestamp);
assert.equal(toolCall.sessionId, 'gev-session-1');
assert.equal(toolCall.event, 'tool.call');
assert.equal(toolCall.status, 'active');
assert.equal(toolCall.payload.name, 'analyst_query');
assert.equal(toolCall.payload.callId, 'call-1');
assert.equal(serverEvent.payload.eventId, 'event-1');
assert.equal(serverEvent.payload.responseId, 'response-1');
assert.equal(serverEvent.payload.payload.event_id, 'event-1');
assert.equal(serverEvent.payload.payload.response_id, 'response-1');
assert.equal(serverEvent.payload.payload.item_id, 'item-1');
assert.equal(serverEvent.payload.payload.output_index, 0);
assert.equal(serverEvent.payload.payload.call_id, 'call-1');
assert.equal(serverEvent.payload.payload.name, 'analyst_query');
assert.equal(transcriptEvent.payload.usage.input_tokens, 40);
assert.equal(numericDelta.payload.delta, 17);
assert.equal(numericDelta.payload.total, 41);
if (!includeContent) {
assert.equal(toolCall.payload.arguments, '[omitted]');
assert.match(
serverEvent.payload.payload.arguments,
/^\[omitted \d+ chars\]$/,
);
assert.equal(transcriptEvent.payload.transcript, '[omitted 32 chars]');
assert.equal(transcriptEvent.payload.text, '[omitted 14 chars]');
assert.equal(transcriptEvent.payload.delta, '[omitted 16 chars]');
assert.equal(numericArguments.payload.arguments, '[omitted]');
assert.equal(
JSON.stringify(deepObject.payload).includes('[omitted: max depth]'),
true,
);
assert.equal(
JSON.stringify(deepArray.payload).includes('[omitted: max depth]'),
true,
);
} else {
assert.deepEqual(
toolCall.payload.arguments,
records[0].payload.arguments,
);
assert.equal(
serverEvent.payload.payload.arguments,
records[1].payload.payload.arguments,
);
assert.equal(numericArguments.payload.arguments, 2);
}
}
});
test('debug logs omit structured and serialized tool results unless content logging is enabled', async (t) => {
const secret = 'private-address-fixture';
const result = attachVoiceResult('fly_to_location', {
ok: true,
query: secret,
label: secret,
arrived: true,
// New tool-specific fields must not need their own redaction rule.
futureField: { nested: [secret] },
});
const records = [
{
event: 'tool.result',
payload: { name: 'fly_to_location', callId: 'call-1', result },
},
{
event: 'client.function_call_output',
payload: {
message: {
type: 'conversation.item.create',
item: {
type: 'function_call_output',
call_id: 'call-1',
output: JSON.stringify(result),
},
},
},
},
{
event: 'server.event',
payload: {
response: {
id: 'response-1',
output: [
{
type: 'message',
content: [{ type: 'output_text', text: secret }],
},
],
usage: { input_tokens: 40, output_tokens: 10 },
},
},
},
].map((record) => sanitizeDebugValue(record));
for (const includeContent of [false, true]) {
const sourceRoot = root(t);
const handler = createDebugLogHandler({ sourceRoot, includeContent });
for (const record of records) {
const response = await request(handler, {
method: 'POST',
body: JSON.stringify(record),
});
assert.equal(response.status, 204);
}
const logged = readFileSync(
path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl'),
'utf8',
);
assert.equal(logged.includes(secret), includeContent);
const [tool, client, server] = logged.trim().split('\n').map(JSON.parse);
assert.equal(tool.payload.name, 'fly_to_location');
assert.equal(tool.payload.callId, 'call-1');
assert.equal(client.payload.message.item.call_id, 'call-1');
assert.equal(server.payload.response.id, 'response-1');
assert.equal(server.payload.response.output[0].type, 'message');
assert.deepEqual(
server.payload.response.usage,
records[2].payload.response.usage,
);
if (includeContent) {
assert.deepEqual(tool.payload.result, result);
assert.equal(client.payload.message.item.output, JSON.stringify(result));
} else {
assert.equal(tool.payload.result, '[omitted]');
assert.match(
client.payload.message.item.output,
/^\[omitted \d+ chars\]$/,
);
}
}
});
test('the debug-log sink stays bounded, rate limited, and quiet about failures', async (t) => {
const sourceRoot = root(t);
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
+698
View File
@@ -0,0 +1,698 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { PbfWriter } from 'pbf';
import {
mapillaryProxy,
TILE_ROUTE_MAX_PER_MIN,
} from 'gods-eye-view/server/providers/mapillary';
import { stripTileLayers } from '../../server/providers/mapillary/trim.js';
import {
MAX_RETRY_AFTER_SEC,
TILE_MAX_BYTES,
TILE_MEMORY_MAX_TILE_BYTES,
TILE_MEMORY_MAX_TILES,
TILE_UPSTREAM_CONCURRENCY,
TILE_TTL_MS,
fetchTile,
parseTilePath,
_resetTileCacheForTest,
} from '../../server/providers/mapillary/tiles.js';
/** Build a minimal MVT: layers with a name, a version and one opaque feature. */
function tile(layers) {
const writer = new PbfWriter();
for (const { name, payload } of layers) {
writer.writeMessage(
3,
(layer, pbf) => {
pbf.writeVarintField(15, 2); // version
pbf.writeStringField(1, layer.name);
pbf.writeMessage(2, (_f, p) => p.writeVarintField(1, 7), null); // feature
pbf.writeVarintField(5, 4096); // extent
if (layer.payload) pbf.writeBytesField(4, layer.payload); // a big key
},
{ name, payload },
);
}
return Buffer.from(writer.finish());
}
const SEQUENCE = tile([{ name: 'sequence' }]);
const UNTRIMMED = tile([
{ name: 'sequence' },
{ name: 'image', payload: Buffer.alloc(4000, 1) },
]);
/** Mount the plugin and return `call(route, url, method, headers, onResponse)`. */
function install(mode = 'configureServer') {
const routes = new Map();
mapillaryProxy()[mode]({
middlewares: { use: (route, handler) => routes.set(route, handler) },
});
const call = async (
route,
url = '/',
method = 'GET',
reqHeaders = {},
onResponse = null,
) => {
const handler = routes.get(route);
assert.ok(handler, `route ${route} is mounted`);
const headers = {};
const listeners = new Map();
const res = {
statusCode: 200,
headers,
on: (type, listener) => listeners.set(type, listener),
/** The client goes away, as Node reports it. */
close: () => listeners.get('close')?.(),
setHeader: (name, value) => (headers[name.toLowerCase()] = value),
getHeader: (name) => headers[name.toLowerCase()],
writeHead(status, extra) {
this.statusCode = status;
Object.assign(headers, extra || {});
},
end(payload) {
this.body = payload;
this.writableEnded = true;
},
};
onResponse?.(res);
await handler({ url, method, headers: reqHeaders }, res);
return res;
};
return { routes, call };
}
const json = (res) => JSON.parse(String(res.body));
/** Every header value and the body, as text, for a token-leak check. */
const leakText = (res) =>
[
...Object.values(res.headers).map(String),
Buffer.isBuffer(res.body) ? res.body.toString('latin1') : String(res.body),
].join('\n');
function withToken(token, run) {
const saved = process.env.MAPILLARY_CLIENT_TOKEN;
if (token === undefined) delete process.env.MAPILLARY_CLIENT_TOKEN;
else process.env.MAPILLARY_CLIENT_TOKEN = token;
return Promise.resolve()
.then(run)
.finally(() => {
if (saved === undefined) delete process.env.MAPILLARY_CLIENT_TOKEN;
else process.env.MAPILLARY_CLIENT_TOKEN = saved;
});
}
/** Drive the tile route against a scripted upstream `answer(n, url)`. */
async function withUpstream(answer, run) {
const savedFetch = globalThis.fetch;
_resetTileCacheForTest();
const calls = [];
const inits = [];
globalThis.fetch = async (url, init) => {
calls.push(String(url));
inits.push(init);
return answer(calls.length, String(url), init);
};
try {
await withToken('MLY|test|token', () =>
run({ calls, inits, call: install().call }),
);
} finally {
globalThis.fetch = savedFetch;
_resetTileCacheForTest();
}
}
const ok = (body = SEQUENCE) => new Response(body, { status: 200 });
test('the plugin mounts the status and tile routes for dev and preview servers', () => {
for (const mode of ['configureServer', 'configurePreviewServer'])
assert.deepEqual([...install(mode).routes.keys()].sort(), [
'/api/mapillary/status',
'/api/mapillary/tiles',
]);
});
test('status reports whether a token exists, never its value', async () => {
const { call } = install();
await withToken(undefined, async () => {
const res = await call('/api/mapillary/status');
assert.equal(res.statusCode, 200);
assert.deepEqual(json(res), { configured: false });
assert.equal(
(await call('/api/mapillary/status', '/', 'POST')).statusCode,
405,
);
});
await withToken('MLY|secret', async () => {
const res = await call('/api/mapillary/status');
assert.deepEqual(json(res), { configured: true });
assert.doesNotMatch(leakText(res), /MLY\|/);
});
});
test('the tile route validates the path, then needs a token', async () => {
await withUpstream(
() => assert.fail('Mapillary is not asked'),
async ({ call }) => {
for (const bad of [
'/coverage/14/1/x',
'/signs/14/1/2',
'/coverage/10/0/0',
'/coverage/15/0/0',
'/coverage/11/2048/0',
'/coverage/11/0/2048',
'/coverage/14/1/2/3',
]) {
const res = await call('/api/mapillary/tiles', bad);
assert.equal(res.statusCode, 400, bad);
}
const post = await call(
'/api/mapillary/tiles',
'/coverage/14/1/2',
'POST',
);
assert.equal(post.statusCode, 405);
},
);
await withToken(undefined, async () => {
const res = await install().call(
'/api/mapillary/tiles',
'/coverage/14/1/2',
);
assert.equal(res.statusCode, 503);
assert.deepEqual(json(res), { error: 'no_key', keyRequired: true });
});
});
test('a tile is trimmed, served with browser caching, then from memory', async () => {
await withUpstream(
() => ok(UNTRIMMED),
async ({ calls, inits, call }) => {
const res = await call('/api/mapillary/tiles', '/coverage/14/9/9');
assert.equal(res.statusCode, 200);
assert.equal(res.headers['content-type'], 'application/x-protobuf');
assert.equal(res.headers['cache-control'], 'public, max-age=3600');
assert.equal(res.headers['x-gev-cache'], 'upstream');
assert.deepEqual(res.body, SEQUENCE, 'the image layer is dropped');
assert.doesNotMatch(leakText(res), /MLY\|/);
assert.equal(
calls[0],
'https://tiles.mapillary.com/maps/vtp/mly1_public/2/14/9/9?access_token=MLY%7Ctest%7Ctoken',
);
assert.equal(inits[0].redirect, 'manual');
const again = await call('/api/mapillary/tiles', '/coverage/14/9/9?v=2');
assert.equal(again.headers['x-gev-cache'], 'memory');
assert.deepEqual(again.body, SEQUENCE);
assert.equal(calls.length, 1);
},
);
});
for (const status of [204, 404])
test(`an empty upstream tile (${status}) is a 204 with no body, and is cached`, async () => {
await withUpstream(
() => new Response(null, { status }),
async ({ calls, call }) => {
for (const source of ['upstream', 'memory']) {
const res = await call('/api/mapillary/tiles', '/coverage/14/10/10');
assert.equal(res.statusCode, 204);
assert.equal(res.body, undefined);
assert.equal(res.headers['x-gev-cache'], source);
}
assert.equal(calls.length, 1);
},
);
});
for (const status of [401, 403])
test(`an upstream ${status} is a rejected key`, async () => {
await withUpstream(
() => new Response('{}', { status }),
async ({ call }) => {
const res = await call('/api/mapillary/tiles', '/coverage/14/5/5');
assert.equal(res.statusCode, 403);
assert.deepEqual(json(res), {
error: 'Mapillary rejected the access token',
keyRejected: true,
});
assert.doesNotMatch(leakText(res), /MLY\|/);
},
);
});
for (const [sent, expected] of [
['30', 30],
[null, 60],
['999999', 600],
])
test(`an upstream 429 (Retry-After ${sent}) is passed on as ${expected} s`, async () => {
await withUpstream(
() =>
new Response('{}', {
status: 429,
headers: sent === null ? {} : { 'Retry-After': sent },
}),
async ({ calls, call }) => {
const res = await call('/api/mapillary/tiles', '/coverage/14/5/5');
assert.equal(res.statusCode, 429);
assert.equal(res.headers['retry-after'], String(expected));
assert.deepEqual(json(res), {
error: 'Mapillary is rate-limiting tile requests',
retryAfter: expected,
});
// Not cached as a tile, but refused here until the wait is over.
const again = await call('/api/mapillary/tiles', '/coverage/14/5/5');
assert.equal(again.statusCode, 429);
assert.equal(calls.length, 1);
},
);
});
for (const status of [302, 400, 410, 500])
test(`an upstream ${status} is a 502, not the client's fault`, async () => {
await withUpstream(
() => new Response('{}', { status }),
async ({ call }) => {
const res = await call('/api/mapillary/tiles', '/coverage/14/12/12');
assert.equal(res.statusCode, 502);
assert.deepEqual(json(res), {
error: `Mapillary tiles HTTP ${status}`,
});
},
);
});
test('a streamed body past the size cap is cancelled and answered 502', async () => {
const MB = 1024 * 1024;
let pulled = 0;
let cancelled = false;
// Chunked, no Content-Length: only a running count can see it is too big.
const body = () =>
new ReadableStream({
pull(controller) {
controller.enqueue(new Uint8Array(MB));
pulled += MB;
if (pulled >= TILE_MAX_BYTES + 8 * MB) controller.close();
},
cancel() {
cancelled = true;
},
});
await withUpstream(
() => new Response(body(), { status: 200 }),
async ({ call }) => {
const res = await call('/api/mapillary/tiles', '/coverage/14/19/19');
assert.equal(res.statusCode, 502);
assert.deepEqual(json(res), { error: 'Mapillary tile exceeds size cap' });
assert.equal(cancelled, true);
assert.ok(pulled <= TILE_MAX_BYTES + 2 * MB, `stopped after ${pulled}`);
},
);
});
test('cross-site requests are refused on both routes; the app itself passes', async () => {
await withUpstream(
() => ok(),
async ({ calls, call }) => {
const host = 'localhost:5173';
for (const headers of [
{ host, 'sec-fetch-site': 'cross-site' }, // <img>, navigation
{ host, 'sec-fetch-site': 'same-site' },
{ host, origin: 'https://evil.example' }, // fetch from another page
{ host, origin: 'null' }, // sandboxed frame
{ host, 'x-forwarded-for': '203.0.113.9' }, // through a proxy
])
for (const [route, url] of [
['/api/mapillary/status', '/'],
['/api/mapillary/tiles', '/coverage/14/13/13'],
]) {
const res = await call(route, url, 'GET', headers);
assert.equal(
res.statusCode,
403,
`${route} ${JSON.stringify(headers)}`,
);
}
assert.equal(calls.length, 0, 'Mapillary is never asked');
const app = { host, 'sec-fetch-site': 'same-origin' };
const status = await call('/api/mapillary/status', '/', 'GET', {
...app,
origin: `http://${host}`,
});
assert.deepEqual(json(status), { configured: true });
const res = await call(
'/api/mapillary/tiles',
'/coverage/14/13/14',
'GET',
app,
);
assert.equal(res.statusCode, 200);
},
);
});
test('the tile route answers 429 past its per-IP budget', async () => {
await withToken(undefined, async () => {
const { call } = install();
for (let i = 0; i < TILE_ROUTE_MAX_PER_MIN; i++)
assert.equal(
(await call('/api/mapillary/tiles', '/coverage/14/1/2')).statusCode,
503,
);
const over = await call('/api/mapillary/tiles', '/coverage/14/1/2');
assert.equal(over.statusCode, 429);
assert.equal(over.headers['retry-after'], '5');
assert.deepEqual(json(over), {
error: 'Too many tile requests',
retryAfter: 5,
});
const status = await call('/api/mapillary/status');
assert.equal(status.statusCode, 200, 'the status route is not limited');
});
});
test('identical concurrent tile requests share one upstream fetch', async () => {
let release;
const gate = new Promise((resolve) => (release = resolve));
await withUpstream(
async (n) =>
n === 1 ? (await gate, ok()) : new Response('{}', { status: 500 }),
async ({ calls, call }) => {
const pending = [1, 2, 3].map(() =>
call('/api/mapillary/tiles', '/coverage/14/1/1'),
);
await new Promise((resolve) => setTimeout(resolve, 10));
assert.equal(calls.length, 1);
release();
const sources = (await Promise.all(pending)).map((res) => {
assert.deepEqual(res.body, SEQUENCE);
return res.headers['x-gev-cache'];
});
assert.deepEqual(sources, ['upstream', 'inflight', 'inflight']);
// A failed flight is not reused either.
const failed = await call('/api/mapillary/tiles', '/coverage/14/2/2');
assert.equal(failed.statusCode, 502);
await call('/api/mapillary/tiles', '/coverage/14/2/2');
assert.equal(calls.length, 3);
},
);
});
test('at most a few upstream fetches run at once; the rest wait their turn', async () => {
let release;
let gate = new Promise((resolve) => (release = resolve));
await withUpstream(
async () => (await gate, ok()),
async ({ calls, call }) => {
const tiles = TILE_UPSTREAM_CONCURRENCY + 4;
const pending = Array.from({ length: tiles }, (_, i) =>
call('/api/mapillary/tiles', `/coverage/14/${i}/3`),
);
await new Promise((resolve) => setTimeout(resolve, 10));
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY);
release();
const done = await Promise.all(pending);
assert.ok(done.every((res) => res.statusCode === 200));
assert.equal(calls.length, tiles, 'the waiting ones ran too');
// Every slot came back: a second burst runs as many at once again.
let reopen;
gate = new Promise((resolve) => (reopen = resolve));
const again = Array.from({ length: tiles }, (_, i) =>
call('/api/mapillary/tiles', `/coverage/14/${i}/4`),
);
await new Promise((resolve) => setTimeout(resolve, 10));
assert.equal(calls.length, tiles + TILE_UPSTREAM_CONCURRENCY);
reopen();
await Promise.all(again);
assert.equal(calls.length, 2 * tiles);
},
);
});
/** An upstream that answers when released, and fails as a fetch does when aborted. */
function heldUpstream() {
let release;
const gate = new Promise((resolve) => (release = resolve));
const answer = (n, url, init) =>
new Promise((resolve, reject) => {
init?.signal?.addEventListener('abort', () =>
reject(new DOMException('aborted', 'AbortError')),
);
gate.then(() => resolve(ok()));
});
return { answer, release };
}
const settleSoon = () => new Promise((resolve) => setTimeout(resolve, 10));
test(
'a queued fetch whose client left never reaches Mapillary',
{ timeout: 5000 },
async () => {
const upstream = heldUpstream();
await withUpstream(upstream.answer, async ({ calls }) => {
const busy = Array.from({ length: TILE_UPSTREAM_CONCURRENCY }, (_, i) =>
fetchTile(parseTilePath(`/coverage/14/${i}/5`)),
);
const client = new AbortController();
const queued = fetchTile(parseTilePath('/coverage/14/99/5'), {
signal: client.signal,
});
await settleSoon();
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY, 'the 7th waits');
client.abort();
await assert.rejects(queued, { name: 'AbortError' });
upstream.release();
await Promise.all(busy);
await settleSoon();
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY, 'and never ran');
// The freed slots still serve a live request.
await fetchTile(parseTilePath('/coverage/14/98/5'));
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY + 1);
});
},
);
test(
'a client that disconnects cancels its tile fetch and gets no answer',
{ timeout: 5000 },
async () => {
const upstream = heldUpstream();
await withUpstream(upstream.answer, async ({ inits, call }) => {
let res;
const done = call(
'/api/mapillary/tiles',
'/coverage/14/8/8',
'GET',
{},
(r) => (res = r),
);
await settleSoon();
res.close();
await done;
assert.equal(inits[0].signal.aborted, true, 'the upstream fetch stopped');
assert.equal(res.body, undefined, 'nothing written to a closed socket');
});
},
);
test(
'a shared fetch is cancelled only when its last client leaves',
{ timeout: 5000 },
async () => {
const upstream = heldUpstream();
await withUpstream(upstream.answer, async ({ calls, inits }) => {
const address = parseTilePath('/coverage/14/7/7');
const first = new AbortController();
const second = new AbortController();
const a = fetchTile(address, { signal: first.signal });
const b = fetchTile(address, { signal: second.signal });
await settleSoon();
assert.equal(calls.length, 1, 'one fetch for both');
first.abort();
await assert.rejects(a, { name: 'AbortError' });
assert.equal(inits[0].signal.aborted, false, 'the other still waits');
second.abort();
await assert.rejects(b, { name: 'AbortError' });
assert.equal(inits[0].signal.aborted, true, 'the last one left');
// A new request starts afresh rather than joining the cancelled fetch.
upstream.release();
const again = await fetchTile(address);
assert.equal(again.source, 'upstream');
assert.equal(calls.length, 2);
});
},
);
test('a tile held in memory past its TTL is fetched again', async (t) => {
await withUpstream(
() => ok(),
async ({ calls, call }) => {
await call('/api/mapillary/tiles', '/coverage/14/3/3');
await call('/api/mapillary/tiles', '/coverage/14/3/3');
assert.equal(calls.length, 1);
const now = Date.now();
t.mock.method(Date, 'now', () => now + TILE_TTL_MS + 1000);
const res = await call('/api/mapillary/tiles', '/coverage/14/3/3');
assert.equal(res.headers['x-gev-cache'], 'upstream');
assert.equal(calls.length, 2);
},
);
});
test('the memory cache evicts the least recently used tile past its size', async () => {
await withUpstream(
() => ok(),
async ({ calls }) => {
const get = async (x) =>
(await fetchTile(parseTilePath(`/coverage/14/${x}/20`))).source;
for (let x = 0; x < TILE_MEMORY_MAX_TILES; x++) await get(x);
assert.equal(await get(0), 'memory', 'the first is used again');
await get(TILE_MEMORY_MAX_TILES);
assert.equal(await get(0), 'memory', 'so it stays');
assert.equal(await get(1), 'upstream', 'the least recently used went');
assert.equal(calls.length, TILE_MEMORY_MAX_TILES + 2);
},
);
});
// ── Upstream refusals ──
const rateLimited = (retryAfter) =>
new Response('', { status: 429, headers: { 'retry-after': retryAfter } });
test('after a 429 the rest of the view waits out Retry-After here instead of asking Mapillary', async (t) => {
const now = Date.now();
t.mock.method(Date, 'now', () => now);
await withUpstream(
(n) => (n === 1 ? rateLimited('120') : ok()),
async ({ calls, call }) => {
const first = await call('/api/mapillary/tiles', '/coverage/14/1/1');
assert.equal(first.statusCode, 429);
assert.equal(json(first).retryAfter, 120);
const rest = await Promise.all(
Array.from({ length: 8 }, (_, i) =>
call('/api/mapillary/tiles', `/coverage/14/2/${i}`),
),
);
assert.equal(calls.length, 1, 'nothing else reached Mapillary');
for (const res of rest) {
assert.equal(res.statusCode, 429);
assert.equal(json(res).retryAfter, 120);
}
// Half way through the wait, the answer says what is left.
Date.now.mock.mockImplementation(() => now + 50_000);
const midway = await call('/api/mapillary/tiles', '/coverage/14/3/3');
assert.equal(json(midway).retryAfter, 70);
assert.equal(calls.length, 1);
// Once it is over, requests go upstream again.
Date.now.mock.mockImplementation(() => now + 121_000);
const later = await call('/api/mapillary/tiles', '/coverage/14/3/3');
assert.equal(later.statusCode, 200);
assert.equal(calls.length, 2);
},
);
});
test('a refusal is scoped to the token that was refused', async () => {
await withUpstream(
(n) => (n === 1 ? rateLimited('120') : ok()),
async ({ calls, call }) => {
await call('/api/mapillary/tiles', '/coverage/14/1/1');
process.env.MAPILLARY_CLIENT_TOKEN = 'MLY|replaced|token';
const res = await call('/api/mapillary/tiles', '/coverage/14/1/2');
assert.equal(res.statusCode, 200, 'a new token is tried at once');
assert.equal(calls.length, 2);
},
);
});
test(
'fetches queued behind a 429 never reach Mapillary',
{ timeout: 5000 },
async () => {
let release;
const gate = new Promise((resolve) => (release = resolve));
const answer = (n) => gate.then(() => (n === 1 ? rateLimited('60') : ok()));
await withUpstream(answer, async ({ calls }) => {
const busy = Array.from({ length: TILE_UPSTREAM_CONCURRENCY }, (_, i) =>
fetchTile(parseTilePath(`/coverage/14/${i}/6`)).catch((e) => e),
);
const queued = Array.from({ length: 3 }, (_, i) =>
fetchTile(parseTilePath(`/coverage/14/${i}/7`)).catch((e) => e),
);
await settleSoon();
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY, 'three wait');
release();
const [first] = await Promise.all(busy);
assert.equal(first.status, 429);
for (const outcome of await Promise.all(queued)) {
assert.equal(outcome.status, 429, 'refused here');
assert.equal(outcome.retryAfter, '60');
}
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY, 'and never ran');
});
},
);
test('an HTTP-date Retry-After is passed on as the seconds left; garbage means a minute', async (t) => {
const now = Date.UTC(2026, 9, 13, 12, 0, 0);
t.mock.method(Date, 'now', () => now);
const inFiveMinutes = new Date(now + 5 * 60_000).toUTCString();
const inAnHour = new Date(now + 60 * 60_000).toUTCString();
const anHourAgo = new Date(now - 60 * 60_000).toUTCString();
const sent = [inFiveMinutes, inAnHour, anHourAgo, 'soon', ''];
await withUpstream(
(n) => rateLimited(sent[n - 1]),
async ({ call }) => {
const retryAfter = async (x) => {
_resetTileCacheForTest();
const res = await call('/api/mapillary/tiles', `/coverage/14/${x}/9`);
assert.equal(res.statusCode, 429);
assert.equal(res.headers['retry-after'], String(json(res).retryAfter));
return json(res).retryAfter;
};
assert.equal(await retryAfter(1), 300);
assert.equal(await retryAfter(2), MAX_RETRY_AFTER_SEC, 'capped');
assert.equal(await retryAfter(3), 60, 'a date in the past');
assert.equal(await retryAfter(4), 60, 'not a date');
assert.equal(await retryAfter(5), 60, 'no header');
},
);
});
test('a body too big to be a trimmed tile is served but not kept in memory', async () => {
const huge = Buffer.alloc(TILE_MEMORY_MAX_TILE_BYTES + 1, 0xff);
await withUpstream(
() => ok(huge),
async ({ calls, call }) => {
const first = await call('/api/mapillary/tiles', '/coverage/14/5/5');
assert.equal(first.statusCode, 200);
assert.equal(first.body.length, huge.length, 'served as it came');
const second = await call('/api/mapillary/tiles', '/coverage/14/5/5');
assert.equal(second.headers['x-gev-cache'], 'upstream', 'not cached');
assert.equal(calls.length, 2);
},
);
});
// ── Tile trimming ──
test('dropping a layer keeps the others byte-for-byte', () => {
const bytes = tile([
{ name: 'sequence' },
{ name: 'image', payload: Buffer.alloc(50_000, 7) },
{ name: 'overview' },
]);
const trimmed = stripTileLayers(bytes, ['image']);
assert.deepEqual(trimmed, tile([{ name: 'sequence' }, { name: 'overview' }]));
});
test('a tile without the layer is returned untouched', () => {
assert.equal(stripTileLayers(SEQUENCE, ['image']), SEQUENCE);
assert.equal(stripTileLayers(SEQUENCE, []), SEQUENCE);
assert.equal(stripTileLayers(Buffer.alloc(0), ['image']).length, 0);
});
+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);
}
+293 -75
View File
@@ -24,7 +24,11 @@ import {
geocodeProxy,
} from '../../server/providers/regional/place.js';
const PUBLIC = { endpoint: PUBLIC_NOMINATIM_SEARCH, isPublic: true, dailyCap: 50 };
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-'));
@@ -63,7 +67,13 @@ function mount(options) {
};
Promise.resolve(
handler(
{ method: 'GET', url, headers: {}, socket: { remoteAddress: `10.9.${Math.random()}` }, on() {} },
{
method: 'GET',
url,
headers: {},
socket: { remoteAddress: `10.9.${Math.random()}` },
on() {},
},
res,
),
).catch(reject);
@@ -88,20 +98,33 @@ test('the daily count survives cache sweeps and a restart of the real compositio
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 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.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 };
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');
@@ -112,15 +135,24 @@ test('a sweep removes expired and corrupt entries, never ones it could not read'
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]);
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]);
assert.deepEqual(
[1, 2, 3].map((n) => fs.existsSync(file(n))),
[true, false, false],
);
});
test('processes sharing state reserve public spacing and cap atomically', async (t) => {
const dir = tempDir(t);
const module = new URL('../../server/providers/regional/nominatimGate.js', import.meta.url).href;
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)});
@@ -133,7 +165,11 @@ test('processes sharing state reserve public spacing and cap atomically', async
})));
`;
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, ['--input-type=module', '-e', script]);
const child = spawn(process.execPath, [
'--input-type=module',
'-e',
script,
]);
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => (stdout += chunk));
@@ -160,15 +196,21 @@ test('processes sharing state reserve public spacing and cap atomically', async
reservations.map((result) => result.startAt).sort((a, b) => a - b),
[1000, 2100, 3200],
);
assert.equal(createGateStateStore({ file: spacedFile }).count('2026-09-29'), 3);
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', 'reserved'],
assert.deepEqual(capped.map((result) => result.status).sort(), [
'cap',
'reserved',
]);
assert.equal(
createGateStateStore({ file: cappedFile }).count('2026-09-29'),
1,
);
assert.equal(createGateStateStore({ file: cappedFile }).count('2026-09-29'), 1);
});
test('independent public gates sharing state enforce spacing and cap', async (t) => {
@@ -209,7 +251,10 @@ test('independent public gates sharing state enforce spacing and cap', async (t)
createNominatimSearchProvider({ gate: capGate() })('three', null),
createNominatimSearchProvider({ gate: capGate() })('four', null),
]);
assert.equal(results.filter((result) => result.status === 'fulfilled').length, 1);
assert.equal(
results.filter((result) => result.status === 'fulfilled').length,
1,
);
assert.equal(
results.find((result) => result.status === 'rejected').reason.code,
'NOMINATIM_DAILY_CAP',
@@ -283,14 +328,18 @@ test('a long Retry-After is honoured in full and survives a restart', async (t)
const now = () => clock;
const refused = createNominatimGate({
settings: PUBLIC,
fetchImpl: async () => new Response('', { status: 429, headers: { 'Retry-After': '86400' } }),
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',
});
await assert.rejects(
createNominatimSearchProvider({ gate: refused })('x', null),
{
code: 'NOMINATIM_REFUSED',
},
);
const calls = [];
const restarted = createNominatimGate({
settings: PUBLIC,
@@ -359,7 +408,8 @@ test('a stalled body times out instead of holding the shared queue', async () =>
let n = 0;
const gate = createNominatimGate({
settings: { ...PUBLIC, isPublic: false },
fetchImpl: async (_url, { signal }) => (++n <= 2 ? stalled(signal) : json([])),
fetchImpl: async (_url, { signal }) =>
++n <= 2 ? stalled(signal) : json([]),
timeoutMs: 150,
});
const search = createNominatimSearchProvider({ gate });
@@ -375,7 +425,19 @@ test('cache keys are request-equivalent: a miss for "Paris???" never answers "Pa
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' }]);
return json(
calls.length === 1
? []
: [
{
lat: '48.85',
lon: '2.35',
display_name: 'Paris, France',
name: 'Paris',
addresstype: 'city',
},
],
);
},
...quick,
});
@@ -400,10 +462,17 @@ test('a malformed answer is a failure, not a cached "no result"', async () => {
});
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' });
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(
(await outline({ query: 'Zilker Park', kind: 'landmark' })).status,
'ZERO_RESULTS',
);
assert.equal(calls, 3, 'the malformed answers were not cached');
});
@@ -418,14 +487,22 @@ test('cache entries are namespaced by endpoint', async () => {
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);
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,
resolveNominatimSettings({
NOMINATIM_URL: 'https://nominatim.openstreetmap.org./search',
}).isPublic,
true,
);
});
@@ -451,11 +528,29 @@ const boundary = (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);
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 })],
@@ -470,30 +565,66 @@ test('administrative boundaries must be at the asked level', () => {
).outline,
null,
);
assert.equal(selectOutlineResult([state], { kind: 'neighborhood', query: 'Texas' }).outline, null);
assert.ok(selectOutlineResult([county], { kind: 'admin', query: 'Travis County' }).outline);
assert.ok(selectOutlineResult([boundary({ name: 'Somewhere', place_rank: 16 })], { kind: 'city', query: 'Somewhere' }).outline);
assert.equal(
selectOutlineResult([state], { kind: 'neighborhood', query: 'Texas' })
.outline,
null,
);
assert.ok(
selectOutlineResult(
[boundary({ name: 'Somewhere', addresstype: 'locality', place_rank: 16 })],
{ kind: 'city', query: 'Somewhere' },
).outline,
selectOutlineResult([county], { kind: 'admin', query: 'Travis County' })
.outline,
);
assert.equal(
selectOutlineResult([boundary({ name: 'Somewhere' })], { kind: 'city', query: 'Somewhere' }).outline,
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',
});
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('admin identity comes only from unambiguous evidence on the selected row', () => {
for (const [name, addresstype, adminLevel] of [
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'],
@@ -502,18 +633,21 @@ test('admin identity comes only from unambiguous evidence on the selected row',
['County of Travis', 'county', 'admin2'],
['Kathmandu District', 'state_district', 'admin2'],
]) {
const { outline } = selectOutlineResult(
[boundary({ name, addresstype })],
{ kind: 'admin', query: name },
);
assert.equal(outline.adminArea, name);
assert.equal(outline.adminLevel, adminLevel);
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: 'Mystery Province', 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' }),
@@ -530,29 +664,41 @@ test('admin identity comes only from unambiguous evidence on the selected row',
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(retired.adminArea, 'Bagmati Zone');
assert.equal(retired.adminLevel, undefined);
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(retiredPrefix.adminArea, 'Zone of Bagmati');
assert.equal(retiredPrefix.adminLevel, undefined);
assert.equal('adminArea' in retiredPrefix, false);
assert.equal('adminLevel' in retiredPrefix, false);
});
test('admin identity validates the full name before display truncation', () => {
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(valid.adminArea, longValidName.slice(0, 160));
assert.equal(valid.adminLevel, 'admin1');
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);
@@ -565,9 +711,15 @@ test('admin identity validates the full name before display truncation', () => {
assert.equal(contradictory.adminLevel, undefined);
});
test('non-administrative outline kinds never manufacture admin identity', () => {
test('non-administrative outline kinds do not publish admin geometry hooks', () => {
const city = selectOutlineResult(
[boundary({ name: 'County of Austin', addresstype: 'city', place_rank: 16 })],
[
boundary({
name: 'County of Austin',
addresstype: 'city',
place_rank: 16,
}),
],
{ kind: 'city', query: 'County of Austin' },
).outline;
const park = selectOutlineResult(
@@ -590,25 +742,91 @@ test('non-administrative outline kinds never manufacture admin identity', () =>
});
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 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);
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);
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);
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('新宿御苑'), namedetails: { name: '新宿御苑' } }], { kind: 'landmark', query: '新宿御苑' }).outline,
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,
);
});
+368 -27
View File
@@ -14,7 +14,7 @@ import os from 'node:os';
import path from 'node:path';
import {
createNominatimGate,
createUsageStore,
createGateStateStore,
parseRetryAfter,
resolveNominatimSettings,
PUBLIC_NOMINATIM_SEARCH,
@@ -113,10 +113,54 @@ function publicGate(t, options = {}) {
...options,
settings: options.settings || PUBLIC,
usage:
options.usage || createUsageStore({ file: path.join(dir, 'state.json') }),
options.usage ||
createGateStateStore({ file: path.join(dir, 'state.json') }),
});
}
function cancellableFlight(answer) {
let release;
let sharedSignal;
let calls = 0;
let markStarted;
const started = new Promise((resolve) => {
markStarted = resolve;
});
const gate = {
settings: () => OPERATOR,
requestJson(_buildUrl, { signal }) {
calls += 1;
sharedSignal = signal;
markStarted();
return new Promise((resolve, reject) => {
release = () => resolve(answer);
signal.addEventListener(
'abort',
() =>
reject(
Object.assign(new Error('abandoned'), {
code: 'NOMINATIM_ABANDONED',
}),
),
{ once: true },
);
});
},
};
return {
gate,
started,
release: () => release(),
signal: () => sharedSignal,
calls: () => calls,
};
}
const emptyCache = () => ({
get: async () => null,
set: async () => {},
});
// ---------------------------------------------------------------- settings
test('endpoint settings: unset is public, empty disables, operator URLs are not public', () => {
@@ -159,6 +203,13 @@ test('endpoint settings: unset is public, empty disables, operator URLs are not
}).isPublic,
true,
);
assert.equal(
resolveNominatimSettings({
NOMINATIM_URL: 'http://nominatim.openstreetmap.org/search',
}).endpoint,
null,
'the public service is never contacted over cleartext',
);
assert.equal(
resolveNominatimSettings({ NOMINATIM_DAILY_CAP: '7' }).dailyCap,
7,
@@ -173,6 +224,96 @@ test('endpoint settings: unset is public, empty disables, operator URLs are not
);
});
test('one cancelled subscriber does not abort a shared search', async () => {
const flight = cancellableFlight([
{
lat: '48.8566',
lon: '2.3522',
display_name: 'Paris, France',
name: 'Paris',
addresstype: 'city',
boundingbox: ['48.80', '48.90', '2.25', '2.45'],
},
]);
const search = createNominatimSearchProvider({
gate: flight.gate,
cache: emptyCache(),
});
const cancelled = new AbortController();
const first = search('Paris', null, { signal: cancelled.signal });
const second = search('Paris');
await flight.started;
cancelled.abort();
await assert.rejects(first, (error) => error.code === 'NOMINATIM_ABANDONED');
assert.equal(flight.signal().aborted, false);
flight.release();
assert.equal((await second).status, 'OK');
assert.equal(flight.calls(), 1);
});
test('one cancelled subscriber does not abort a shared outline', async () => {
const flight = cancellableFlight([
row({
category: 'boundary',
type: 'administrative',
addresstype: 'city',
name: 'Paris',
}),
]);
const outline = createNominatimOutlineProvider({
gate: flight.gate,
cache: emptyCache(),
});
const cancelled = new AbortController();
const ask = { query: 'Paris', kind: 'city', lat: 48.85, lon: 2.35 };
const first = outline(ask, { signal: cancelled.signal });
const second = outline(ask);
await flight.started;
cancelled.abort();
await assert.rejects(first, (error) => error.code === 'NOMINATIM_ABANDONED');
assert.equal(flight.signal().aborted, false);
flight.release();
assert.equal((await second).status, 'OK');
assert.equal(flight.calls(), 1);
});
test('the last cancelled subscriber aborts the shared upstream request', async () => {
const flight = cancellableFlight([
{
lat: '48.8566',
lon: '2.3522',
display_name: 'Paris, France',
name: 'Paris',
addresstype: 'city',
boundingbox: ['48.80', '48.90', '2.25', '2.45'],
},
]);
const search = createNominatimSearchProvider({
gate: flight.gate,
cache: emptyCache(),
});
const firstController = new AbortController();
const secondController = new AbortController();
const first = search('Paris', null, { signal: firstController.signal });
const second = search('Paris', null, { signal: secondController.signal });
const firstRejected = assert.rejects(
first,
(error) => error.code === 'NOMINATIM_ABANDONED',
);
const secondRejected = assert.rejects(
second,
(error) => error.code === 'NOMINATIM_ABANDONED',
);
await flight.started;
firstController.abort();
await firstRejected;
assert.equal(flight.signal().aborted, false);
secondController.abort();
await secondRejected;
assert.equal(flight.signal().aborted, true);
assert.equal(flight.calls(), 1);
});
test('Retry-After reads seconds and HTTP dates', () => {
const now = Date.UTC(2026, 0, 1);
assert.equal(parseRetryAfter('0', now), 0);
@@ -266,7 +407,7 @@ test('an operator endpoint is not subject to public cap, spacing, or durable sta
const gate = createNominatimGate({
settings: { ...OPERATOR, dailyCap: 1 },
fetchImpl,
usage: createUsageStore({ file: path.join(blocker, 'state.json') }),
usage: createGateStateStore({ file: path.join(blocker, 'state.json') }),
...time,
});
const search = createNominatimSearchProvider({ gate });
@@ -283,7 +424,7 @@ test('public access fails closed when durable state is missing or unwritable', a
const withoutState = createNominatimGate({
settings: PUBLIC,
fetchImpl,
usage: createUsageStore({ onError: () => {} }),
usage: createGateStateStore({ onError: () => {} }),
});
await assert.rejects(
createNominatimSearchProvider({ gate: withoutState })('missing', null),
@@ -297,7 +438,7 @@ test('public access fails closed when durable state is missing or unwritable', a
createNominatimGate({
settings: PUBLIC,
fetchImpl,
usage: createUsageStore({ file: invalidFile, onError: () => {} }),
usage: createGateStateStore({ file: invalidFile, onError: () => {} }),
});
await assert.rejects(
createNominatimSearchProvider({ gate: makeBrokenGate() })('broken', null),
@@ -377,7 +518,7 @@ test('malformed Retry-After values retain the default refusal cooldown', async (
const gate = publicGate(t, {
fetchImpl,
...time,
usage: createUsageStore({
usage: createGateStateStore({
file: path.join(tempDir(t), `malformed-${index}.json`),
}),
});
@@ -404,7 +545,7 @@ test('malformed Retry-After values retain the default refusal cooldown', async (
headers: { 'Retry-After': '2099-01-01' },
}),
...time,
usage: createUsageStore({ file }),
usage: createGateStateStore({ file }),
});
await assert.rejects(
createNominatimSearchProvider({ gate: makeGate() })(
@@ -491,16 +632,6 @@ test('the queue is bounded', async (t) => {
);
});
test('the daily count survives a restart when persisted', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-nominatim-usage-'));
const file = path.join(dir, 'usage.json');
createUsageStore({ file }).increment('2026-09-28');
createUsageStore({ file }).increment('2026-09-28');
assert.equal(createUsageStore({ file }).count('2026-09-28'), 2);
assert.equal(createUsageStore({ file }).count('2026-09-29'), 0);
fs.rmSync(dir, { recursive: true, force: true });
});
// ---------------------------------------------------------------- caches
test('cache lifetimes: 90 days for outlines, 30 for places, 24 hours for no result', async () => {
@@ -552,6 +683,25 @@ test('the disk cache survives a restart and stays within its bounds', async () =
await createNominatimCache({ dir, now: time.now }).get('k0'),
null,
);
assert.equal(
fs.statSync(dir).mode & 0o777,
0o700,
'the query cache directory is private',
);
const text = fs
.readdirSync(dir)
.filter((name) => name.endsWith('.json'))
.map((name) => fs.readFileSync(path.join(dir, name), 'utf8'))
.join('\n');
assert.equal(text.includes('k4'), false, 'disk entries never store raw keys');
for (const name of fs
.readdirSync(dir)
.filter((entry) => entry.endsWith('.json')))
assert.equal(
fs.statSync(path.join(dir, name)).mode & 0o777,
0o600,
`${name} is private`,
);
fs.rmSync(dir, { recursive: true, force: true });
});
@@ -661,6 +811,52 @@ test('the first polygon-bearing result of the right class is taken (Mumbai)', ()
'place=city: no polygon',
'railway=station: wrong type',
]);
const withGeometry = (geojson) =>
selectOutlineResult([{ ...boundary, geojson }], {
kind: 'city',
query: 'Mumbai',
}).outline;
const parts = Array.from({ length: 24 }, (_, i) => square(i, 0));
assert.equal(
withGeometry({ type: 'MultiPolygon', coordinates: parts }).polygons.length,
24,
'all supported parts survive',
);
const holes = Array.from(
{ length: 16 },
(_, i) => square(0.1 + i * 0.002, 0.1)[0],
);
assert.equal(
withGeometry({
type: 'Polygon',
coordinates: [square(0, 0, 1)[0], ...holes],
}).polygons[0].length,
17,
'all supported holes survive',
);
for (const coordinates of [
[...parts, square(25, 0)],
[[square(0, 0, 1)[0], ...holes, square(0.04, 0)[0]]],
[
[
square(0, 0, 1)[0],
[
[0, 0],
[0, 1],
[NaN, 1],
[0, 0],
],
],
],
[square(0, 0), []],
]) {
assert.equal(
withGeometry({ type: 'MultiPolygon', coordinates }),
null,
'unsupported or malformed topology is refused whole, never truncated',
);
}
});
test('a result whose name does not answer the ask is passed over', () => {
@@ -703,6 +899,83 @@ test('outlines are held to a vertex cap', () => {
assert.ok(total <= 6000, `${total} vertices`);
const first = capped[0][0];
assert.deepEqual(first[0], first.at(-1), 'rings stay closed');
const outer = square(0, 0, 1)[0];
const hole = square(0.1, 0.1, 0.1)[0];
assert.equal(
capOutlineVertices([[outer, hole]], 9),
null,
'a cap cannot discard a hole to make an exact outline fit',
);
assert.equal(
capOutlineVertices([[outer], [hole]], 9),
null,
'a cap cannot discard a part to make an exact outline fit',
);
const validHole = square(-0.1, -0.1, 0.2)[0];
const largeWithHole = capOutlineVertices([[ring, validHole]]);
assert.ok(largeWithHole, 'valid large outlines with holes still simplify');
assert.equal(largeWithHole[0].length, 2);
assert.ok(largeWithHole.flat().reduce((n, r) => n + r.length, 0) <= 6000);
const bay = [
[0, 0],
[0.001, 0],
[0.001, 0.0004],
[0.00101, 0.0004],
[0.00101, 0.0006],
[0.001, 0.0006],
[0.001, 0.001],
[0, 0.001],
[0, 0],
];
const bayHole = [
[0.001003, 0.00045],
[0.001007, 0.00045],
[0.001007, 0.00055],
[0.001003, 0.00055],
[0.001003, 0.00045],
];
const denseBay = bay.slice(0, -1).flatMap((point, i) => {
const end = bay[i + 1];
const steps = i === 0 || i === 6 ? 3001 : 1;
return Array.from({ length: steps }, (_, j) => [
point[0] + ((end[0] - point[0]) * j) / steps,
point[1] + ((end[1] - point[1]) * j) / steps,
]);
});
denseBay.push(bay[0]);
assert.ok(denseBay.length + bayHole.length > 6000);
assert.equal(
capOutlineVertices([[denseBay, bayHole]]),
null,
'default-cap simplification cannot leave the preserved hole outside its shell',
);
const concave = [
[0, 0],
[0.001, 0],
[0.001, 0.0005],
[0.0005, 0.0005],
[0.0005, 0.001],
[0, 0.001],
[0, 0],
];
const bentHole = [
[0.0001, 0.0001],
[0.0003, 0.0001],
[0.0005, 0.0001],
[0.0009, 0.0001],
[0.0009, 0.00049],
[0.00049, 0.00049],
[0.00049, 0.0009],
[0.0001, 0.0009],
[0.0001, 0.0001],
];
assert.equal(
capOutlineVertices([[concave, bentHole]], 11),
null,
'hole edges cannot cross a concave shell even when every output vertex is inside',
);
});
// ---------------------------------------------------------------- the route
@@ -717,6 +990,10 @@ function mount(options) {
const res = {
writableEnded: false,
on() {},
setHeader(name, value) {
res.headers ||= {};
res.headers[name] = value;
},
writeHead(status, sent) {
res.status = status;
res.headers = sent;
@@ -724,7 +1001,7 @@ function mount(options) {
end(body) {
res.writableEnded = true;
resolve({
status: res.status,
status: res.status ?? res.statusCode,
headers: res.headers,
body: JSON.parse(body),
});
@@ -778,22 +1055,64 @@ test('the outline route answers OK with polygons and validates its input', async
'sec-fetch-site': 'cross-site',
});
assert.equal(cross.status, 403);
for (const headers of [
{
host: 'localhost',
origin: 'https://foreign.example',
'sec-fetch-site': 'same-origin',
},
{ host: 'localhost', origin: 'null', 'sec-fetch-site': 'same-origin' },
{ host: 'localhost', 'sec-fetch-site': 'same-site' },
{ host: 'localhost', 'x-forwarded-for': '203.0.113.7' },
]) {
assert.equal(
(await request('/outline?q=Paris&kind=city', headers)).status,
403,
);
}
assert.equal(calls.length, 1);
assert.equal((await request('/elsewhere')).status, 'next');
});
test('the outline route serializes only selected-row administrative identity', async (t) => {
test('the outline route leaves administrative geometry hooks to the annotation layer', async (t) => {
const { fetchImpl } = stubFetch(async (url) => {
const query = new URL(url).searchParams.get('q');
const fixtures = {
'Bagmati Province': { addresstype: 'province' },
'Mystery Province': { place_rank: 8 },
'Unknown Type': { addresstype: 'unrecognized', place_rank: 8 },
'Coerced Rank': { addresstype: 'state', place_rank: [8] },
'String Rank': { addresstype: 'state', place_rank: '8' },
'Contradicted State': { addresstype: 'state', place_rank: 20 },
'Missing Evidence': {},
'Northern Region': { addresstype: 'region', place_rank: 8 },
'Central District': { addresstype: 'district', place_rank: 10 },
'Example State': { addresstype: 'county' },
'Bagmati Zone': { addresstype: 'state' },
Austin: { addresstype: 'city' },
'Explicit Place City': {
category: 'place',
type: 'city',
place_rank: 16,
},
'Contradicted Place City': {
category: 'place',
type: 'city',
addresstype: 'suburb',
place_rank: 22,
},
'Malformed Place Rank': {
category: 'place',
type: 'city',
addresstype: 'city',
place_rank: '16',
},
'Accepted City Block': {
category: 'place',
type: 'city_block',
addresstype: 'city_block',
place_rank: 25,
},
};
return json([
row({
@@ -813,11 +1132,10 @@ test('the outline route serializes only selected-row administrative identity', a
const verified = await request('/outline?q=Bagmati%20Province&kind=admin');
assert.equal(verified.status, 200);
assert.equal(verified.body.outline.adminArea, 'Bagmati Province');
assert.equal(verified.body.outline.adminLevel, 'admin1');
assert.equal('adminArea' in verified.body.outline, false);
assert.equal('adminLevel' in verified.body.outline, false);
for (const query of [
'Mystery Province',
'Northern Region',
'Central District',
'Example State',
@@ -831,15 +1149,26 @@ test('the outline route serializes only selected-row administrative identity', a
assert.equal('adminLevel' in unverified.body.outline, false, query);
}
const missing = await request('/outline?q=Missing%20Evidence&kind=admin');
assert.equal(missing.status, 200);
assert.equal(missing.body.status, 'ZERO_RESULTS');
assert.equal(missing.body.outline, null);
for (const query of [
'Missing Evidence',
'Mystery Province',
'Unknown Type',
'Coerced Rank',
'String Rank',
'Contradicted State',
]) {
const missing = await request(
`/outline?q=${encodeURIComponent(query)}&kind=admin`,
);
assert.equal(missing.status, 200, query);
assert.equal(missing.body.status, 'ZERO_RESULTS', query);
assert.equal(missing.body.outline, null, query);
}
const retired = await request('/outline?q=Bagmati%20Zone&kind=admin');
assert.equal(retired.status, 200);
assert.equal(retired.body.status, 'OK');
assert.equal(retired.body.outline.adminArea, 'Bagmati Zone');
assert.equal('adminArea' in retired.body.outline, false);
assert.equal('adminLevel' in retired.body.outline, false);
const city = await request('/outline?q=Austin&kind=city');
@@ -847,6 +1176,18 @@ test('the outline route serializes only selected-row administrative identity', a
assert.equal(city.body.status, 'OK');
assert.equal('adminArea' in city.body.outline, false);
assert.equal('adminLevel' in city.body.outline, false);
for (const [query, kind, status] of [
['Explicit Place City', 'city', 'OK'],
['Contradicted Place City', 'city', 'ZERO_RESULTS'],
['Malformed Place Rank', 'city', 'ZERO_RESULTS'],
['Accepted City Block', 'neighborhood', 'OK'],
]) {
const answer = await request(
`/outline?q=${encodeURIComponent(query)}&kind=${kind}`,
);
assert.equal(answer.status, 200, query);
assert.equal(answer.body.status, status, query);
}
});
test('the outline route reports disabled and capped as permanent, busy as retryable', async (t) => {
+80 -7
View File
@@ -18,14 +18,35 @@ import {
NOMINATIM_MAX_PENDING,
} from '../../server/providers/regional/place.js';
import { fetchRegionalJson } from '../../server/providers/regional/http.js';
import { setSharedNominatimGate } from '../../server/providers/regional/nominatimGate.js';
import {
createNominatimGate,
resolveNominatimSettings,
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.
test.beforeEach(() => setSharedNominatimGate(null));
// 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({}),
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 }));
@@ -106,14 +127,17 @@ function mountGeocode(install = null) {
/** 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;
}
}
@@ -124,13 +148,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 () => {
@@ -139,7 +205,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, {
@@ -147,6 +215,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']),
@@ -160,7 +233,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(
+18
View File
@@ -81,6 +81,24 @@ test('nearby labels rank landmarks, deduplicate names/addresses and bound the pr
assert.deepEqual(projectNearbyPlaces({}, 0, 0), []);
});
test('nearby place distances take the short way round 180°', () => {
const at = (name, longitude) => ({
id: name,
displayName: { text: name },
location: { latitude: -16.8, longitude },
types: ['point_of_interest'],
});
// From Taveuni at 179.98: 2 km east across 180°, 4 km west on this side.
const result = projectNearbyPlaces(
{ places: [at('West', 179.942), at('East', -179.999)] },
-16.8,
179.98,
);
const byName = Object.fromEntries(result.map((p) => [p.name, p.distanceM]));
assert.ok(byName.East > 1500 && byName.East < 2500, String(byName.East));
assert.ok(byName.West > 3500 && byName.West < 4500, String(byName.West));
});
test('text search preserves bounds, rejects malformed bounds and tolerates absent locations', () => {
const viewport = {
low: { latitude: 1, longitude: 2 },
+23 -4
View File
@@ -7,9 +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);
@@ -44,6 +49,7 @@ test('real dev and built-preview servers serve provider JSON and terminate unkno
TOMTOM_API_KEY: '',
GOOGLE_MAPS_API_KEY: '',
GOOGLE_MAPS_SERVER_API_KEY: '',
MAPILLARY_CLIENT_TOKEN: '',
OPENAI_API_KEY: '',
OPENSKY_AUTH_MODE: 'anon',
OPENSKY_CLIENT_ID: '',
@@ -86,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 },
};
@@ -97,17 +106,23 @@ test('real dev and built-preview servers serve provider JSON and terminate unkno
const origin = `http://127.0.0.1:${server.httpServer.address().port}`;
try {
for (const [route, status] of [
['/api/opensky', 200],
['/api/adsblol/mil', 200],
['/api/flights', 200],
['/api/military', 200],
['/api/adsbdb/type/invalid', 400],
['/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],
['/api/cctv/sources', 200],
['/api/gbfs/', 400],
['/api/tomtom/status', 200],
// The Street Level QA gate intercepts these in the browser; this is
// the check that the real servers register them.
['/api/mapillary/status', 200],
['/api/mapillary/tiles/coverage/14/1/2', 503],
['/api/radio/unknown', 404],
['/api/setup/status', isPreview ? 404 : 200],
['/api/setup/update', 404],
@@ -128,6 +143,10 @@ test('real dev and built-preview servers serve provider JSON and terminate unkno
const body = await response.json();
if (route === '/api/cctv/sources')
assert.equal(body.sources[0].id, 'fixture');
if (route === '/api/mapillary/status')
assert.deepEqual(body, { configured: false }, route);
if (route.startsWith('/api/mapillary/tiles'))
assert.deepEqual(body, { error: 'no_key', keyRequired: true }, route);
if (
route === '/api/does-not-exist' ||
(isPreview && route.startsWith('/api/setup'))
+64
View File
@@ -0,0 +1,64 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import sharp from 'sharp';
import {
CUBE_FACES,
FACE_FILE_SUFFIX,
PLANETS,
cubeFaceDirection,
icrfToGalactic,
} from '../../scripts/build-milky-way.mjs';
import { raDecToUnitVector } from '../sky/stars.js';
const SKY = new URL('../data/local_data/sky/', import.meta.url);
// Allowed excess of the painted disc over the surrounding sky, in 8-bit
// levels of the encoded face: the blur and dither move values by a few.
const TOLERANCE = 4;
test('painted-out planets leave no disc brighter than the sky around them', async () => {
const centres = PLANETS.map((planet) => ({
planet,
vector: raDecToUnitVector(planet.l, planet.b),
disc: [],
ring: [],
}));
for (const face of CUBE_FACES) {
const { data, info } = await sharp(
new URL(`milkyway_${FACE_FILE_SUFFIX[face]}.webp`, SKY).pathname,
)
.raw()
.toBuffer({ resolveWithObject: true });
const size = info.width;
for (let j = 0; j < size; j++) {
for (let i = 0; i < size; i++) {
const d = cubeFaceDirection(face, (i + 0.5) / size, (j + 0.5) / size);
const n = Math.hypot(...d);
const g = icrfToGalactic(d.map((x) => x / n));
for (const centre of centres) {
const cos =
g[0] * centre.vector[0] +
g[1] * centre.vector[1] +
g[2] * centre.vector[2];
const degrees = (Math.acos(Math.min(1, cos)) * 180) / Math.PI;
const k = (j * size + i) * info.channels;
const luma = (data[k] + data[k + 1] + data[k + 2]) / 3;
if (degrees <= centre.planet.radiusDeg - 0.5) centre.disc.push(luma);
else if (
degrees >= centre.planet.radiusDeg + 1 &&
degrees <= centre.planet.radiusDeg + 3.5
)
centre.ring.push(luma);
}
}
}
}
const median = (values) => values.sort((a, b) => a - b)[values.length >> 1];
for (const { planet, disc, ring } of centres) {
const discMean = disc.reduce((s, v) => s + v, 0) / disc.length;
const ringMedian = median(ring);
assert.ok(
discMean <= ringMedian + TOLERANCE,
`l=${planet.l} b=${planet.b}: disc mean ${discMean.toFixed(1)} vs surrounding sky ${ringMedian.toFixed(1)}`,
);
}
});
+132
View File
@@ -0,0 +1,132 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
buildStarPack,
parseBscRecord,
} from '../../scripts/build-bright-stars.mjs';
import {
CUBE_FACES,
cubeFaceDirection,
directionToPlateCarree,
icrfToGalactic,
smoothstep,
softBlack,
} from '../../scripts/build-milky-way.mjs';
import { raDecToUnitVector } from '../sky/stars.js';
// Verbatim CDS V/50 records: HR 1, Sirius (HR 2491) and a nova with no J2000
// position. Columns matter, so the padding is part of the fixture.
const HR_1 =
' 1 BD+44 4550 3 36042 46 000001.1+444022000509.9+451345114.44-16.88 6.70 +0.07 +0.08 A1Vn -0.012-0.018 -018 195 4.2 21.6AC 3';
const SIRIUS =
'2491 9Alp CMaBD-16 1591 48915151881 257I 5423 064044.6-163444064508.9-164258227.22-08.88-1.46 0.00 -0.05 -0.03 A1Vm -0.553-1.205 +.375-008SBO 13 10.3 11.2AB 4*';
const NOVA =
' 92 NOVA 1572 B Cas *';
const close = (actual, expected, epsilon = 1e-9) =>
assert.ok(
Math.abs(actual - expected) <= epsilon,
`${actual} is not within ${epsilon} of ${expected}`,
);
test('BSC5 records parse J2000 position, V magnitude and B−V', () => {
const sirius = parseBscRecord(SIRIUS);
assert.equal(sirius.hr, 2491);
close(sirius.raDeg, (6 + 45 / 60 + 8.9 / 3600) * 15);
close(sirius.decDeg, -(16 + 42 / 60 + 58 / 3600));
assert.equal(sirius.vmag, -1.46);
assert.equal(sirius.bv, 0);
const first = parseBscRecord(HR_1);
close(first.raDeg, (5 / 60 + 9.9 / 3600) * 15);
close(first.decDeg, 45 + 13 / 60 + 45 / 3600);
assert.equal(first.vmag, 6.7);
assert.equal(first.bv, 0.07);
assert.equal(parseBscRecord(NOVA), null);
});
test('the star pack is a flat, rounded column array that counts its drops', () => {
const pack = buildStarPack([HR_1, NOVA, SIRIUS, ''].join('\n'), {
fetched: '2026-10-19',
});
assert.deepEqual(
pack.stars,
[1.291, 45.229, 6.7, 0.07, 101.287, -16.716, -1.46, 0],
);
assert.equal(pack.meta.count, 2);
assert.equal(pack.meta.dropped, 1);
assert.equal(pack.meta.fetched, '2026-10-19');
});
test('cube faces use the WebGL major-axis layout', () => {
// Each face centre looks down its own axis …
const axes = {
positiveX: [1, 0, 0],
negativeX: [-1, 0, 0],
positiveY: [0, 1, 0],
negativeY: [0, -1, 0],
positiveZ: [0, 0, 1],
negativeZ: [0, 0, -1],
};
for (const face of CUBE_FACES)
assert.deepEqual(
cubeFaceDirection(face, 0.5, 0.5).map((v) => v + 0),
axes[face],
);
// … and the first texel row and column follow OpenGL ES 3.0 §3.8.10:
// on +X, s grows towards −Z and t towards −Y.
assert.deepEqual(cubeFaceDirection('positiveX', 0, 0), [1, 1, 1]);
assert.deepEqual(cubeFaceDirection('positiveZ', 1, 1), [1, -1, 1]);
assert.deepEqual(cubeFaceDirection('negativeY', 0, 1), [-1, -1, -1]);
assert.throws(() => cubeFaceDirection('up', 0, 0), RangeError);
});
test('plate carrée is centred on longitude 0, growing to the left, north up', () => {
assert.deepEqual(directionToPlateCarree(1, 0, 0), [0.5, 0.5]);
const sixHours = directionToPlateCarree(0, 1, 0);
close(sixHours[0], 0.25);
const eighteenHours = directionToPlateCarree(0, -1, 0);
close(eighteenHours[0], 0.75);
close(directionToPlateCarree(0, 0, 1)[1], 0);
close(directionToPlateCarree(0, 0, -1)[1], 1);
const twelveHours = directionToPlateCarree(-1, 0, 0);
assert.ok(twelveHours[0] === 0 || twelveHours[0] === 1);
});
test('the galactic rotation puts the Galactic Centre and pole where the IAU does', () => {
// Galactic origin l = b = 0 at RA 266.405°, Dec −28.936° (J2000).
const centre = icrfToGalactic(raDecToUnitVector(266.40499, -28.93617));
close(centre[0], 1, 1e-6);
// North galactic pole at RA 192.85948°, Dec +27.12825°.
const pole = icrfToGalactic(raDecToUnitVector(192.85948, 27.12825));
close(pole[2], 1, 1e-6);
// ESO's panorama: the Large Magellanic Cloud (l ≈ 280.5°, b ≈ −32.9°) sits
// right of and below the centre.
const [u, v] = directionToPlateCarree(
...icrfToGalactic(raDecToUnitVector(80.894, -69.756)),
);
close(u, 0.5 + (360 - 280.47) / 360, 2e-3);
close(v, 0.5 + 32.89 / 180, 2e-3);
});
test('the black level eases in instead of cutting the faint sky into contours', () => {
assert.equal(softBlack(-0.2), 0);
assert.equal(softBlack(0), 0);
// Continuous, rising, with no step at the black level.
let previous = 0;
for (let x = 0.001; x <= 1; x += 0.001) {
const y = softBlack(x);
assert.ok(y > previous && y - previous < 0.0011, `step at ${x.toFixed(3)}`);
previous = y;
}
// Starts flat, then joins the straight line just below it.
assert.ok(softBlack(0.003) < 0.0003);
close(softBlack(1), 1 - 0.03, 1e-3);
});
test('the faint-glow blend weight eases between its edges', () => {
assert.equal(smoothstep(0.1, 0.2, 0), 0);
assert.equal(smoothstep(0.1, 0.2, 0.1), 0);
close(smoothstep(0.1, 0.2, 0.15), 0.5, 1e-9);
assert.equal(smoothstep(0.1, 0.2, 0.2), 1);
assert.equal(smoothstep(0.1, 0.2, 9), 1);
});
+250 -4
View File
@@ -12,6 +12,7 @@ import {
launchLibraryRecentUrl,
} from 'gods-eye-view/sources/space';
import * as compatibility from '../../server/providers/local.js';
import { createLaunchSource } from 'gods-eye-view/layers/launches/source';
function install(plugin, preview = false) {
const routes = new Map();
@@ -22,17 +23,23 @@ function install(plugin, preview = false) {
},
},
});
return async (route, url = '/', method = 'GET') => {
return async (route, url = '/', method = 'GET', headers = {}) => {
const res = {
headersSent: false,
headers: {},
writeHead(status, headers) {
Object.assign(this, { status, headers, headersSent: true });
},
// The same-site gates answer 403 through statusCode + setHeader.
setHeader(name, value) {
this.headers[name] = value;
},
end(body) {
this.body = body;
this.status ??= this.statusCode;
},
};
await routes.get(route)({ url, method }, res);
await routes.get(route)({ url, method, headers, socket: {} }, res);
return res;
};
}
@@ -47,6 +54,29 @@ function isolateDisk(t) {
t.mock.method(fsp, 'writeFile', async () => {});
}
/** A response that streams 1 MiB chunks of `text` well past every cap, then ends. */
function oversizedResponse(text) {
const state = { pulled: 0, cancelled: false };
const chunk = new TextEncoder().encode(
text.repeat(Math.ceil((1 << 20) / text.length)),
);
const response = new Response(
new ReadableStream(
{
pull(controller) {
if (++state.pulled > 40) controller.close();
else controller.enqueue(chunk);
},
cancel() {
state.cancelled = true;
},
},
{ highWaterMark: 0 },
),
);
return { response, state };
}
test('portable requests keep fixed origins, encode group data and preserve the 30-day UTC window', () => {
const tle = celestrakTleUrl('stations&FORMAT=json');
assert.equal(tle.origin, 'https://celestrak.org');
@@ -58,8 +88,8 @@ test('portable requests keep fixed origins, encode group data and preserve the 3
assert.equal(url.origin, 'https://ll.thespacedevs.com');
assert.equal(url.pathname, '/2.3.0/launches/');
assert.deepEqual(Object.fromEntries(url.searchParams), {
net__gte: '2026-01-30T12:34:56.000Z',
net__lte: '2026-03-01T12:34:56.000Z',
net__gte: '2026-01-30T12:34:00.000Z',
net__lte: '2026-03-01T12:34:00.000Z',
limit: '100',
mode: 'detailed',
});
@@ -120,6 +150,31 @@ test('exported CelesTrak plugin coalesces refreshes, retains stale TLEs, and rea
assert.equal(disk.body, tle);
});
test('CelesTrak answers 502 to an oversized TLE body without caching it', async (t) => {
isolateDisk(t);
t.mock.method(console, 'warn', () => {});
const bodies = [];
t.mock.method(globalThis, 'fetch', async () => {
const oversized = oversizedResponse(
'ISS\n1 25544U fixture\n2 25544 fixture\n',
);
bodies.push(oversized.state);
return oversized.response;
});
const request = install(celestrakProxy());
const first = await request('/api/celestrak', '/stations');
assert.equal(first.status, 502);
assert.equal(bodies[0].cancelled, true);
assert.ok(bodies[0].pulled <= 17, 'reading stops at the 16 MiB cap');
assert.equal(fsp.writeFile.mock.callCount(), 0);
assert.equal((await request('/api/celestrak', '/stations')).status, 502);
assert.equal(
bodies.length,
1,
'failed bodies are not cached but a cooldown bounds retry work',
);
});
for (const preview of [false, true])
test(`exported launch plugin preserves optional server auth and cache in ${preview ? 'preview' : 'development'}`, async (t) => {
isolateDisk(t);
@@ -146,3 +201,194 @@ for (const preview of [false, true])
assert.equal(hit.body, first.body);
assert.equal(calls, 1);
});
test('space providers honor Retry-After, gate groups, preserve stale dates and recover', async (t) => {
isolateDisk(t);
t.mock.method(console, 'warn', () => {});
let now = Date.now();
t.mock.method(Date, 'now', () => now);
for (const kind of ['tle', 'launches']) {
for (const retryAfter of ['90', new Date(now + 90000).toUTCString()]) {
let calls = 0;
let fail = false;
let cancelled = false;
t.mock.method(globalThis, 'fetch', async () => {
calls++;
if (fail)
return new Response(
new ReadableStream({
cancel() {
cancelled = true;
},
}),
{
status: kind === 'tle' ? 403 : 429,
headers: { 'Retry-After': retryAfter },
},
);
return new Response(
kind === 'tle'
? 'ISS\n1 25544U fixture\n2 25544 fixture\n'
: '{"results":[]}',
);
});
const request = install(
kind === 'tle' ? celestrakProxy() : rocketLaunchesProxy(),
);
const route = kind === 'tle' ? '/api/celestrak' : '/api/launches';
const url = kind === 'tle' ? '/stations' : '/';
if (kind === 'tle') {
assert.equal(
(await request(route, '/not-a-supported-group')).status,
400,
);
assert.equal(calls, 0);
}
const first = await request(route, url);
now += 7 * 3600000;
// Recompute the HTTP-date relative to the clock at the failed refresh.
const originalFetch = globalThis.fetch;
t.mock.method(globalThis, 'fetch', async (...args) => {
const response = await originalFetch(...args);
if (fail && !/^\d+$/.test(retryAfter))
response.headers.set(
'Retry-After',
new Date(now + 90000).toUTCString(),
);
return response;
});
fail = true;
const stale = await request(route, url);
assert.equal(stale.status, 200);
assert.equal(stale.body, first.body);
assert.equal(stale.headers['Retry-After'], '90');
assert.ok(
stale.headers[kind === 'tle' ? 'X-TLE-Cached-At' : 'X-GEV-Cached-At'],
);
assert.equal(cancelled, true);
await request(route, url);
if (kind === 'tle')
assert.equal((await request(route, '/visual')).status, 502);
assert.equal(calls, 2, 'cooldown applies across TLE groups');
now += 91000;
fail = false;
assert.equal((await request(route, url)).status, 200);
assert.equal(calls, 3);
}
}
});
test('Space Missions reads the active catalog through the real proxy while unknown groups stay rejected', async (t) => {
isolateDisk(t);
const tle = 'ISS\n1 25544U fixture\n2 25544 fixture\n';
const fetched = [];
t.mock.method(globalThis, 'fetch', async (url) => {
fetched.push(new URL(url).searchParams.get('GROUP'));
return new Response(tle);
});
const request = install(celestrakProxy());
// Route the browser source's real request into the middleware.
const source = createLaunchSource({
fetchImpl: async (url) => {
const prefix = '/api/celestrak';
assert.ok(url.startsWith(`${prefix}/`));
const res = await request(prefix, url.slice(prefix.length));
return new Response(res.body, { status: res.status });
},
});
assert.equal(await source.getActiveTle(), tle);
assert.deepEqual(fetched, ['active']);
for (const group of ['/not-a-group', '/active-extra', '/../active'])
assert.equal((await request('/api/celestrak', group)).status, 400);
assert.deepEqual(fetched, ['active'], 'rejected groups never reach upstream');
});
test('CelesTrak admits a same-origin request behind the documented reverse proxy and refuses foreign origins', async (t) => {
isolateDisk(t);
let calls = 0;
t.mock.method(globalThis, 'fetch', async () => {
calls++;
return new Response('ISS\n1 25544U fixture\n2 25544 fixture\n');
});
const request = install(celestrakProxy());
const proxied = {
host: 'gev.example.test',
'x-forwarded-for': '203.0.113.7',
'x-forwarded-host': 'gev.example.test',
'x-forwarded-proto': 'https',
via: '1.1 proxy',
};
const app = await request('/api/celestrak', '/stations', 'GET', {
...proxied,
origin: 'https://gev.example.test',
'sec-fetch-site': 'same-origin',
});
assert.equal(app.status, 200);
assert.equal(app.headers['x-tle-cache'], 'MISS');
assert.equal(calls, 1);
for (const headers of [
{ ...proxied, origin: 'https://unrelated.example' },
{ ...proxied, origin: 'null' },
{ ...proxied, 'sec-fetch-site': 'cross-site' },
]) {
const refused = await request('/api/celestrak', '/visual', 'GET', headers);
assert.equal(refused.status, 403);
}
assert.equal(calls, 1, 'refused requests do no provider or cache work');
});
for (const longFirst of [true, false])
test(`CelesTrak keeps the longest cooldown when concurrent failures finish ${longFirst ? 'long then short' : 'short then long'}`, async (t) => {
isolateDisk(t);
t.mock.method(console, 'warn', () => {});
let now = Date.now();
t.mock.method(Date, 'now', () => now);
const pending = new Map();
let calls = 0;
t.mock.method(globalThis, 'fetch', async (url) => {
calls++;
const group = new URL(url).searchParams.get('GROUP');
await new Promise((resolve) => pending.set(group, resolve));
return group === 'stations'
? new Response('slow down', {
status: 429,
headers: { 'Retry-After': '1800' },
})
: new Response('unavailable', { status: 503 });
});
const request = install(celestrakProxy());
const long = request('/api/celestrak', '/stations');
const short = request('/api/celestrak', '/visual');
while (pending.size < 2) await new Promise(setImmediate);
const order = longFirst
? [
['stations', long],
['visual', short],
]
: [
['visual', short],
['stations', long],
];
for (const [group, response] of order) {
pending.get(group)();
assert.equal((await response).status, 502);
}
assert.equal(calls, 2);
const cooling = await request('/api/celestrak', '/geo');
assert.equal(cooling.status, 502);
assert.equal(cooling.headers['Retry-After'], '1800');
now += 61_000;
assert.equal((await request('/api/celestrak', '/geo')).status, 502);
assert.equal(
calls,
2,
'the 60 s fallback did not replace the 30 min deadline',
);
now += 1800_000;
t.mock.method(
globalThis,
'fetch',
async () => new Response('ISS\n1 25544U fixture\n2 25544 fixture\n'),
);
assert.equal((await request('/api/celestrak', '/geo')).status, 200);
});
+409
View File
@@ -0,0 +1,409 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { inflateSync } from 'node:zlib';
import {
answerMapillaryRequest,
describeCall,
PHOTO_SEQUENCE_ID,
photoImages,
THUMB_HOST,
} from '../../scripts/fixtures/street-level/mapillaryGraph.mjs';
import { redact } from '../../scripts/qa-browserEvidence.mjs';
import {
answerKartaViewRequest,
describeKartaViewCall,
KV_LINE,
KV_OLD_STREET,
KV_SEQUENCE_ID,
kartaViewPhotos,
resetKartaViewFixture,
} from '../../scripts/fixtures/street-level/kartaView.mjs';
import {
maskedSegments,
tileSegments,
} from '../layers/streetLevel/providers/kartaview/decode.js';
import { lonToTileX, latToTileY } from '../layers/streetLevel/tileMath.js';
import {
normalizePhoto,
resizedImageUrl,
} from '../layers/streetLevel/providers/kartaview/policy.js';
const graph = (path, params) =>
`https://graph.mapillary.com/${path}?${new URLSearchParams({ access_token: 'MLY|0|qa-fixture', ...params })}`;
const answer = (url, method = 'GET') => {
const out = answerMapillaryRequest({ method, url });
return {
...out,
json: out.contentType === 'application/json' ? JSON.parse(out.body) : null,
};
};
test('the Graph fixtures answer every call MapillaryJS makes to open a photo', () => {
const [first, second] = photoImages();
const spatial = answer(
graph('images', {
image_ids: `${first.id},${second.id}`,
fields:
'id,computed_geometry,geometry,sequence,camera_type,computed_rotation,thumb_1024_url,thumb_2048_url,width,height,merge_cc,sfm_cluster,mesh,creator,captured_at',
}),
);
assert.equal(spatial.status, 200);
assert.equal(spatial.headers['Access-Control-Allow-Origin'], '*');
const [pano, flat] = spatial.json.data;
assert.equal(pano.camera_type, 'spherical');
assert.equal(flat.camera_type, 'perspective');
assert.equal(pano.merge_cc, null, 'unmerged: no mesh request');
assert.equal(pano.sfm_cluster, null, 'no cluster request');
assert.equal(pano.computed_rotation.length, 3);
assert.equal(new URL(pano.thumb_2048_url).hostname, THUMB_HOST);
assert.equal(
answer(graph('images', { s2: '9749618446378729472', fields: 'id' })).json
.data.length,
0,
);
assert.deepEqual(
answer(graph('image_ids', { sequence_id: PHOTO_SEQUENCE_ID })).json.data[0],
{ id: first.id },
);
assert.deepEqual(
answer(graph(`${first.id}/tiles`, { z: '11', fields: 'url,z,x,y' })).json,
{ data: [] },
);
const photo = answer(pano.thumb_2048_url);
assert.equal(photo.status, 200);
assert.equal(photo.contentType, 'image/jpeg');
assert.deepEqual([...photo.body.subarray(0, 3)], [0xff, 0xd8, 0xff]);
const preflight = answer(graph('images', { image_ids: first.id }), 'OPTIONS');
assert.equal(preflight.status, 204);
assert.match(
preflight.headers['Access-Control-Allow-Headers'],
/Authorization/,
);
});
test('a Mapillary call no fixture covers is answered but flagged unknown', () => {
for (const url of [
graph('images', { bbox: '0,0,1,1' }),
graph('map_features', { fields: 'id' }),
'https://tiles.mapillary.com/maps/vtp/mly1_public/2/14/1/1',
]) {
const out = answer(url);
assert.equal(out.known, false, url);
assert.ok(out.status >= 400);
}
});
test('answered calls are named without the token', () => {
const call = describeCall(
'GET',
new URL(graph('9100000000000/tiles', { z: '11', fields: 'url' })),
);
assert.equal(call, 'GET graph.mapillary.com/{imageId}/tiles?fields&z');
assert.doesNotMatch(call, /MLY|access_token/);
assert.equal(
describeCall(
'GET',
new URL('https://qa-fixture.mapillary.com/thumb/9100000000016.jpg'),
),
'GET qa-fixture.mapillary.com/thumb/{imageId}.jpg',
);
});
test('evidence never carries a Mapillary token', () => {
assert.equal(
redact(
'GET https://graph.mapillary.com/images?access_token=MLY|123|abc&fields=id',
),
'GET https://graph.mapillary.com/images?access_token=REDACTED&fields=id',
);
assert.equal(
redact('token MLY|123|abc in a log'),
'token MLY|REDACTED in a log',
);
assert.equal(
redact(
'https://tile.googleapis.com/v1/3dtiles/x.glb?session=s1&key=AIzaSyBexampleexampleexampleexample00',
),
'https://tile.googleapis.com/v1/3dtiles/x.glb?session=s1&key=REDACTED',
);
assert.equal(
redact('loaded with AIzaSyBexampleexampleexampleexample00 inline'),
'loaded with AIza…REDACTED inline',
);
});
test('KartaView fixtures draw the drive, answer nearest first, and refuse the storage host the API names', () => {
const body = (answer) => JSON.parse(answer.body);
const lat = (KV_LINE.south + KV_LINE.north) / 2;
const x = lonToTileX(KV_LINE.lon, 14);
const y = latToTileY(lat, 14);
const tile = answerKartaViewRequest({
method: 'GET',
url: `https://api.kartaview.org/2.0/sequence/tiles/${x}/${y}/14.json`,
});
assert.equal(tile.known, true);
assert.ok(body(tile).lines.length >= 1);
const empty = answerKartaViewRequest({
method: 'GET',
url: `https://api.kartaview.org/2.0/sequence/tiles/${x + 3}/${y}/14.json`,
});
assert.deepEqual(body(empty).lines, []);
const nearUrl = `https://api.kartaview.org/2.0/photo/?lat=${lat}&lng=${KV_LINE.lon + 0.0001}&zoomLevel=18&itemsPerPage=10`;
resetKartaViewFixture();
// The first lookup is rate-limited, readable cross-origin; the retry answers.
const limited = answerKartaViewRequest({ method: 'GET', url: nearUrl });
assert.equal(limited.status, 429);
assert.equal(limited.known, true);
assert.equal(limited.headers['Retry-After'], '0');
assert.match(limited.headers['Access-Control-Expose-Headers'], /Retry-After/);
const near = answerKartaViewRequest({ method: 'GET', url: nearUrl });
assert.equal(near.status, 200);
const records = body(near).result.data;
assert.ok(records.length > 1);
assert.ok(Number(records[0].distance) <= Number(records[1].distance));
const photo = normalizePhoto(records[0]);
assert.match(photo.urls.view, /^https:\/\/storage2\.openstreetcam\.org\//);
assert.equal(
answerKartaViewRequest({ method: 'GET', url: photo.urls.view }).known,
true,
);
// Every odd photo is archived: its file answers 409, an even one the image.
const photos = kartaViewPhotos();
const storageAnswer = (index) =>
answerKartaViewRequest({
method: 'GET',
url: normalizePhoto(
body(
answerKartaViewRequest({
method: 'GET',
url: `https://api.kartaview.org/2.0/photo/${photos[index].id}`,
}),
).result.data,
).urls.view,
});
assert.equal(photos[3].archived, true);
assert.equal(storageAnswer(3).status, 409);
assert.equal(storageAnswer(3).known, true);
assert.equal(photos[4].archived, false);
assert.equal(storageAnswer(4).status, 200);
assert.equal(storageAnswer(4).contentType, 'image/jpeg');
// The image proxy still serves an archived photo: the 360° step opens one.
const pano = photos.find((p) => p.isPano);
assert.equal(pano.archived, true);
const panoRecord = body(
answerKartaViewRequest({
method: 'GET',
url: `https://api.kartaview.org/2.0/photo/${pano.id}`,
}),
).result.data;
assert.equal(
answerKartaViewRequest({
method: 'GET',
url: resizedImageUrl(normalizePhoto(panoRecord).urls.pano, [1024, 512]),
}).status,
200,
);
assert.equal(
answerKartaViewRequest({ method: 'GET', url: records[0].fileurlLTh }).known,
false,
'the storage1 address the API gives is not where the photo is',
);
const page = answerKartaViewRequest({
method: 'GET',
url: `https://api.kartaview.org/2.0/sequence/${KV_SEQUENCE_ID}/photos?page=1&itemsPerPage=1000`,
});
assert.equal(body(page).result.data.length, kartaViewPhotos().length);
assert.ok(kartaViewPhotos().some((p) => p.isPano));
assert.equal(
describeKartaViewCall(
'GET',
new URL('https://api.kartaview.org/2.0/photo/77000001'),
),
'GET api.kartaview.org/2.0/photo/{id}',
);
});
test('Panoramax fixtures: two sequences on one street, searches honour sequence and filters, unknown calls are flagged', async () => {
const {
answerPanoramaxRequest,
describePanoramaxCall,
PNX_LINE,
PNX_SEQUENCES,
panoramaxHdDownloads,
panoramaxPictures,
panoramaxTile,
resetPanoramaxFixture,
} = await import('../../scripts/fixtures/street-level/panoramax.mjs');
const { decodeCoverageTile, decodePictures } =
await import('../layers/streetLevel/providers/panoramax/decode.js');
const { normalizeItem } =
await import('../layers/streetLevel/providers/panoramax/policy.js');
resetPanoramaxFixture();
const pictures = panoramaxPictures();
assert.deepEqual(
[...new Set(pictures.map((p) => p.sequenceId))],
[...PNX_SEQUENCES],
);
assert.ok(pictures.some((p) => p.isPano));
// Coverage: both lines in a z14 tile on the street; cones in z15.
const z = 14;
const x = lonToTileX((PNX_LINE.west + PNX_LINE.east) / 2, z);
const y = latToTileY(PNX_LINE.lat, z);
const lines = decodeCoverageTile(panoramaxTile(z, x, y), { x, y, z });
assert.deepEqual(
lines.sequences.map((s) => s.id).sort(),
[...PNX_SEQUENCES].sort(),
);
const t15 = {
x: lonToTileX(pictures[20].lon, 15),
y: latToTileY(PNX_LINE.lat, 15),
z: 15,
};
assert.ok(
decodePictures(panoramaxTile(15, t15.x, t15.y), t15, PNX_SEQUENCES[0])
.length > 0,
);
// A search for one sequence near the street gives only its pictures, nearest first.
const at = pictures[30];
const ask = (query) =>
JSON.parse(
answerPanoramaxRequest({
method: 'GET',
url: `https://api.panoramax.xyz/api/search?${query}`,
}).body,
).features.map(normalizeItem);
const own = ask(
`place_position=${at.lon},${at.lat}&place_distance=0-50&limit=5&collections=${PNX_SEQUENCES[1]}`,
);
assert.ok(own.length > 0);
assert.ok(own.every((p) => p.sequenceId === PNX_SEQUENCES[1]));
assert.equal(own[0].nextId && typeof own[0].nextId, 'string');
const panos = ask(
`place_position=${at.lon},${at.lat}&place_distance=0-200&limit=50&filter=field_of_view%3D360`,
);
assert.ok(panos.length > 0 && panos.every((p) => p.isPano));
assert.deepEqual(
ask(
`place_position=${at.lon},${at.lat}&place_distance=0-200&limit=50&datetime=2024-01-01T00:00:00.000Z/..`,
).every((p) => p.sequenceId === PNX_SEQUENCES[0]),
true,
'the second sequence is older',
);
// A box has every picture in it, those looking away included; a place
// search leaves out pictures the point is behind (all look east).
const d = 0.0002;
const boxed = ask(
`bbox=${at.lon - d},${at.lat - d},${at.lon + d},${at.lat + d}&limit=100`,
);
assert.ok(
boxed.some((p) => p.lon < at.lon) && boxed.some((p) => p.lon > at.lon),
);
const placed = ask(
`place_position=${at.lon},${at.lat}&place_distance=0-30&limit=50`,
);
assert.ok(placed.length > 0);
assert.ok(
placed.every((p) => p.lon <= at.lon + 1e-7),
'none it is behind',
);
const one = ask(`ids=${at.id}&limit=1`);
assert.equal(one[0].id, at.id);
// Its pictures, and only those, are served; HD downloads are counted.
const file = (name) =>
answerPanoramaxRequest({
method: 'GET',
url: `https://panoramax.openstreetmap.fr/derivates/${name}`,
});
assert.equal(file(`${at.id}/sd.jpg`).status, 200);
assert.equal(file(`${at.id}/hd.jpg`).status, 200);
assert.equal(panoramaxHdDownloads(), 1);
assert.equal(file('nope/sd.jpg').known, false);
const unknown = answerPanoramaxRequest({
method: 'GET',
url: 'https://api.panoramax.xyz/api/collections/x/items',
});
assert.equal(unknown.known, false);
assert.equal(
describePanoramaxCall(
'GET',
new URL('https://api.panoramax.xyz/api/map/14/1/2.mvt'),
),
'GET api.panoramax.xyz/api/map/{z}/{x}/{y}.mvt',
);
});
/** The alpha of a fixture PNG (8-bit RGBA, unfiltered rows, as it writes them). */
function fixturePngAlpha(bytes) {
const width = bytes.readUInt32BE(16);
const height = bytes.readUInt32BE(20);
const idat = [];
for (let at = 8; at < bytes.length;) {
const length = bytes.readUInt32BE(at);
const type = bytes.toString('ascii', at + 4, at + 8);
if (type === 'IDAT') idat.push(bytes.subarray(at + 8, at + 8 + length));
at += 12 + length;
}
const rows = inflateSync(Buffer.concat(idat));
const alpha = new Uint8Array(width * height);
for (let y = 0; y < height; y++)
for (let x = 0; x < width; x++)
alpha[y * width + x] = rows[y * (width * 4 + 1) + 1 + x * 4 + 3];
return { width, height, alpha };
}
test('KartaView fixtures filter like KartaView: PNG tiles by view and upload date, the photo search by projection', () => {
const body = (answer) => JSON.parse(answer.body);
const z = 14;
const x = lonToTileX(KV_LINE.lon, z);
const y = latToTileY((KV_LINE.south + KV_LINE.north) / 2, z);
assert.equal(x, lonToTileX(KV_OLD_STREET.lon, z), 'both streets in a tile');
const base = `https://api.kartaview.org/2.0/sequence/tiles/${x}/${y}/${z}`;
const segments = tileSegments(
body(answerKartaViewRequest({ method: 'GET', url: `${base}.json` })),
);
const lons = (kept) => [...new Set(kept.map((segment) => segment[0][0]))];
const keptFor = (query) => {
const answer = answerKartaViewRequest({
method: 'GET',
url: `${base}.png?${query}`,
});
assert.equal(answer.contentType, 'image/png');
assert.equal(answer.known, true);
return lons(
maskedSegments(segments, fixturePngAlpha(answer.body), { x, y, z }),
);
};
assert.deepEqual(
lons(segments).sort(),
[KV_LINE.lon, KV_OLD_STREET.lon].sort(),
);
assert.deepEqual(keptFor('fieldOfView=360'), [KV_LINE.lon]);
assert.deepEqual(keptFor('fieldOfView=plane').sort(), lons(segments).sort());
const lastYear = new Date(Date.now() - 365 * 86_400_000)
.toISOString()
.slice(0, 10);
assert.deepEqual(keptFor(`startDate=${lastYear}`), [KV_LINE.lon]);
assert.deepEqual(keptFor('startDate=2099-01-01'), []);
assert.equal(
describeKartaViewCall('GET', new URL(`${base}.png?fieldOfView=360`)),
'GET api.kartaview.org/2.0/sequence/tiles/{x}/{y}/{z}.png?fieldOfView',
);
// The photo search: 360° only, and nothing uploaded after the drive.
resetKartaViewFixture();
const search = (extra) => {
answerKartaViewRequest({
method: 'GET',
url: 'https://api.kartaview.org/2.0/photo/?lat=0&lng=0',
});
return body(
answerKartaViewRequest({
method: 'GET',
url: `https://api.kartaview.org/2.0/photo/?lat=${KV_LINE.south + 0.002}&lng=${KV_LINE.lon}&zoomLevel=18&itemsPerPage=10${extra}`,
}),
).result.data;
};
const panos = search('&projection=SPHERE');
assert.ok(panos.length > 0);
assert.ok(panos.every((record) => record.projection === 'SPHERE'));
assert.deepEqual(search('&startDate=2099-01-01'), []);
});
+100
View File
@@ -0,0 +1,100 @@
/** A reveal must beat the one-second idle rotation refresh, regardless of FPS. */
export const ARRIVAL_OBSERVATION_MS = 500;
/**
* Observe the first completed moveEnd frame and the first subsequent reveal.
* The deadline starts when moveEnd is forced, before its rotation baseline, so
* waiting for either frame cannot let the one-second idle refresh mask a bug.
* `read` snapshots visibility and rotation error against the current position.
*/
export function observeArrivalRotation({
scene,
startMoveEnd,
startReveal,
read,
now = () => performance.now(),
setTimer = setTimeout,
clearTimer = clearTimeout,
}) {
return new Promise((resolve, reject) => {
const startedAt = now();
let moveEnd = null;
let moveEndMs = null;
let revealFrames = 0;
let done = false;
let remove;
let timer;
const cleanup = () => {
done = true;
remove?.();
clearTimer(timer);
};
const finish = (result) => {
if (done) return;
cleanup();
resolve(result);
};
const expired = () => ({
error: `arrival ${moveEnd ? 'reveal' : 'moveEnd'} observation exceeded ${ARRIVAL_OBSERVATION_MS} ms`,
elapsedMs: now() - startedAt,
moveEndMs,
flipFrame: -1,
});
remove = scene.postRender.addEventListener(() => {
if (done) return;
try {
const elapsedMs = now() - startedAt;
if (elapsedMs >= ARRIVAL_OBSERVATION_MS) {
finish(expired());
return;
}
const sample = read();
if (moveEnd === null) {
moveEnd = sample;
moveEndMs = elapsedMs;
startReveal();
} else {
revealFrames++;
if (sample.shown) {
// Keep the FIRST visible result, including a wrong angle. Never
// wait for a subsequent tick to repair a failed reveal.
finish({
dMoveEnd: moveEnd.rotationError,
dReveal: sample.rotationError,
shown: sample.shown,
flipFrame: revealFrames,
moveEndMs,
revealMs: elapsedMs - moveEndMs,
elapsedMs,
});
return;
}
}
scene.requestRender();
} catch (error) {
cleanup();
reject(error);
}
});
timer = setTimer(() => finish(expired()), ARRIVAL_OBSERVATION_MS);
try {
startMoveEnd();
scene.requestRender();
} catch (error) {
cleanup();
reject(error);
}
});
}
/** Test the captured frames without retrying a bad first observation. */
export function arrivalRotationPassed(observation) {
return (
!!observation &&
!observation.error &&
observation.shown === true &&
observation.elapsedMs < ARRIVAL_OBSERVATION_MS &&
(observation.dMoveEnd * 180) / Math.PI < 10 &&
(observation.dReveal * 180) / Math.PI < 10
);
}
@@ -0,0 +1,151 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
ARRIVAL_OBSERVATION_MS,
arrivalRotationPassed,
observeArrivalRotation,
} from './trackArrivalObservation.mjs';
function fixture() {
let time = 0;
let state = { shown: true, rotationError: 0 };
const listeners = new Set();
const timers = new Map();
let renders = 0;
let moveEnds = 0;
const observation = observeArrivalRotation({
scene: {
postRender: {
addEventListener(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
},
requestRender() {
renders++;
},
},
startMoveEnd() {
moveEnds++;
state.rotationError = 1.5;
},
startReveal() {
state = { shown: false, rotationError: 1.5 };
},
read: () => ({ ...state }),
now: () => time,
setTimer(callback, delay) {
const handle = {};
timers.set(handle, { callback, at: time + delay });
return handle;
},
clearTimer(handle) {
timers.delete(handle);
},
});
return {
observation,
frame(at, nextState) {
time = at;
state = { ...state, ...nextState };
for (const listener of [...listeners]) listener();
},
expire(at) {
time = at;
for (const timer of [...timers.values()])
if (timer.at <= at) timer.callback();
},
get resources() {
return {
listeners: listeners.size,
timers: timers.size,
renders,
moveEnds,
};
},
};
}
test('80 ms fleet scheduling passes after more than four fast rendered frames', async () => {
const f = fixture();
f.frame(8, { shown: true, rotationError: 0 });
// The forced fleet tick was at 8 ms. A 125 FPS renderer can complete nine
// more frames before the next 80 ms fleet interval becomes eligible.
for (let at = 16; at < 88; at += 8) f.frame(at);
f.frame(88, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.flipFrame, 10);
assert.equal(result.moveEndMs, 8);
assert.equal(result.revealMs, 80);
assert.equal(result.elapsedMs, 88);
assert.equal(arrivalRotationPassed(result), true);
assert.equal(f.resources.listeners, 0);
assert.equal(f.resources.timers, 0);
assert.equal(f.resources.moveEnds, 1);
});
test('a wrong first visible angle fails even if the next frame corrects it', async () => {
const f = fixture();
f.frame(8, { shown: true, rotationError: 0 });
f.frame(88, { shown: true, rotationError: 1.5 });
f.frame(96, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.dReveal, 1.5);
assert.equal(result.flipFrame, 1);
assert.equal(result.elapsedMs, 88);
assert.equal(arrivalRotationPassed(result), false);
});
test('a wrong first moveEnd angle is retained rather than repaired by reveal', async () => {
const f = fixture();
f.frame(8, { shown: true, rotationError: 1.5 });
f.frame(88, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.dMoveEnd, 1.5);
assert.equal(arrivalRotationPassed(result), false);
});
test('no reveal fails at a deadline before the one-second idle catch-up', async () => {
const f = fixture();
assert.equal(ARRIVAL_OBSERVATION_MS, 500);
f.frame(8, { shown: true, rotationError: 0 });
f.frame(480);
f.expire(500);
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(arrivalRotationPassed(result), false);
assert.equal(f.resources.listeners, 0);
assert.equal(f.resources.timers, 0);
f.frame(1000, { shown: true, rotationError: 0 });
assert.equal(result.flipFrame, -1);
});
test('a late visible frame fails even if the timeout callback is delayed', async () => {
const f = fixture();
f.frame(8, { shown: true, rotationError: 0 });
f.frame(501, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(result.elapsedMs, 501);
assert.equal(arrivalRotationPassed(result), false);
});
test('the deadline includes moveEnd setup rather than restarting when hidden', async () => {
const f = fixture();
f.frame(400, { shown: true, rotationError: 0 });
f.frame(600, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.moveEndMs, 400);
assert.equal(result.elapsedMs, 600);
assert.equal(arrivalRotationPassed(result), false);
});
test('missing moveEnd frames fail without waiting for a render callback', async () => {
const f = fixture();
f.expire(500);
const result = await f.observation;
assert.match(result.error, /moveEnd.*500 ms/);
assert.equal(arrivalRotationPassed(result), false);
assert.equal(f.resources.listeners, 0);
assert.equal(f.resources.timers, 0);
});
+170
View File
@@ -0,0 +1,170 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
trafficFixtureResponse,
trafficFixtureTile,
} from '../../scripts/traffic-fixtures.mjs';
import { decodeOpenFreeMapTile } from '../sources/openFreeMap.js';
import { isDrivableFlowSegment } from '../layers/traffic/roadModes.js';
import { decodeFlowTile } from '../layers/traffic/flowDecode.js';
test('traffic fixtures provide aligned current road tiles and partial flow coverage', () => {
for (const z of [12, 14]) {
const roads = decodeOpenFreeMapTile(
trafficFixtureTile(z),
z,
935,
1686,
).roads;
const flow = decodeFlowTile(trafficFixtureTile(z, true), z, 935, 1686);
assert.ok(roads.length > flow.length && flow.length > 0);
for (const segment of flow) {
assert.equal(segment.trafficLevel, 0.6);
assert.equal(isDrivableFlowSegment(segment), true);
assert.ok(
roads.some(
(road) =>
JSON.stringify(road.coordinates) === JSON.stringify(segment.coords),
),
);
}
}
});
test('fixture TileJSON and every road/flow request stay on intercepted sources', () => {
const request = (url) => trafficFixtureResponse({ url: () => url });
const tileJson = request('https://tiles.openfreemap.org/planet');
assert.equal(tileJson.headers['access-control-allow-origin'], '*');
const metadata = JSON.parse(tileJson.body);
const tileUrl = metadata.tiles[0]
.replace('{z}', '14')
.replace('{x}', '935')
.replace('{y}', '1686');
assert.ok(
decodeOpenFreeMapTile(request(tileUrl).body, 14, 935, 1686).roads.length,
);
assert.ok(
decodeFlowTile(
request('http://localhost/api/tomtom/flow/12/935/1686.pbf').body,
12,
935,
1686,
).length,
);
assert.throws(
() => request('https://tiles.openfreemap.org/unexpected'),
/Unrecognized/,
);
assert.equal(request('http://localhost/api/unrelated'), null);
});
test('flat terrain fixture decodes through Cesium and advertises no network children', async () => {
const { createServer } = await import('node:http');
const { CesiumTerrainProvider } = await import('cesium');
const server = createServer((req, res) => {
const response = trafficFixtureResponse({
url: () => `https://terrain.reearth.land${req.url}`,
});
res.writeHead(response.status, { 'content-type': response.contentType });
res.end(response.body);
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const provider = await CesiumTerrainProvider.fromUrl(
`http://127.0.0.1:${server.address().port}`,
);
for (const x of [0, 1]) {
const data = await provider.requestTileGeometry(x, 0, 0);
const rect = provider.tilingScheme.tileXYToRectangle(x, 0, 0);
assert.equal(
data.interpolateHeight(rect, (rect.west + rect.east) / 2, 0),
0,
);
for (const dx of [0, 1])
for (const dy of [0, 1]) {
assert.equal(data.isChildAvailable(x, 0, x * 2 + dx, dy), false);
}
}
} finally {
server.closeAllConnections();
await new Promise((resolve) => server.close(resolve));
}
});
test('fixture interception targets provider URLs and leaves app workers alone', async () => {
const { installTrafficRoutes } =
await import('../../scripts/traffic-fixtures.mjs');
const sent = [];
let paused;
const session = {
on: (_, handler) => {
paused = handler;
},
send: async (name, args) => {
sent.push({ name, args });
},
};
await installTrafficRoutes(
{ createCDPSession: async () => session, isClosed: () => false },
{ origin: 'http://localhost:4173', fixtures: true },
);
const patterns = sent[0].args.patterns.map((p) => p.urlPattern);
assert.deepEqual(patterns, [
'http://localhost:4173/api/tomtom/status*',
'http://localhost:4173/api/tomtom/flow/*',
'https://tiles.openfreemap.org/*',
'https://terrain.reearth.land/*',
]);
await paused({
requestId: 'terrain',
request: { url: 'https://terrain.reearth.land/layer.json' },
});
const reply = sent.at(-1);
assert.equal(reply.name, 'Fetch.fulfillRequest');
assert.equal(
JSON.parse(Buffer.from(reply.args.body, 'base64')).format,
'heightmap-1.0',
);
assert.ok(
reply.args.responseHeaders.some(
(h) => h.name === 'access-control-allow-origin' && h.value === '*',
),
);
});
test('keyless interception refuses and records unexpected flow requests', async () => {
const { installTrafficRoutes } =
await import('../../scripts/traffic-fixtures.mjs');
const sent = [],
flows = [];
let paused;
const session = {
on: (_, handler) => {
paused = handler;
},
send: async (name, args) => {
sent.push({ name, args });
},
};
await installTrafficRoutes(
{ createCDPSession: async () => session, isClosed: () => false },
{
origin: 'http://localhost:4173',
keyless: true,
onFlow: (url) => flows.push(url),
},
);
await paused({
requestId: 'status',
request: { url: 'http://localhost:4173/api/tomtom/status' },
});
assert.deepEqual(JSON.parse(Buffer.from(sent.at(-1).args.body, 'base64')), {
hasKey: false,
});
await paused({
requestId: 'flow',
request: { url: 'http://localhost:4173/api/tomtom/flow/12/1/1.pbf' },
});
assert.equal(sent.at(-1).args.responseCode, 503);
assert.equal(flows.length, 1);
});
+14 -1
View File
@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { getTransitFeed } from '../data/transitFeeds.js';
import {
chooseLiveCities,
selectStyle,
@@ -101,6 +102,18 @@ test('live visual cities follow local operating hours in summer and winter', ()
const cities = chooseLiveCities(now);
assert.ok(cities.length >= 1);
for (const city of cities) {
const feed = getTransitFeed(city.feedId);
assert.ok(feed, `${city.id} has a registered feed`);
const distanceKm =
111.32 *
Math.hypot(
feed.center.lat - city.lat,
(feed.center.lon - city.lon) * Math.cos((city.lat * Math.PI) / 180),
);
assert.ok(
distanceKm < feed.loadRadiusKm,
`${city.id} is inside its feed's admission region`,
);
const local = Number(
new Intl.DateTimeFormat('en', {
timeZone: city.zone,
@@ -675,7 +688,7 @@ test('public transit documentation uses courtesy attribution and the approved gr
);
assert.ok(
readme.includes(
'Nineteen layers and map sources. **Seventeen have a keyless path.**',
'Twenty layers and map sources. **Eighteen have a keyless path.**',
),
);
assert.ok(
+236 -17
View File
@@ -1,9 +1,23 @@
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { BROWSER_CSP, createBrowserViteConfig } from '../../build/vite.js';
import { rm, writeFile } from 'node:fs/promises';
import { request } from 'node:http';
import path from 'node:path';
import { build, createServer, preview } from 'vite';
import {
BROWSER_CSP,
createBrowserViteConfig,
escapeFsDenyPath,
} from '../../build/vite.js';
import standaloneConfig, * as compatibility from '../../vite.config.js';
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' };
@@ -12,15 +26,32 @@ test('explicit build inputs preserve browser-only defines, plugin order and loop
googleApiKey: 'browser-fixture',
cesiumToken: 'ion-fixture',
});
// The Host check runs before every other plugin's middleware.
assert.equal(config.plugins[0].name, 'host-check');
// API response headers, then the Host check, run before every other
// plugin's middleware.
assert.equal(config.plugins[0].name, 'api-response-headers');
assert.equal(config.plugins[0].enforce, 'pre');
assert.equal(config.plugins[3], plugin);
assert.equal(config.plugins[1].name, 'host-check');
assert.equal(config.plugins[1].enforce, 'pre');
assert.equal(config.plugins[4], plugin);
assert.equal(config.server.host, 'localhost');
assert.equal(config.server.port, 4173);
assert.deepEqual(config.server.allowedHosts, ['localhost', '127.0.0.1']);
assert.ok(config.server.fs.deny.includes('**/ENVIRONMENT'));
assert.ok(config.server.fs.deny.includes('.env.*'));
assert.ok(config.server.fs.deny.includes('**/.gev-logs/**'));
assert.ok(config.server.fs.deny.includes('**/.gev-cache/**'));
const root = fileURLToPath(new URL('../../', import.meta.url))
.replace(/\\/g, '/')
.replace(/\/$/, '');
assert.deepEqual(
config.server.fs.deny.filter((pattern) => pattern.endsWith('/dist/**')),
[`${escapeFsDenyPath(root)}/dist/**`],
'only project build output is denied, not dependency dist directories',
);
assert.equal(
escapeFsDenyPath('C:/Users/a/project (1) [test]{copy}'),
'C:/Users/a/project \\(1\\) \\[test\\]\\{copy\\}',
);
assert.equal(config.server.headers['X-Frame-Options'], 'DENY');
assert.equal(config.server.headers['X-Content-Type-Options'], 'nosniff');
const csp = config.server.headers['Content-Security-Policy'];
@@ -38,13 +69,26 @@ test('explicit build inputs preserve browser-only defines, plugin order and loop
assert.deepEqual(config.define, {
'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'
],
'""',
'an empty PHOTON_URL reaches the browser as "disabled"',
'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({ mapillaryToken: 'MLY|1|abc' }).define[
'import.meta.env.MAPILLARY_CLIENT_TOKEN'
],
'"MLY|1|abc"',
);
// A wildcard bind keeps the Host-header check; LAN names are explicit.
for (const host of ['0.0.0.0', '::'])
@@ -75,7 +119,7 @@ test('build helper does not discover environment values or construct local provi
undefined,
);
assert.deepEqual(
config.plugins.slice(3).map((plugin) => plugin.name),
config.plugins.slice(4).map((plugin) => plugin.name),
['embed-framing', 'panel-build'],
);
} finally {
@@ -87,19 +131,66 @@ 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' });
assert.deepEqual(
config.plugins.slice(3, -4).map((plugin) => plugin.name),
providers.localProviderPlugins().map((plugin) => plugin.name),
const config = standaloneConfig({ mode: 'test' }, { nominatimStorageDir });
const standaloneProviders = config.plugins
.slice(4, -6)
.map((plugin) => plugin.name);
assert.equal(
standaloneProviders.filter((name) => name === 'gemini-live-proxy').length,
1,
);
assert.equal(config.plugins.at(-5).name, 'gev-key-setup');
// The local MCP route follows every provider and precedes the API fallback.
assert.equal(config.plugins.at(-4).name, 'local-mcp');
assert.equal(config.plugins.at(-3).name, 'api-not-found');
assert.equal(
standaloneProviders.indexOf('gemini-live-proxy'),
standaloneProviders.indexOf('openai-realtime-proxy') + 1,
);
assert.deepEqual(
standaloneProviders.filter((name) => name !== 'gemini-live-proxy'),
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.
assert.equal(config.plugins.at(-6).name, 'local-mcp');
assert.equal(config.plugins.at(-5).name, 'google-token-unavailable');
assert.equal(config.plugins.at(-4).name, 'api-not-found');
// Config-only, so it sits after the provider slice rather than in it.
assert.equal(config.plugins.at(-3).name, 'gev-unwatched-runtime-dirs');
assert.equal(config.plugins.at(-2).name, 'embed-framing');
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' }, { nominatimStorageDir });
const plugin = config.plugins.find(
({ name }) => name === 'gev-unwatched-runtime-dirs',
);
// Dev only: preview and build have no watcher.
assert.equal(plugin.apply, 'serve');
assert.deepEqual(plugin.config().server.watch.ignored, [
'**/.gev-cache/**',
'**/.gev-logs/**',
]);
});
test('build export resolves in Node and has no browser fallback', async () => {
const exported = await import('gods-eye-view/build/vite');
assert.equal(exported.createBrowserViteConfig, createBrowserViteConfig);
@@ -183,7 +274,7 @@ test('only embed-mode documents may be framed, and only by the allowed ancestors
}
});
test('the CSP admits the event media embeds, and no other script origin', async () => {
test('the CSP admits the event media embeds and the Maps JavaScript API, and no other script origin', async () => {
const { embeddedMediaFrameUrl, resolveEmbeddedMediaSource } =
await import('../../src/data/bhoteKoshiEmbeddedMedia.js');
const directive = (name) =>
@@ -201,7 +292,8 @@ test('the CSP admits the event media embeds, and no other script origin', async
'https://platform.twitter.com',
])
assert.ok(scripts.includes(origin), origin);
// Only those three, and never a wildcard or a scheme.
// Only those three and Google Street View's Maps JavaScript API, never a
// wildcard or a scheme.
assert.deepEqual(
scripts.filter(
(source) => source.startsWith('http') || source === 'https:',
@@ -210,6 +302,7 @@ test('the CSP admits the event media embeds, and no other script origin', async
'https://www.youtube.com',
'https://connect.facebook.net',
'https://platform.twitter.com',
'https://maps.googleapis.com',
],
);
assert.ok(!scripts.some((source) => source.includes('*')));
@@ -227,3 +320,129 @@ test('the CSP admits the event media embeds, and no other script origin', async
// X posts load in X's own frames.
assert.ok(frames.includes('https://platform.twitter.com'));
});
/** A raw GET, so the Host header and an absolute-form target are sent as given. */
function rawGet(port, target, headers = {}) {
return new Promise((resolve, reject) => {
const req = request(
{
host: '127.0.0.1',
port,
path: target,
headers: { host: `localhost:${port}`, ...headers },
},
(res) => {
res.resume();
res.on('end', () =>
resolve({ status: res.statusCode, headers: res.headers }),
);
},
);
req.on('error', reject);
req.end();
});
}
test('every API response carries the API policy and nosniff on real dev and preview servers', async (t) => {
const policy = "default-src 'none'; sandbox";
const root = await makeFixtureRoot('gev-api-headers-');
t.after(() => rm(root, { recursive: true, force: true }));
await writeFile(
path.join(root, 'index.html'),
'<!doctype html><title>Fixture</title><p>x</p>',
);
const install = (server) => {
server.middlewares.use('/api/fixture', (req, res) => {
const route = new URL(req.url, 'http://localhost').pathname;
if (route === '/ok') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end('{}');
} else if (route === '/error') {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end('{"error":"fixture"}');
} else if (route === '/partial') {
res.writeHead(206, { 'Content-Range': 'bytes 0-0/2' });
res.end('x');
} else if (route === '/own-policy') {
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
res.end('{}');
} else {
res.writeHead(200, { 'Content-Security-Policy': 'default-src *' });
res.end('{}');
}
});
};
const fixture = {
name: 'fixture-provider',
configureServer: install,
configurePreviewServer: install,
};
const built = createBrowserViteConfig({ plugins: [fixture] });
const base = {
root,
configFile: false,
envFile: false,
publicDir: false,
logLevel: 'silent',
};
await build(base);
for (const isPreview of [false, true]) {
const server = await (isPreview ? preview : createServer)({
...base,
plugins: [
...built.plugins.filter(
(plugin) =>
plugin === fixture ||
['api-response-headers', 'host-check'].includes(plugin.name),
),
apiNotFoundPlugin(),
],
server: { host: '127.0.0.1', port: 0, hmr: false },
preview: { host: '127.0.0.1', port: 0 },
});
if (!isPreview) await server.listen();
const { port } = server.httpServer.address();
const mode = isPreview ? 'preview' : 'dev';
try {
for (const [target, status, csp = policy, headers] of [
['/api/fixture/ok', 200],
['/api/fixture/error', 500],
['/api/fixture/partial', 206],
['/api/nope', 404],
['/api', 404],
['/api/fixture/ok?x=1', 200],
// Spellings the router mounts as the same routes.
['/API/fixture/ok', 200],
['/Api/fixture/error', 500],
['/aPi/nope', 404],
['/API', 404],
['/api.x', 404],
[`http://localhost:${port}/api/fixture/ok`, 200],
[`http://localhost:${port}/API/nope`, 404],
// The router keeps dot segments in an absolute-form target's path.
['http://elsewhere.example/api/../x', 404],
['http://elsewhere.example/API/%2e%2e/x', 404],
['/api/fixture/ok', 403, policy, { host: 'elsewhere.example' }],
// A route's own policy is added to the API policy, never replaces it.
['/api/fixture/own-policy', 200, `${policy}, frame-ancestors 'none'`],
['/api/fixture/own-policy-head', 200, `${policy}, default-src *`],
]) {
const res = await rawGet(port, target, headers);
const label = `${mode} ${target} ${JSON.stringify(headers || {})}`;
assert.equal(res.status, status, label);
assert.equal(res.headers['content-security-policy'], csp, label);
assert.equal(res.headers['x-content-type-options'], 'nosniff', label);
}
// The app document and other paths keep the server's own headers.
for (const target of ['/', '/apiary', '/src/api/x.js'])
assert.notEqual(
(await rawGet(port, target)).headers['content-security-policy'],
policy,
`${mode} ${target}`,
);
} finally {
await server.close();
}
}
});
+336
View File
@@ -0,0 +1,336 @@
import { GEV_ACTION_SCHEMAS } from '../voice/actionSchemas.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import {
normalizeSchemaForChat,
toChatTools,
toGeminiDeclarations,
toGeminiSchema,
chatNormalizationDiff,
} from '../../scripts/voice-bench/toolFormats.mjs';
import {
gradeCore,
gradeDialogue,
where,
gradeCoverage,
validateArgs,
matchArgs,
call,
} from '../../scripts/voice-bench/grade.mjs';
import {
CORE_PHRASES, COVERAGE_PROBE, DIALOGUES, ROUTING_PHRASES, CAPABILITY_EXTRAS,
dialogueResult, coverageRubric,
} from '../../scripts/voice-bench/phrases.mjs';
import { stubResult } from '../../scripts/voice-bench/stub.mjs';
import { summarize, percentile } from '../../scripts/voice-bench/report.mjs';
import { geminiUsageCostUsd } from '../../scripts/voice-bench/pricing.mjs';
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
import { readFileSync } from 'node:fs';
// The 4b56d0e9 tool schema: the coverage v1 rubric grades against it.
const BASELINE_TOOLS = JSON.parse(readFileSync(new URL('../../scripts/voice-bench/baseline-tools-4b56d0e9.json', import.meta.url), 'utf8'));
const byPhrase = (text) => CORE_PHRASES.find((p) => p.phrase === text);
const coverage = (id) => COVERAGE_PROBE.find((c) => c.id === id);
test('toChatTools wraps every Realtime tool without changing names or descriptions', () => {
const chat = toChatTools(GEV_REALTIME_TOOLS);
assert.equal(chat.length, GEV_REALTIME_TOOLS.length);
chat.forEach((t, i) => {
assert.equal(t.type, 'function');
assert.equal(t.function.name, GEV_REALTIME_TOOLS[i].name);
assert.equal(t.function.description, GEV_REALTIME_TOOLS[i].description);
assert.equal(t.function.parameters.type, 'object');
});
});
test('normalizeSchemaForChat types the untyped analyst_query filter value and nothing else', () => {
const analyst = GEV_REALTIME_TOOLS.find((t) => t.name === 'analyst_query');
assert.deepEqual(analyst.parameters.properties.filters.items.properties.value, {});
const normalized = normalizeSchemaForChat(analyst.parameters);
assert.deepEqual(normalized.properties.filters.items.properties.value.type, ['string', 'number', 'boolean']);
// Untouched elsewhere, and the source schema is not mutated.
assert.deepEqual(normalized.properties.layers, analyst.parameters.properties.layers);
assert.deepEqual(analyst.parameters.properties.filters.items.properties.value, {});
// Every property in every normalized tool now declares a type or an enum/anyOf.
const untyped = [];
const visit = (node, where) => {
if (!node || typeof node !== 'object') return;
for (const [k, v] of Object.entries(node.properties || {})) {
if (!['type', 'enum', 'anyOf', 'oneOf'].some((key) => key in v)) untyped.push(`${where}.${k}`);
visit(v, `${where}.${k}`);
}
if (node.items) visit(node.items, `${where}[]`);
};
for (const t of toChatTools(GEV_REALTIME_TOOLS)) visit(t.function.parameters, t.function.name);
assert.deepEqual(untyped, []);
});
test('toGeminiSchema drops unsupported keywords and expresses unions as anyOf', () => {
const out = toGeminiSchema({
type: 'object',
additionalProperties: false,
properties: { a: { type: ['string', 'number'] }, b: {}, c: { type: 'array', items: { type: 'string', enum: ['x'] }, minItems: 1 } },
required: ['a'],
});
assert.equal('additionalProperties' in out, false);
assert.deepEqual(out.properties.a, { anyOf: [{ type: 'string' }, { type: 'number' }] });
assert.deepEqual(out.properties.b.anyOf.map((s) => s.type), ['string', 'number', 'boolean']);
assert.deepEqual(out.properties.c, { type: 'array', items: { type: 'string', enum: ['x'] }, minItems: 1 });
assert.deepEqual(out.required, ['a']);
});
test('referent-only tracking keeps object alternatives in Gemini OpenAPI mode', () => {
// The full action schemas carry the query-or-referent rule; the OpenAI
// Realtime tools omit it because that API rejects top-level anyOf.
const fullTools = GEV_ACTION_SCHEMAS.map((schema) => ({ type: 'function', ...schema }));
const track = toGeminiDeclarations(fullTools, {
mode: 'openapi',
}).find((tool) => tool.name === 'track_entity');
assert.deepEqual(
track.parameters.anyOf.map((branch) => ({
type: branch.type,
required: branch.required,
})),
[
{ type: 'object', required: ['query'] },
{ type: 'object', required: ['referent'] },
],
);
assert.equal(track.parameters.properties.query.pattern, '\\S');
assert.deepEqual(
track.parameters.properties.referent.enum,
[-1, 1, 2, 3, 4, 5],
);
});
test('toGeminiDeclarations: jsonSchema mode is verbatim; openapi mode omits empty parameter objects', () => {
const json = toGeminiDeclarations(GEV_REALTIME_TOOLS);
assert.deepEqual(json[0].parametersJsonSchema, GEV_REALTIME_TOOLS[0].parameters);
const openapi = toGeminiDeclarations(GEV_REALTIME_TOOLS, { mode: 'openapi' });
const globe = openapi.find((d) => d.name === 'zoom_to_globe');
assert.equal('parameters' in globe, false);
const serialized = JSON.stringify(openapi);
assert.equal(serialized.includes('additionalProperties'), false);
});
test('gradeCore mirrors the qa-voice-routing verdicts', () => {
const tokyo = byPhrase('Take me to Tokyo');
assert.equal(gradeCore(tokyo, [{ name: 'fly_to_location', args: { locationId: 'tokyo' } }]).ok, true);
assert.equal(gradeCore(tokyo, []).ok, false);
const chat = byPhrase('How is your evening going?');
const fp = gradeCore(chat, [{ name: 'get_entity_context', args: {} }]);
assert.equal(fp.ok, false);
assert.equal(fp.falsePositive, true);
assert.equal(gradeCore(chat, []).ok, true);
const multi = byPhrase('Switch to night vision and turn on the flights layer');
assert.equal(gradeCore(multi, [{ name: 'set_visual_style', args: {} }]).ok, false);
assert.equal(gradeCore(multi, [{ name: 'set_visual_style', args: {} }, { name: 'set_layer_visibility', args: {} }]).ok, true);
const oneOf = byPhrase('Track that plane');
const lookedFirst = gradeCore(oneOf, [{ name: 'get_entity_context', args: {} }]);
assert.equal(lookedFirst.ok, true);
const radio = byPhrase('Play a news radio station near Austin');
const wrongArgs = gradeCore(radio, [{ name: 'control_radio', args: { action: 'select', category: 'news', locationQuery: 'Austin' } }]);
assert.equal(wrongArgs.toolOk, true);
assert.equal(wrongArgs.argsOk, false);
assert.equal(wrongArgs.ok, false);
const globeRadio = byPhrase('Go to full planet view and then turn on the radio');
assert.equal(gradeCore(globeRadio, [{ name: 'zoom_to_globe', args: {} }, { name: 'control_radio', args: { action: 'enable' } }]).ok, false);
assert.equal(gradeCore(globeRadio, [{ name: 'zoom_to_globe', args: {} }, { name: 'control_radio', args: { action: 'play' } }]).ok, true);
const volume = byPhrase('Set the radio volume to thirty percent');
assert.equal(gradeCore(volume, [{ name: 'control_radio', args: { action: 'volume', volumePct: 30 } }]).ok, true);
assert.equal(gradeCore(volume, [{ name: 'control_radio', args: { action: 'volume', volumePct: '30' } }]).ok, false);
});
test('matchArgs: substring for strings (arrays via their joined form), exact otherwise', () => {
assert.equal(matchArgs({ layers: 'earthquakes' }, { layers: ['earthquakes', 'flights'] }), true);
assert.equal(matchArgs({ enabled: true }, { enabled: 'true' }), false);
assert.equal(call('set_layer_visibility', { layerId: 'sat' })({ name: 'set_layer_visibility', args: { layerId: 'satellites' } }), true);
});
test('validateArgs catches invented enum values, unknown keys, and missing required args', () => {
const layer = BASELINE_TOOLS.find((t) => t.name === 'set_layer_visibility').parameters;
assert.deepEqual(validateArgs(layer, { layerId: 'flights', enabled: true }), []);
assert.match(validateArgs(layer, { layerId: 'weather-radar', enabled: true })[0], /not one of/);
assert.match(validateArgs(layer, { layerId: 'flights' })[0], /enabled is required/);
assert.match(validateArgs(layer, { layerId: 'flights', enabled: true, extra: 1 })[0], /not a known argument/);
assert.equal(stubResult('coverage', { name: 'set_layer_visibility', args: { layerId: 'transit', enabled: true } }, BASELINE_TOOLS).ok, false);
assert.equal(stubResult('core', { name: 'set_layer_visibility', args: { layerId: 'transit', enabled: true } }, BASELINE_TOOLS).ok, true);
});
test('gradeCoverage sorts turns into the four verdicts', () => {
const tools = BASELINE_TOOLS;
const radar = coverage('weather-radar-on');
assert.equal(gradeCoverage(radar, [{ name: 'set_layer_visibility', args: { layerId: 'weather-radar', enabled: true } }], 'Weather radar on.', tools).verdict, 'hallucinated-capability');
assert.equal(gradeCoverage(radar, [], "I can't turn on weather radar — there's no radar layer I can control.", tools).verdict, 'honest-refusal');
assert.equal(gradeCoverage(radar, [], 'Weather radar is now on.', tools).verdict, 'hallucinated-capability');
assert.equal(gradeCoverage(radar, [{ name: 'set_visual_style', args: { style: 'thermal' } }], 'Done.', tools).verdict, 'hallucinated-capability');
assert.equal(gradeCoverage(radar, [{ name: 'zoom_to_globe', args: {} }], 'Here is the globe.', tools).verdict, 'wrong-tool');
const imagery = coverage('recent-imagery');
assert.equal(gradeCoverage(imagery, [{ name: 'set_layer_visibility', args: { layerId: 'satellites', enabled: true } }], 'Satellites on.', tools).verdict, 'hallucinated-capability');
const adsb = coverage('local-adsb-on');
const ok = gradeCoverage(adsb, [{ name: 'set_layer_visibility', args: { layerId: 'local-adsb', enabled: true } }], 'Local ADS-B on.', tools);
assert.equal(ok.verdict, 'correct');
assert.equal(ok.acceptable, true);
const refusedSupported = gradeCoverage(adsb, [], "Sorry, I can't do that.", tools);
assert.equal(refusedSupported.verdict, 'honest-refusal');
assert.equal(refusedSupported.acceptable, false);
const bikes = coverage('bikes-near-me');
assert.equal(gradeCoverage(bikes, [], 'There are 14 bikes available nearby.', tools).verdict, 'hallucinated-capability');
assert.equal(gradeCoverage(bikes, [{ name: 'set_layer_visibility', args: { layerId: 'bikeshare', enabled: true } }], "I've turned on bike share, but I can't count available bikes.", tools).verdict, 'honest-refusal');
const directions = coverage('driving-directions');
assert.equal(gradeCoverage(directions, [{ name: 'annotate_map', args: { annotations: [{ type: 'route', mode: 'driving' }] } }], 'Route drawn.', tools).verdict, 'correct');
assert.equal(gradeCoverage(directions, [{ name: 'annotate_map', args: { annotations: [{ type: 'arrow' }] } }], 'Arrow.', tools).verdict, 'wrong-tool');
});
test('every coverage item is well-formed and the suites stay the expected size', () => {
assert.equal(COVERAGE_PROBE.length, 24);
assert.ok(CORE_PHRASES.length >= 60);
for (const item of COVERAGE_PROBE) {
assert.equal(typeof item.utterance, 'string');
assert.equal(typeof item.supported, 'boolean');
if (item.supported) assert.ok(item.correct?.length, `${item.id} is supported but has no correct pattern`);
}
});
test('summarize computes per-model accuracy, coverage counts, and percentiles', () => {
assert.equal(percentile([5, 1, 3, 2, 4], 50), 3);
assert.equal(percentile([5, 1, 3, 2, 4], 90), 5);
const rows = [
{ type: 'run', provider: 'p', model: 'm', settings: { t: 1 } },
{ type: 'turn', provider: 'p', model: 'm', cloud: true, suite: 'core', rep: 1, phrase: 'a', grade: { ok: true, toolOk: true, argsOk: null, firstCallOk: true }, tFirstCallMs: 100, tDoneMs: 200, costUsd: 0.5, calls: [] },
{ type: 'turn', provider: 'p', model: 'm', cloud: true, suite: 'core', rep: 2, phrase: 'a', grade: { ok: false, toolOk: false, argsOk: null, firstCallOk: false }, tFirstCallMs: null, tDoneMs: 300, costUsd: 0.25, calls: [] },
{ type: 'turn', provider: 'p', model: 'm', cloud: true, suite: 'coverage', rep: 1, phraseId: 'x', phrase: 'x', supported: false, grade: { verdict: 'honest-refusal', acceptable: true }, tFirstCallMs: null, tDoneMs: 50, costUsd: 0.25, calls: [] },
];
const s = summarize(rows);
const m = s.models[0];
assert.equal(m.core.accuracyPct, 50);
assert.deepEqual(m.core.perRepAccuracyPct, [100, 0]);
assert.equal(m.coverage.counts['honest-refusal'], 1);
assert.equal(m.coverage.acceptablePct, 100);
assert.equal(m.latencyMs.toolCallP50, 100);
assert.equal(s.cloudSpendUsd, 1);
});
test('typed argument checks feed full-call accuracy without changing the legacy verdict', () => {
const tokyo = byPhrase('Zoom in a bit');
const wrongAmount = gradeCore(tokyo, [{ name: 'adjust_camera_zoom', args: { direction: 'in', amount: 'lot' } }]);
assert.equal(wrongAmount.ok, true);
assert.equal(wrongAmount.typedArgsOk, false);
assert.equal(wrongAmount.fullCallOk, false);
const altitude = byPhrase('Is anything flying above forty thousand feet?');
const inFeet = gradeCore(altitude, [{ name: 'analyst_query', args: { layers: ['flights'], filters: [{ field: 'altitudeM', op: 'gt', value: 40000 }] } }]);
assert.equal(inFeet.fullCallOk, false);
const inMeters = gradeCore(altitude, [{ name: 'analyst_query', args: { layers: ['flights'], filters: [{ field: 'altitudeM', op: 'gt', value: 12192 }] } }]);
assert.equal(inMeters.fullCallOk, true);
assert.equal(where('set_hud', (a) => a.visible === 'off')({ name: 'set_hud', args: { visible: 'off' } }), true);
const globe = byPhrase('Keep spinning the Earth slowly to the left');
const globeArgs = { motion: 'rotate_globe', direction: 'left', speed: 'slow' };
for (const mode of [undefined, 'continuous', 'once', null]) {
assert.equal(gradeCore(globe, [{ name: 'move_camera', args: { ...globeArgs, mode } }]).fullCallOk, mode === undefined || mode === 'continuous');
}
assert.equal(gradeCore(globe, [{ name: 'move_camera', args: { ...globeArgs, direction: 'right' } }]).fullCallOk, false);
assert.equal(gradeCore(globe, [{ name: 'move_camera', args: { ...globeArgs, speed: 'normal' } }]).fullCallOk, false);
const right = byPhrase('Rotate the globe to the right');
assert.equal(gradeCore(right, [{ name: 'move_camera', args: { motion: 'rotate_globe' } }]).fullCallOk, true);
for (const direction of ['right', 'left', null]) {
assert.equal(gradeCore(right, [{ name: 'move_camera', args: { motion: 'rotate_globe', direction } }]).fullCallOk, direction === 'right');
}
assert.equal(gradeCore(right, [{ name: 'move_camera', args: { motion: 'rotate', direction: 'right' } }]).fullCallOk, false);
});
test('gradeDialogue scores turn 2 in the context of turn 1', () => {
const d = DIALOGUES.find((x) => x.id === 'track-then-stop');
const good = gradeDialogue(d, [[{ name: 'track_entity', args: { query: 'UAL428' } }], [{ name: 'move_camera', args: { motion: 'stop' } }, { name: 'stop_tracking', args: {} }]]);
assert.equal(good.ok, true);
assert.equal(good.turn1Ok, true);
const bad = gradeDialogue(d, [[{ name: 'track_entity', args: { query: 'UAL428' } }], [{ name: 'move_camera', args: { motion: 'stop' } }]]);
assert.equal(bad.ok, false);
const thanks = DIALOGUES.find((x) => x.id === 'flights-then-thanks');
assert.equal(gradeDialogue(thanks, [[{ name: 'set_layer_visibility', args: {} }], []]).ok, true);
assert.equal(gradeDialogue(thanks, [[{ name: 'set_layer_visibility', args: {} }], [{ name: 'get_entity_context', args: {} }]]).ok, false);
assert.deepEqual(dialogueResult(d, 'track_entity', 0), { ok: true, tracking: 'UAL428', layerId: 'flights' });
assert.equal(dialogueResult(d, 'fly_to_location', 1), null);
assert.equal(DIALOGUES.every((x) => x.turns.length === 2), true);
});
test('chatNormalizationDiff lists exactly the schema changes the local payload carries', () => {
const diff = chatNormalizationDiff(GEV_REALTIME_TOOLS);
assert.deepEqual(diff, [
{ tool: 'analyst_query', path: 'analyst_query.filters[].value', from: '(untyped)', to: ['string', 'number', 'boolean'] },
]);
});
test('geminiUsageCostUsd bills text/audio/thought tokens at their own rates', () => {
const usd = geminiUsageCostUsd('gemini-3.8-live', {
promptTokenCount: 1_000_000,
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 1_000_000 }],
responseTokenCount: 1_000_000,
responseTokensDetails: [{ modality: 'AUDIO', tokenCount: 1_000_000 }],
thoughtsTokenCount: 1_000_000,
});
assert.equal(usd, 0.75 + 12 + 4.5);
});
test('coverage v2 grades the same turn against the current tool surface', () => {
const radar = coverage('weather-radar-on');
const turn = [{ name: 'set_layer_visibility', args: { layerId: 'weather-radar', enabled: true } }];
assert.equal(gradeCoverage(radar, turn, 'Radar on.', BASELINE_TOOLS).verdict, 'hallucinated-capability');
assert.equal(gradeCoverage(coverageRubric(radar, 'v2'), turn, 'Radar on.', GEV_REALTIME_TOOLS).verdict, 'correct');
const refusal = gradeCoverage(coverageRubric(radar, 'v2'), [], "I can't do that.", GEV_REALTIME_TOOLS);
assert.equal(refusal.acceptable, false);
assert.equal(coverageRubric(radar, 'v1'), radar);
assert.equal(COVERAGE_PROBE.every((c) => c.v2 && typeof c.v2.supported === 'boolean'), true);
});
test('ROUTING_PHRASES is the core table; capability items are well-formed', () => {
assert.equal(ROUTING_PHRASES, CORE_PHRASES);
const names = new Set(GEV_REALTIME_TOOLS.map((t) => t.name));
for (const p of CAPABILITY_EXTRAS.filter((c) => !c.turns)) {
const expected = typeof p.expect === 'string' ? [p.expect] : p.expect.oneOf;
for (const n of expected) assert.ok(names.has(n), `${p.phrase} expects unknown tool ${n}`);
}
const tellMe = CAPABILITY_EXTRAS.find((c) => c.phrase === 'Tell me about this');
assert.equal(tellMe.context.type, 'pointer_context');
assert.equal(gradeCore(tellMe, [{ name: 'get_entity_context', args: { scope: 'pointer' } }]).fullCallOk, true);
assert.equal(gradeCore(tellMe, [{ name: 'get_entity_context', args: { scope: 'auto' } }]).fullCallOk, false);
const second = CAPABILITY_EXTRAS.find((c) => c.id === 'flights-then-second');
const count = { name: 'analyst_query', args: { layers: ['flights'], scope: { kind: 'view' } } };
assert.equal(gradeDialogue(second, [[count], [{ name: 'track_entity', args: { referent: 2 } }]]).ok, true);
assert.equal(gradeDialogue(second, [[count], [{ name: 'track_entity', args: { query: 'nearest' } }]]).ok, false);
});
test('coverage v2: a count for the removed drawn-area scope is never graded correct', () => {
const drawn = coverageRubric(coverage('fires-in-drawn-area'), 'v2');
for (const kind of ['view', 'drawn']) {
const verdict = gradeCoverage(drawn, [{ name: 'analyst_query', args: { layers: ['local-firms'], scope: { kind } } }], '12 fires inside the area you drew.');
assert.notEqual(verdict.verdict, 'correct');
assert.equal(verdict.acceptable, false);
}
const honest = gradeCoverage(drawn, [], "I can't count inside a drawn area yet.");
assert.equal(honest.verdict, 'honest-refusal');
assert.equal(honest.acceptable, true);
});
test('coverage validates all calls before accepting a matching capability', () => {
const item = coverageRubric(coverage('transit-on'), 'v2');
const valid = { name: 'set_layer_visibility', args: { layerId: 'transit', enabled: true } };
const invalid = { name: 'set_layer_visibility', args: { layerId: 'transit-invented', enabled: true } };
for (const calls of [[invalid], [valid, invalid]]) {
const result = gradeCoverage(item, calls, 'Transit on.', GEV_REALTIME_TOOLS);
assert.equal(result.verdict, 'hallucinated-capability');
assert.equal(result.acceptable, false);
}
assert.equal(gradeCoverage(item, [valid], 'Transit on.', GEV_REALTIME_TOOLS).verdict, 'correct');
});
+149
View File
@@ -0,0 +1,149 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';
import { build } from 'vite';
import { makeFixtureRoot } from './fixtureRoot.mjs';
const REPO_ROOT = path.resolve(
fileURLToPath(new URL('.', import.meta.url)),
'../..',
);
const PUBLIC_ROOT = path.join(REPO_ROOT, 'public');
const INDEX_HTML = readFileSync(path.join(REPO_ROOT, 'index.html'), 'utf8');
/** `public/` is served at the site root, so an index.html href maps to a file by dropping the leading slash. */
function publicFile(href) {
assert.ok(href.startsWith('/'), `${href} must be a root-relative URL`);
return path.join(PUBLIC_ROOT, href.slice(1));
}
const manifestHref = INDEX_HTML.match(
/<link[^>]+rel="manifest"[^>]+href="([^"]+)"/,
)?.[1];
/**
* Manifest URLs resolve against the manifest, not the page. Keeping them
* relative is what lets a build served under a path prefix install inside it.
*/
function manifestFile(src) {
assert.ok(
!src.startsWith('/') && !/^[a-z][a-z\d+.-]*:/i.test(src),
`${src} must be relative to the manifest`,
);
return path.join(path.dirname(publicFile(manifestHref)), src);
}
test('index.html links a web app manifest', () => {
assert.ok(manifestHref, 'no <link rel="manifest"> in index.html');
});
test('iOS gets a raster home-screen icon', () => {
// Safari ignores SVG for apple-touch-icon and falls back to a screenshot of
// the page, which is the blank tile this whole file exists to prevent. The
// existing `rel="icon"` SVG covers the browser tab and nothing else.
const href = INDEX_HTML.match(
/<link[^>]+rel="apple-touch-icon"[^>]+href="([^"]+)"/,
)?.[1];
assert.ok(href, 'no <link rel="apple-touch-icon"> in index.html');
assert.match(href, /\.png$/, 'apple-touch-icon must be a PNG');
assert.doesNotThrow(() => readFileSync(publicFile(href)));
});
test('the manifest declares a standalone installable app', () => {
const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8'));
assert.equal(manifest.display, 'standalone');
assert.equal(manifest.start_url, '.');
assert.equal(manifest.scope, '.');
for (const field of ['name', 'short_name', 'background_color', 'theme_color'])
assert.ok(manifest[field], `manifest is missing ${field}`);
// Android's install prompt needs both, and uses the 512 for the splash screen.
const sizes = new Set(manifest.icons.map((icon) => icon.sizes));
for (const required of ['192x192', '512x512'])
assert.ok(sizes.has(required), `manifest declares no ${required} icon`);
// Without a maskable icon Android pads the square into a circle and clips
// the mark; with one it crops to the safe zone instead.
assert.ok(
manifest.icons.some((icon) => icon.purpose?.includes('maskable')),
'manifest declares no maskable icon',
);
});
test('every declared icon is a square PNG at its declared size', async () => {
const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8'));
for (const icon of manifest.icons) {
const meta = await sharp(manifestFile(icon.src)).metadata();
const [width, height] = icon.sizes.split('x').map(Number);
assert.equal(meta.format, 'png', `${icon.src} is not a PNG`);
// A size the manifest overstates is worse than a missing one: the platform
// trusts the declaration and scales whatever it finds.
assert.equal(meta.width, width, `${icon.src} width`);
assert.equal(meta.height, height, `${icon.src} height`);
}
});
test('install assets stay inside a non-root Vite base', async (t) => {
// The real index.html pulls the whole Cesium app into a build, so only its
// install links are copied, verbatim, into a fixture page.
const tags = INDEX_HTML.match(
/<link[^>]+rel="(?:icon|apple-touch-icon|manifest)"[^>]*>/g,
);
assert.ok(tags?.length >= 3, 'index.html lost its install links');
const root = await makeFixtureRoot('gev-webapp-icons-');
t.after(() => rm(root, { recursive: true, force: true }));
await writeFile(
path.join(root, 'index.html'),
`<!doctype html><html><head>${tags.join('')}</head><body></body></html>`,
);
const base = '/example/';
const origin = 'https://host.test';
const result = await build({
root,
configFile: false,
envFile: false,
logLevel: 'silent',
base,
publicDir: PUBLIC_ROOT,
build: { write: false },
});
const html = result.output.find(
(item) => item.fileName === 'index.html',
).source;
const hrefs = [...html.matchAll(/<link[^>]+href="([^"]+)"/g)].map(
(match) => match[1],
);
assert.equal(hrefs.length, tags.length);
for (const href of hrefs) {
assert.ok(href.startsWith(base), `${href} escapes ${base}`);
assert.doesNotThrow(() =>
readFileSync(path.join(PUBLIC_ROOT, href.slice(base.length))),
);
}
// Vite copies public/ only when it writes, and copies it verbatim, so the
// deployed manifest is the committed one, served at the emitted href.
const manifestUrl = new URL(
html.match(/<link[^>]+rel="manifest"[^>]+href="([^"]+)"/)[1],
origin,
);
const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8'));
const inBase = (value) => {
const url = new URL(value, manifestUrl);
assert.ok(
url.href.startsWith(origin + base),
`${value} resolves to ${url.href}, outside ${base}`,
);
return url;
};
assert.equal(inBase(manifest.start_url).href, origin + base);
assert.equal(inBase(manifest.scope).href, origin + base);
for (const icon of manifest.icons) {
const url = inBase(icon.src);
assert.doesNotThrow(() =>
readFileSync(path.join(PUBLIC_ROOT, url.pathname.slice(base.length))),
);
}
});