mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge feat/voice-geometry onto main
Both stack parents (#837, #842) are on main, so the voice geometry branch now builds on main directly. Conflict resolutions keep main's behaviour and add the geometry work on top: osm_query gating on every provider, restored admin identity for Nominatim outlines, merged analyst follow-up memory, and the cancelled Globe View reset instruction ahead of the generic cancellation.
This commit is contained in:
commit
ecaddd838b
102 files changed
+12925
-1006
No files matched your search
@@ -0,0 +1,78 @@
|
||||
// THE BOUNDED ANSWER CACHE — memory and disk bounds for the Nominatim area
|
||||
// and OSM routes. Every memory insertion is bounded (disk hits included), and
|
||||
// the disk keeps to its entry and byte quotas, removing expired files first.
|
||||
//
|
||||
// Run with: npm test
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { promises as fsp } from 'node:fs';
|
||||
import { createBoundedCache } from '../../server/providers/common/boundedCache.js';
|
||||
|
||||
async function tempDir() {
|
||||
return fsp.mkdtemp(path.join(os.tmpdir(), 'gev-bounded-cache-'));
|
||||
}
|
||||
|
||||
test('disk hits are bounded in memory like fresh answers', async () => {
|
||||
const dir = await tempDir();
|
||||
const writer = createBoundedCache({
|
||||
dir,
|
||||
ttlMs: 60_000,
|
||||
maxMemoryEntries: 100,
|
||||
maxDiskEntries: 200,
|
||||
});
|
||||
for (let i = 0; i < 75; i += 1) await writer.set(`k${i}`, `v${i}`);
|
||||
const reader = createBoundedCache({
|
||||
dir,
|
||||
ttlMs: 60_000,
|
||||
maxMemoryEntries: 10,
|
||||
maxDiskEntries: 200,
|
||||
});
|
||||
for (let i = 0; i < 75; i += 1)
|
||||
assert.equal(await reader.get(`k${i}`), `v${i}`);
|
||||
assert.equal(reader.memorySize, 10);
|
||||
await fsp.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('the disk keeps to its entry and byte quotas and drops expired files', async () => {
|
||||
const dir = await tempDir();
|
||||
let clock = 1_000_000;
|
||||
const cache = createBoundedCache({
|
||||
dir,
|
||||
ttlMs: 10_000,
|
||||
maxDiskEntries: 5,
|
||||
maxDiskBytes: 10_000,
|
||||
now: () => clock,
|
||||
});
|
||||
for (let i = 0; i < 12; i += 1) await cache.set(`k${i}`, 'x'.repeat(100));
|
||||
let files = await fsp.readdir(dir);
|
||||
assert.ok(files.length <= 5, `${files.length} files`);
|
||||
await cache.set('big', 'y'.repeat(20_000));
|
||||
const sizes = await Promise.all(
|
||||
(await fsp.readdir(dir)).map(
|
||||
async (name) => (await fsp.stat(path.join(dir, name))).size,
|
||||
),
|
||||
);
|
||||
assert.ok(sizes.reduce((a, b) => a + b, 0) <= 10_000, 'byte quota holds');
|
||||
// Age every file past the TTL, then write once more: the old files go.
|
||||
const past = new Date(Date.now() - 3_600_000);
|
||||
for (const name of await fsp.readdir(dir))
|
||||
await fsp.utimes(path.join(dir, name), past, past);
|
||||
clock = Date.now();
|
||||
await cache.set('fresh', 'z');
|
||||
files = await fsp.readdir(dir);
|
||||
assert.equal(files.length, 1);
|
||||
assert.equal(await cache.get('k0'), undefined);
|
||||
await fsp.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('an expired entry is not served', async () => {
|
||||
let clock = 0;
|
||||
const cache = createBoundedCache({ dir: null, ttlMs: 100, now: () => clock });
|
||||
await cache.set('a', 1);
|
||||
clock = 50;
|
||||
assert.equal(await cache.get('a'), 1);
|
||||
clock = 500;
|
||||
assert.equal(await cache.get('a'), undefined);
|
||||
});
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
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 {
|
||||
GEV_REALTIME_TOOLS,
|
||||
offeredTools,
|
||||
realtimeTools,
|
||||
} 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';
|
||||
@@ -164,6 +168,7 @@ test('weather-only requests share upstream work and retain fresh and stale respo
|
||||
test('Realtime handler preserves tools and default instructions, isolates supplied annotation guidance, and keeps the upstream key server-side', async (t) => {
|
||||
env(t, 'OPENAI_API_KEY', 'fixture-upstream-secret');
|
||||
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
|
||||
env(t, 'OVERPASS_UPSTREAMS', undefined);
|
||||
const sent = [];
|
||||
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
||||
assert.equal(url, 'https://api.openai.com/v1/realtime/client_secrets');
|
||||
@@ -194,7 +199,7 @@ test('Realtime handler preserves tools and default instructions, isolates suppli
|
||||
sent.at(-1).session.instructions,
|
||||
realtimeInstructions(guidance),
|
||||
);
|
||||
assert.deepEqual(sent.at(-1).session.tools, GEV_REALTIME_TOOLS);
|
||||
assert.deepEqual(sent.at(-1).session.tools, realtimeTools());
|
||||
}
|
||||
assert.notEqual(sent[0].session.instructions, sent[1].session.instructions);
|
||||
assert.equal(sent[0].session.instructions, sent[2].session.instructions);
|
||||
@@ -580,6 +585,36 @@ test('OAuth login endpoint selects an existing local sign-in without launching C
|
||||
assert.equal(response.body.includes('fixture-existing-secret'), false);
|
||||
});
|
||||
|
||||
test('osm_query is offered only when an operator configured Overpass', 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).session);
|
||||
return Response.json({ value: 'fixture-ephemeral' });
|
||||
});
|
||||
const mint = () =>
|
||||
request(install(openAiRealtimeProxy({})).get('/api/realtime/token'), {
|
||||
url: '/',
|
||||
});
|
||||
env(t, 'OVERPASS_UPSTREAMS', undefined);
|
||||
await mint();
|
||||
env(t, 'OVERPASS_UPSTREAMS', 'https://overpass.example/api/interpreter');
|
||||
await mint();
|
||||
const [plain, configured] = sent;
|
||||
const names = (session) => session.tools.map((tool) => tool.name);
|
||||
assert.equal(names(plain).includes('osm_query'), false);
|
||||
assert.doesNotMatch(plain.instructions, /osm_query/);
|
||||
assert.ok(names(plain).includes('resolve_area'));
|
||||
assert.ok(names(plain).includes('find_imagery'));
|
||||
assert.deepEqual(configured.tools, GEV_REALTIME_TOOLS);
|
||||
assert.match(configured.instructions, /osm_query/);
|
||||
assert.equal(
|
||||
configured.instructions,
|
||||
realtimeInstructions(undefined, { overpass: true }),
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -607,6 +642,7 @@ test('caption transcription is on by default, reported to the client meter, and
|
||||
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);
|
||||
env(t, 'OVERPASS_UPSTREAMS', undefined);
|
||||
const sent = [];
|
||||
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
||||
sent.push(JSON.parse(options.body));
|
||||
@@ -619,7 +655,11 @@ test('Realtime sessions carry supplied tools, and the standalone voice adds the
|
||||
),
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(sent.at(-1).session.tools, tools);
|
||||
// Without an operator Overpass, osm_query is left out of the session.
|
||||
assert.deepEqual(sent.at(-1).session.tools, offeredTools(tools));
|
||||
assert.ok(
|
||||
!sent.at(-1).session.tools.some(({ name }) => name === 'osm_query'),
|
||||
);
|
||||
const names = tools.map((tool) => tool.name);
|
||||
assert.equal(new Set(names).size, names.length);
|
||||
assert.deepEqual(
|
||||
@@ -713,7 +753,12 @@ test('Realtime service configuration selects compatible endpoint/model without f
|
||||
assert.equal(options.headers.Authorization, 'Bearer server-fixture');
|
||||
const payload = JSON.parse(options.body);
|
||||
assert.equal(payload.session.model, 'configured-model');
|
||||
assert.deepEqual(payload.session.tools, GEV_REALTIME_TOOLS);
|
||||
assert.deepEqual(
|
||||
payload.session.tools.map((tool) => tool.name),
|
||||
realtimeTools({
|
||||
overpass: Boolean(process.env.OVERPASS_UPSTREAMS),
|
||||
}).map((tool) => tool.name),
|
||||
);
|
||||
return Response.json({ value: 'short-lived-fixture' });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -785,8 +785,8 @@ test('administrative boundaries must be at the asked level', () => {
|
||||
assert.equal(picked.outline.osm, 'relation/9');
|
||||
});
|
||||
|
||||
test('outline selection does not publish child-owned admin geometry hooks', () => {
|
||||
for (const [name, addresstype] of [
|
||||
test('admin identity comes only from unambiguous evidence on the selected row', () => {
|
||||
for (const [name, addresstype, adminLevel] of [
|
||||
['Nepal', 'country', 'country'],
|
||||
['Bagmati Province', 'province', 'admin1'],
|
||||
['Province of Bagmati', 'province', 'admin1'],
|
||||
@@ -799,8 +799,8 @@ test('outline selection does not publish child-owned admin geometry hooks', () =
|
||||
kind: 'admin',
|
||||
query: name,
|
||||
});
|
||||
assert.equal('adminArea' in outline, false);
|
||||
assert.equal('adminLevel' in outline, false);
|
||||
assert.equal(outline.adminArea, name);
|
||||
assert.equal(outline.adminLevel, adminLevel);
|
||||
}
|
||||
|
||||
for (const row of [
|
||||
@@ -842,25 +842,25 @@ test('outline selection does not publish child-owned admin geometry hooks', () =
|
||||
[boundary({ name: 'Bagmati Zone', addresstype: 'state' })],
|
||||
{ kind: 'admin', query: 'Bagmati Zone' },
|
||||
).outline;
|
||||
assert.equal('adminArea' in retired, false);
|
||||
assert.equal(retired.adminArea, 'Bagmati Zone');
|
||||
assert.equal('adminLevel' in retired, false);
|
||||
|
||||
const retiredPrefix = selectOutlineResult(
|
||||
[boundary({ name: 'Zone of Bagmati', addresstype: 'state' })],
|
||||
{ kind: 'admin', query: 'Zone of Bagmati' },
|
||||
).outline;
|
||||
assert.equal('adminArea' in retiredPrefix, false);
|
||||
assert.equal(retiredPrefix.adminArea, 'Zone of Bagmati');
|
||||
assert.equal('adminLevel' in retiredPrefix, false);
|
||||
});
|
||||
|
||||
test('long admin names do not publish child-owned geometry hooks', () => {
|
||||
test('admin identity validates the full name before display truncation', () => {
|
||||
const longValidName = `Province of ${'Example '.repeat(24)}`.trim();
|
||||
const valid = selectOutlineResult(
|
||||
[boundary({ name: longValidName, addresstype: 'province' })],
|
||||
{ kind: 'admin', query: longValidName },
|
||||
).outline;
|
||||
assert.equal('adminArea' in valid, false);
|
||||
assert.equal('adminLevel' in valid, false);
|
||||
assert.equal(valid.adminArea, longValidName.slice(0, 160));
|
||||
assert.equal(valid.adminLevel, 'admin1');
|
||||
|
||||
const longContradictoryName = `${'Very Long Administrative Name '.repeat(7)}County`;
|
||||
assert.ok(longContradictoryName.length > 160);
|
||||
|
||||
@@ -1137,7 +1137,7 @@ test('the outline route answers OK with polygons and validates its input', async
|
||||
assert.equal((await request('/elsewhere')).status, 'next');
|
||||
});
|
||||
|
||||
test('the outline route leaves administrative geometry hooks to the annotation layer', async (t) => {
|
||||
test('the outline route serializes only selected-row administrative identity', async (t) => {
|
||||
const { fetchImpl } = stubFetch(async (url) => {
|
||||
const query = new URL(url).searchParams.get('q');
|
||||
const fixtures = {
|
||||
@@ -1195,8 +1195,8 @@ test('the outline route leaves administrative geometry hooks to the annotation l
|
||||
|
||||
const verified = await request('/outline?q=Bagmati%20Province&kind=admin');
|
||||
assert.equal(verified.status, 200);
|
||||
assert.equal('adminArea' in verified.body.outline, false);
|
||||
assert.equal('adminLevel' in verified.body.outline, false);
|
||||
assert.equal(verified.body.outline.adminArea, 'Bagmati Province');
|
||||
assert.equal(verified.body.outline.adminLevel, 'admin1');
|
||||
|
||||
for (const query of [
|
||||
'Northern Region',
|
||||
@@ -1231,7 +1231,7 @@ test('the outline route leaves administrative geometry hooks to the annotation l
|
||||
const retired = await request('/outline?q=Bagmati%20Zone&kind=admin');
|
||||
assert.equal(retired.status, 200);
|
||||
assert.equal(retired.body.status, 'OK');
|
||||
assert.equal('adminArea' in retired.body.outline, false);
|
||||
assert.equal(retired.body.outline.adminArea, 'Bagmati Zone');
|
||||
assert.equal('adminLevel' in retired.body.outline, false);
|
||||
|
||||
const city = await request('/outline?q=Austin&kind=city');
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
// THE BOUNDED OSM FEATURE SEARCH — `/api/osm/features`.
|
||||
//
|
||||
// A request names a curated preset, a box, a mode and a limit; the server
|
||||
// writes the Overpass query and sends it only to an operator-configured
|
||||
// Overpass (OVERPASS_UPSTREAMS). These cases pin what can never reach Overpass:
|
||||
// unknown kinds, raw tags, oversized boxes, boundary-area scans, and a second
|
||||
// upstream query while one is running. The generic `/api/overpass` guard is
|
||||
// covered by its own tests and is not loosened here.
|
||||
//
|
||||
// Run with: npm test
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import {
|
||||
compileOsmFeatureQuery,
|
||||
OSM_FEATURE_LIMITS,
|
||||
summarizeOsmFeatures,
|
||||
} from '../../server/providers/overpass/osmFeatures.js';
|
||||
import { installOsmFeaturesRoute } from '../../server/providers/overpass/osmFeaturesRoute.js';
|
||||
import { sanitizeOverpassBody } from '../../server/providers/overpass/query.js';
|
||||
|
||||
test('a preset and a small box compile to a bounded query', () => {
|
||||
const compiled = compileOsmFeatureQuery({
|
||||
preset: 'hospital',
|
||||
bbox: '85.2,27.6,85.4,27.8',
|
||||
limit: '50',
|
||||
});
|
||||
assert.equal(compiled.ok, true);
|
||||
assert.equal(
|
||||
compiled.ql,
|
||||
'[out:json][timeout:25][maxsize:67108864];(nwr["amenity"="hospital"](27.6,85.2,27.8,85.4););out center tags 50;',
|
||||
);
|
||||
assert.doesNotMatch(compiled.ql, /area/, 'no boundary-area scan');
|
||||
const count = compileOsmFeatureQuery({
|
||||
preset: 'bar',
|
||||
bbox: [80, 20, 85, 25],
|
||||
mode: 'count',
|
||||
});
|
||||
assert.match(
|
||||
count.ql,
|
||||
/nwr\["amenity"="bar"\].*nwr\["amenity"="pub"\].*out count;$/,
|
||||
);
|
||||
});
|
||||
|
||||
test('unknown kinds, raw tags, bad and oversized boxes are refused', () => {
|
||||
assert.equal(
|
||||
compileOsmFeatureQuery({ preset: 'amenity=hospital', bbox: '0,0,1,1' })
|
||||
.code,
|
||||
'UNKNOWN_PRESET',
|
||||
);
|
||||
assert.equal(
|
||||
compileOsmFeatureQuery({ preset: '__proto__', bbox: '0,0,1,1' }).code,
|
||||
'UNKNOWN_PRESET',
|
||||
);
|
||||
assert.equal(
|
||||
compileOsmFeatureQuery({ preset: 'hospital', bbox: '0,0,1' }).code,
|
||||
'BAD_BOX',
|
||||
);
|
||||
assert.equal(
|
||||
compileOsmFeatureQuery({ preset: 'hospital', bbox: '0,1,1,0' }).code,
|
||||
'BAD_BOX',
|
||||
);
|
||||
assert.equal(
|
||||
compileOsmFeatureQuery({ preset: 'hospital', bbox: '0,0,3,3' }).code,
|
||||
'AREA_TOO_LARGE',
|
||||
);
|
||||
assert.equal(
|
||||
compileOsmFeatureQuery({
|
||||
preset: 'hospital',
|
||||
bbox: '0,0,3,3',
|
||||
mode: 'count',
|
||||
}).ok,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
compileOsmFeatureQuery({
|
||||
preset: 'hospital',
|
||||
bbox: '0,0,7,7',
|
||||
mode: 'count',
|
||||
}).code,
|
||||
'AREA_TOO_LARGE',
|
||||
);
|
||||
const limited = compileOsmFeatureQuery({
|
||||
preset: 'hospital',
|
||||
bbox: '0,0,1,1',
|
||||
limit: 999999,
|
||||
});
|
||||
assert.match(
|
||||
limited.ql,
|
||||
new RegExp(`out center tags ${OSM_FEATURE_LIMITS.maxLimit};$`),
|
||||
);
|
||||
});
|
||||
|
||||
test('a box across the antimeridian is searched as its two halves', () => {
|
||||
const compiled = compileOsmFeatureQuery({
|
||||
preset: 'hospital',
|
||||
bbox: '179.5,-17,-179.5,-16',
|
||||
});
|
||||
assert.equal(compiled.ok, true);
|
||||
assert.match(compiled.ql, /\(-17,179\.5,-16,180\).*\(-17,-180,-16,-179\.5\)/);
|
||||
});
|
||||
|
||||
test('the generic Overpass guard still refuses boundary-area element scans', () => {
|
||||
const scan =
|
||||
'data=' +
|
||||
encodeURIComponent(
|
||||
'[out:json];area(3600000001)->.a;nwr["amenity"="hospital"](area.a);out;',
|
||||
);
|
||||
assert.equal(sanitizeOverpassBody(scan).ok, false);
|
||||
});
|
||||
|
||||
test('answers are compacted: centres, kept tags, truncation and counts', () => {
|
||||
const compiled = compileOsmFeatureQuery({
|
||||
preset: 'hospital',
|
||||
bbox: '0,0,1,1',
|
||||
limit: 2,
|
||||
});
|
||||
const features = summarizeOsmFeatures(
|
||||
{
|
||||
elements: [
|
||||
{
|
||||
type: 'node',
|
||||
id: 1,
|
||||
lat: 0.5,
|
||||
lon: 0.5,
|
||||
tags: { name: 'A', amenity: 'hospital', 'contact:phone': 'x' },
|
||||
},
|
||||
{
|
||||
type: 'way',
|
||||
id: 2,
|
||||
center: { lat: 0.4, lon: 0.4 },
|
||||
tags: { name: 'B', amenity: 'hospital' },
|
||||
},
|
||||
],
|
||||
},
|
||||
compiled,
|
||||
);
|
||||
assert.equal(features.count, 2);
|
||||
assert.equal(features.truncated, true, 'the limit was reached');
|
||||
assert.deepEqual(features.features[1], {
|
||||
id: 'way/2',
|
||||
lat: 0.4,
|
||||
lon: 0.4,
|
||||
tags: { name: 'B', amenity: 'hospital' },
|
||||
});
|
||||
assert.equal('contact:phone' in features.features[0].tags, false);
|
||||
const count = summarizeOsmFeatures(
|
||||
{
|
||||
elements: [
|
||||
{
|
||||
type: 'count',
|
||||
tags: { nodes: '3', ways: '2', relations: '0', total: '5' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ ...compiled, mode: 'count' },
|
||||
);
|
||||
assert.equal(count.count, 5);
|
||||
});
|
||||
|
||||
function mount(fetchPayload, { featuresCache, configured = () => true } = {}) {
|
||||
const routes = new Map();
|
||||
const writes = [];
|
||||
const memoryCache = () => {
|
||||
const held = new Map();
|
||||
return {
|
||||
get: async (key) => held.get(key),
|
||||
set: async (key, value) => {
|
||||
held.set(key, value);
|
||||
writes.push(key);
|
||||
},
|
||||
};
|
||||
};
|
||||
installOsmFeaturesRoute(
|
||||
{ use: (route, handler) => routes.set(route, handler) },
|
||||
{
|
||||
fetchPayload,
|
||||
cacheRoot: null,
|
||||
configured,
|
||||
featuresCache: featuresCache || memoryCache(),
|
||||
},
|
||||
);
|
||||
let client = 0;
|
||||
const start = (search, route = '/api/osm/features') => {
|
||||
let req;
|
||||
let res;
|
||||
const promise = new Promise((resolve) => {
|
||||
client += 1;
|
||||
req = Object.assign(new EventEmitter(), {
|
||||
method: 'GET',
|
||||
url: `/?${search}`,
|
||||
headers: {},
|
||||
socket: { remoteAddress: `10.9.0.${client}` },
|
||||
});
|
||||
res = Object.assign(new EventEmitter(), {
|
||||
writableEnded: false,
|
||||
writeHead(status, headers) {
|
||||
res.status = status;
|
||||
res.headers = headers;
|
||||
},
|
||||
end(body) {
|
||||
res.writableEnded = true;
|
||||
resolve({
|
||||
status: res.status,
|
||||
headers: res.headers,
|
||||
body: JSON.parse(body),
|
||||
});
|
||||
},
|
||||
});
|
||||
routes.get(route)(req, res);
|
||||
});
|
||||
return {
|
||||
promise,
|
||||
disconnect() {
|
||||
res.emit('close');
|
||||
},
|
||||
};
|
||||
};
|
||||
const request = (search, route = '/api/osm/features') =>
|
||||
start(search, route).promise;
|
||||
return { request, start, writes };
|
||||
}
|
||||
|
||||
test('the route answers, caches, and runs one upstream query at a time', async () => {
|
||||
let release;
|
||||
let upstream = 0;
|
||||
const { request, writes } = mount(async (body) => {
|
||||
upstream += 1;
|
||||
assert.match(decodeURIComponent(body), /^data=\[out:json\]/);
|
||||
await new Promise((r) => {
|
||||
release = r;
|
||||
});
|
||||
return {
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
elements: [{ type: 'node', id: 7, lat: 1, lon: 1, tags: {} }],
|
||||
}),
|
||||
};
|
||||
});
|
||||
const first = request('preset=hospital&bbox=0,0,2,2');
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const other = await request('preset=school&bbox=0,0,2,2');
|
||||
assert.equal(other.status, 503, 'a second, different search waits its turn');
|
||||
release();
|
||||
const answered = await first;
|
||||
assert.equal(answered.status, 200);
|
||||
assert.equal(answered.body.count, 1);
|
||||
const again = await request('preset=hospital&bbox=0,0,2,2');
|
||||
assert.equal(again.headers['X-OSM-Cache'], 'HIT');
|
||||
assert.equal(upstream, 1);
|
||||
assert.equal(writes.length, 1);
|
||||
const refused = await request('preset=hospital&bbox=0,0,9,9');
|
||||
assert.equal(refused.status, 400);
|
||||
assert.equal(refused.body.code, 'AREA_TOO_LARGE');
|
||||
});
|
||||
|
||||
test('an upstream refusal is not an empty answer', async () => {
|
||||
const { request } = mount(async () => ({
|
||||
status: 429,
|
||||
body: 'rate_limited',
|
||||
rateLimited: true,
|
||||
}));
|
||||
const answer = await request('preset=hospital&bbox=0,0,1,1');
|
||||
assert.equal(answer.status, 429);
|
||||
assert.ok(answer.body.error);
|
||||
});
|
||||
|
||||
test('malformed successful answers are refused and never cached', async () => {
|
||||
const payloads = [
|
||||
{},
|
||||
{ elements: [{ type: 'count', tags: { nodes: '2' } }] },
|
||||
{ elements: [{ type: 'way', id: 9, tags: { amenity: 'hospital' } }] },
|
||||
];
|
||||
const { request, writes } = mount(async () => ({
|
||||
status: 200,
|
||||
body: JSON.stringify(payloads.shift()),
|
||||
}));
|
||||
const missingElements = await request('preset=hospital&bbox=0,0,1,1');
|
||||
const missingTotal = await request('preset=hospital&bbox=0,0,1,1&mode=count');
|
||||
const unusableRows = await request('preset=hospital&bbox=1,1,2,2');
|
||||
for (const answer of [missingElements, missingTotal, unusableRows]) {
|
||||
assert.equal(answer.status, 502);
|
||||
assert.match(answer.body.error, /failed/i);
|
||||
}
|
||||
assert.deepEqual(writes, []);
|
||||
});
|
||||
|
||||
test('the last disconnected waiter aborts upstream work and frees the slot', async () => {
|
||||
let calls = 0;
|
||||
let firstStarted;
|
||||
let firstAborted;
|
||||
const started = new Promise((resolve) => {
|
||||
firstStarted = resolve;
|
||||
});
|
||||
const aborted = new Promise((resolve) => {
|
||||
firstAborted = resolve;
|
||||
});
|
||||
const { request, start } = mount(async (_body, _cap, { signal }) => {
|
||||
calls += 1;
|
||||
if (calls === 1) {
|
||||
firstStarted();
|
||||
return new Promise((resolve, reject) => {
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
firstAborted();
|
||||
reject(signal.reason);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
return { status: 200, body: '{"elements":[]}' };
|
||||
});
|
||||
const abandoned = start('preset=hospital&bbox=0,0,1,1');
|
||||
await started;
|
||||
abandoned.disconnect();
|
||||
await aborted;
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
const next = await request('preset=school&bbox=0,0,1,1');
|
||||
assert.equal(next.status, 200);
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
test('one disconnected coalesced waiter does not cancel a live sibling', async () => {
|
||||
let release;
|
||||
let upstreamSignal;
|
||||
let calls = 0;
|
||||
const { start } = mount(async (_body, _cap, { signal }) => {
|
||||
calls += 1;
|
||||
upstreamSignal = signal;
|
||||
await new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
return { status: 200, body: '{"elements":[]}' };
|
||||
});
|
||||
const first = start('preset=hospital&bbox=0,0,1,1');
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
const second = start('preset=hospital&bbox=0,0,1,1');
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
first.disconnect();
|
||||
assert.equal(upstreamSignal.aborted, false);
|
||||
release();
|
||||
const answer = await second.promise;
|
||||
assert.equal(answer.status, 200);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
test('without a configured Overpass the route refuses and fetches nothing', async () => {
|
||||
let upstream = 0;
|
||||
const { request } = mount(
|
||||
async () => {
|
||||
upstream += 1;
|
||||
return { status: 200, body: '{"elements":[]}' };
|
||||
},
|
||||
{ configured: () => false },
|
||||
);
|
||||
const answer = await request('preset=hospital&bbox=85.2,27.6,85.4,27.8');
|
||||
assert.equal(answer.status, 503);
|
||||
assert.equal(answer.body.code, 'OVERPASS_NOT_CONFIGURED');
|
||||
assert.equal(answer.body.retryable, false);
|
||||
assert.match(answer.body.error, /configured Overpass/);
|
||||
assert.equal(upstream, 0);
|
||||
});
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
call,
|
||||
} from '../../scripts/voice-bench/grade.mjs';
|
||||
import {
|
||||
CORE_PHRASES, COVERAGE_PROBE, DIALOGUES, ROUTING_PHRASES, CAPABILITY_EXTRAS,
|
||||
CORE_PHRASES, COVERAGE_PROBE, DIALOGUES, ROUTING_PHRASES, GEOMETRY_PHRASES, OVERPASS_PHRASES, CAPABILITY_EXTRAS,
|
||||
dialogueResult, coverageRubric,
|
||||
} from '../../scripts/voice-bench/phrases.mjs';
|
||||
import { stubResult } from '../../scripts/voice-bench/stub.mjs';
|
||||
@@ -293,10 +293,11 @@ test('coverage v2 grades the same turn against the current tool surface', () =>
|
||||
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);
|
||||
test('ROUTING_PHRASES is the core table plus geometry and place search; capability items are well-formed', () => {
|
||||
assert.deepEqual(ROUTING_PHRASES, [...CORE_PHRASES, ...GEOMETRY_PHRASES, ...OVERPASS_PHRASES]);
|
||||
assert.ok(OVERPASS_PHRASES.every((p) => p.requires?.includes('osm_query')), 'place search is gated');
|
||||
const names = new Set(GEV_REALTIME_TOOLS.map((t) => t.name));
|
||||
for (const p of CAPABILITY_EXTRAS.filter((c) => !c.turns)) {
|
||||
for (const p of [...GEOMETRY_PHRASES, ...OVERPASS_PHRASES, ...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}`);
|
||||
}
|
||||
@@ -310,16 +311,14 @@ test('ROUTING_PHRASES is the core table; capability items are well-formed', () =
|
||||
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', () => {
|
||||
test('coverage v2: a drawn-area count must use the drawn scope, never the view', () => {
|
||||
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);
|
||||
const scoped = (kind) => gradeCoverage(drawn, [{ name: 'analyst_query', args: { layers: ['local-firms'], scope: { kind } } }], '12 fires inside the area you drew.');
|
||||
assert.equal(scoped('drawn').verdict, 'correct');
|
||||
assert.notEqual(scoped('view').verdict, 'correct');
|
||||
const bagmati = CAPABILITY_EXTRAS.find((c) => c.id === 'bagmati-then-fires');
|
||||
const fires = { name: 'analyst_query', args: { layers: ['local-firms'], scope: { kind: 'area', areaId: 'area-1' } } };
|
||||
assert.equal(gradeDialogue(bagmati, [[{ name: 'resolve_area', args: { query: 'Bagmati province' } }], [fires]]).ok, true);
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user