mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
The HUD builds place context for its AI summary whenever the camera settles, and each build paid for up to four reverse geocodes plus a Nearby Places search, even when no OpenAI key was configured to use them. - Ask GET /api/openai/hud-summary/status once; with no summary configured, or after an unconfigured reply, make no place lookups for the page. - Reverse-geocode at most two points: the view center and one viewport point. - Reuse a lookup until the view center moves a quarter of the visible width, the camera's distance to it doubles or halves, or the camera crosses a lookup height. The reused points are asked again, so only a timeout, failure or empty answer spends. - Repaint the last AI reply when a small pan leaves the context unchanged. Cached-only hosts and voice context lookups are unchanged.
1206 lines
40 KiB
JavaScript
1206 lines
40 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createServer } from 'node:http';
|
|
import { Readable } from 'node:stream';
|
|
import {
|
|
mkdtempSync,
|
|
readFileSync,
|
|
existsSync,
|
|
rmSync,
|
|
statSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { overpassProxy } from 'gods-eye-view/server/providers/overpass';
|
|
import { militaryInstallationsProxy } from 'gods-eye-view/server/providers/military-installations';
|
|
import {
|
|
regionalBriefProxy,
|
|
weatherEffectsProxy,
|
|
} from 'gods-eye-view/server/providers/regional';
|
|
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) {
|
|
const routes = new Map();
|
|
plugin[preview ? 'configurePreviewServer' : 'configureServer']({
|
|
middlewares: { use: (route, handler) => routes.set(route, handler) },
|
|
restart: async () => {},
|
|
});
|
|
return routes;
|
|
}
|
|
function request(
|
|
handler,
|
|
{
|
|
method = 'GET',
|
|
url = '/',
|
|
body = '',
|
|
origin = 'http://localhost:4173',
|
|
remoteAddress = '127.0.0.1',
|
|
} = {},
|
|
) {
|
|
return new Promise((resolve, reject) => {
|
|
const req = Readable.from(body ? [Buffer.from(body)] : []);
|
|
Object.assign(req, {
|
|
method,
|
|
url,
|
|
headers: {
|
|
host: 'localhost:4173',
|
|
origin,
|
|
'content-type': 'application/json',
|
|
},
|
|
socket: { remoteAddress },
|
|
});
|
|
const headers = {};
|
|
const res = {
|
|
statusCode: 200,
|
|
setHeader(name, value) {
|
|
headers[name.toLowerCase()] = value;
|
|
},
|
|
writeHead(status, values) {
|
|
this.statusCode = status;
|
|
for (const [k, v] of Object.entries(values)) this.setHeader(k, v);
|
|
},
|
|
end(body = '') {
|
|
resolve({
|
|
status: this.statusCode,
|
|
headers,
|
|
body: String(body),
|
|
json: () => JSON.parse(String(body)),
|
|
});
|
|
},
|
|
};
|
|
Promise.resolve(handler(req, res)).catch(reject);
|
|
});
|
|
}
|
|
function env(t, name, value) {
|
|
const old = process.env[name];
|
|
if (value === undefined) delete process.env[name];
|
|
else process.env[name] = value;
|
|
t.after(() => {
|
|
if (old === undefined) delete process.env[name];
|
|
else process.env[name] = old;
|
|
});
|
|
}
|
|
function root(t) {
|
|
const dir = mkdtempSync(path.join(tmpdir(), 'gev-services-'));
|
|
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
return dir;
|
|
}
|
|
|
|
test('standalone service guards run in development and preview without upstream acquisition', async (t) => {
|
|
t.mock.method(globalThis, 'fetch', () => {
|
|
throw Error('invalid requests must not fetch');
|
|
});
|
|
for (const preview of [false, true]) {
|
|
for (const [factory, route] of [
|
|
[overpassProxy, '/api/overpass'],
|
|
[militaryInstallationsProxy, '/api/military-installations'],
|
|
[regionalBriefProxy, '/api/regional-brief'],
|
|
[weatherEffectsProxy, '/api/weather-effects'],
|
|
]) {
|
|
const routes = install(factory(), preview);
|
|
assert.equal(
|
|
(await request(routes.get(route), { method: 'DELETE' })).status,
|
|
405,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await request(routes.get(route), {
|
|
method: route === '/api/overpass' ? 'POST' : 'GET',
|
|
})
|
|
).status,
|
|
400,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('weather-only requests share upstream work and retain fresh and stale responses', async (t) => {
|
|
let now = Date.now();
|
|
t.mock.method(Date, 'now', () => now);
|
|
let calls = 0;
|
|
t.mock.method(globalThis, 'fetch', async (url) => {
|
|
calls++;
|
|
assert.equal(new URL(url).hostname, 'api.open-meteo.com');
|
|
if (calls > 1) throw Error('offline');
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
return Response.json({
|
|
current: {
|
|
time: '2026-09-12T12:00',
|
|
temperature_2m: 20,
|
|
weather_code: 0,
|
|
wind_speed_10m: 10,
|
|
},
|
|
});
|
|
});
|
|
const handler = install(weatherEffectsProxy()).get('/api/weather-effects');
|
|
const query = { url: '/?latitude=34.61&longitude=-112.43' };
|
|
const pair = await Promise.all([
|
|
request(handler, query),
|
|
request(handler, query),
|
|
]);
|
|
assert.deepEqual(pair.map((r) => r.headers['x-weather-effects']).sort(), [
|
|
'INFLIGHT',
|
|
'MISS',
|
|
]);
|
|
assert.equal(calls, 1);
|
|
assert.equal(
|
|
(await request(handler, query)).headers['x-weather-effects'],
|
|
'HIT',
|
|
);
|
|
now += 6 * 60_000;
|
|
assert.equal(
|
|
(await request(handler, query)).headers['x-weather-effects'],
|
|
'STALE',
|
|
);
|
|
});
|
|
|
|
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);
|
|
const sent = [];
|
|
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
|
assert.equal(url, 'https://api.openai.com/v1/realtime/client_secrets');
|
|
assert.equal(
|
|
options.headers.Authorization,
|
|
'Bearer fixture-upstream-secret',
|
|
);
|
|
sent.push(JSON.parse(options.body));
|
|
return Response.json({ value: 'fixture-ephemeral' });
|
|
});
|
|
for (const [options, guidance] of [
|
|
[{}, undefined],
|
|
[
|
|
{ annotationGuidance: 'Fixture annotation instruction.' },
|
|
'Fixture annotation instruction.',
|
|
],
|
|
[{}, undefined],
|
|
]) {
|
|
const response = await request(
|
|
install(openAiRealtimeProxy(options)).get('/api/realtime/token'),
|
|
{ url: '/?tier=unknown' },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.headers['x-gev-voice-tier'], 'standard');
|
|
assert.equal(response.headers['x-gev-voice-tier-fallback'], '1');
|
|
assert.equal(response.body.includes('fixture-upstream-secret'), false);
|
|
assert.equal(
|
|
sent.at(-1).session.instructions,
|
|
realtimeInstructions(guidance),
|
|
);
|
|
assert.deepEqual(sent.at(-1).session.tools, GEV_REALTIME_TOOLS);
|
|
}
|
|
assert.notEqual(sent[0].session.instructions, sent[1].session.instructions);
|
|
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);
|
|
const sent = [];
|
|
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
|
sent.push(JSON.parse(options.body));
|
|
return Response.json({ value: 'fixture-ephemeral' });
|
|
});
|
|
const tools = standaloneVoiceTools();
|
|
const response = await request(
|
|
install(openAiRealtimeProxy({ realtime: { tools } })).get(
|
|
'/api/realtime/token',
|
|
),
|
|
);
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(sent.at(-1).session.tools, tools);
|
|
const names = tools.map((tool) => tool.name);
|
|
assert.equal(new Set(names).size, names.length);
|
|
assert.deepEqual(
|
|
tools.slice(0, GEV_REALTIME_TOOLS.length),
|
|
GEV_REALTIME_TOOLS,
|
|
);
|
|
assert.ok(names.includes('get_weather'));
|
|
assert.ok(names.includes('military_awareness'));
|
|
assert.ok(!names.includes('show_in_gods_eye_view'));
|
|
assert.ok(!names.includes('aircraft_in_area'));
|
|
assert.ok(!names.includes('get_weather_map'));
|
|
// The action of the same name answers satellite passes.
|
|
assert.equal(
|
|
tools.findLast((tool) => tool.name === 'next_satellite_pass'),
|
|
GEV_REALTIME_TOOLS.find((tool) => tool.name === 'next_satellite_pass'),
|
|
);
|
|
});
|
|
|
|
test('debug logging resolves each supplied application directory independently', async (t) => {
|
|
const first = root(t),
|
|
second = root(t);
|
|
for (const [sourceRoot, marker] of [
|
|
[first, 'first'],
|
|
[second, 'second'],
|
|
]) {
|
|
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
|
|
'/api/realtime/debug-log',
|
|
);
|
|
assert.equal(
|
|
(
|
|
await request(handler, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ marker }),
|
|
})
|
|
).status,
|
|
204,
|
|
);
|
|
const file = path.join(
|
|
sourceRoot,
|
|
'.gev-logs/realtime-conversations.jsonl',
|
|
);
|
|
assert.equal(JSON.parse(readFileSync(file, 'utf8')).marker, marker);
|
|
}
|
|
});
|
|
|
|
test('key setup writes only the supplied application root, retains request guards and stays absent from preview', async (t) => {
|
|
const first = root(t),
|
|
untouched = root(t);
|
|
env(t, 'OPENAI_API_KEY', undefined);
|
|
const plugin = keySetupEndpoint({ sourceRoot: first });
|
|
assert.equal(plugin.apply({}, { command: 'serve', isPreview: true }), false);
|
|
assert.equal(plugin.configurePreviewServer, undefined);
|
|
const routes = install(plugin);
|
|
const handler = routes.get('/api/setup/keys');
|
|
const body = JSON.stringify({
|
|
OPENAI_API_KEY: 'sk-fixture-only-not-a-real-key',
|
|
});
|
|
assert.equal(
|
|
(
|
|
await request(handler, {
|
|
method: 'POST',
|
|
body,
|
|
origin: 'https://example.com',
|
|
})
|
|
).status,
|
|
403,
|
|
);
|
|
assert.equal(existsSync(path.join(first, '.env')), false);
|
|
const saved = await request(handler, { method: 'POST', body });
|
|
assert.equal(saved.status, 200);
|
|
assert.match(
|
|
readFileSync(path.join(first, '.env'), 'utf8'),
|
|
/OPENAI_API_KEY=sk-fixture-only-not-a-real-key/,
|
|
);
|
|
if (process.platform !== 'win32')
|
|
assert.equal(statSync(path.join(first, '.env')).mode & 0o777, 0o600);
|
|
assert.equal(saved.body.includes('sk-fixture-only-not-a-real-key'), false);
|
|
assert.equal(existsSync(path.join(untouched, '.env')), false);
|
|
});
|
|
|
|
test('Realtime service configuration selects compatible endpoint/model without forwarding request model IDs or keys', async () => {
|
|
const handler = install(
|
|
openAiRealtimeProxy({
|
|
realtime: {
|
|
endpoint: 'https://voice.example/client-secrets',
|
|
models: { standard: 'configured-model' },
|
|
resolveApiKey: () => 'server-fixture',
|
|
fetchImpl: async (url, options) => {
|
|
assert.equal(url, 'https://voice.example/client-secrets');
|
|
assert.equal(options.redirect, 'error');
|
|
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);
|
|
return Response.json({ value: 'short-lived-fixture' });
|
|
},
|
|
},
|
|
}),
|
|
).get('/api/realtime/token');
|
|
const response = await request(handler, {
|
|
url: '/?tier=arbitrary-model&model=other',
|
|
});
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(response.json(), { value: 'short-lived-fixture' });
|
|
assert.equal(response.headers['x-gev-voice-model'], 'configured-model');
|
|
assert.equal(response.headers['cache-control'], 'no-store');
|
|
assert.doesNotMatch(response.body, /server-fixture|voice\.example/);
|
|
});
|
|
|
|
test('the HUD summary status says whether a summary is configured, without calling OpenAI', async (t) => {
|
|
t.mock.method(globalThis, 'fetch', async () => {
|
|
throw new Error('the status probe must not reach OpenAI');
|
|
});
|
|
const handler = install(openAiRealtimeProxy()).get('/api/openai/hud-summary');
|
|
for (const [key, configured] of [
|
|
[undefined, false],
|
|
[' ', false],
|
|
['fixture-key', true],
|
|
]) {
|
|
env(t, 'OPENAI_API_KEY', key);
|
|
const response = await request(handler, { url: '/status' });
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(response.json(), { configured });
|
|
assert.equal(response.headers['cache-control'], 'no-store');
|
|
}
|
|
assert.equal(
|
|
(await request(handler, { url: '/' })).status,
|
|
405,
|
|
'only the status path answers GET',
|
|
);
|
|
});
|
|
|
|
test('OpenAI routes answer generically when the upstream or the request fails', async (t) => {
|
|
env(t, 'OPENAI_API_KEY', 'fixture-upstream-secret');
|
|
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
|
|
const leak =
|
|
'fixture-upstream-secret req_fixture_1234 org-fixture quota exhausted';
|
|
|
|
// `data.error.message` is OpenAI's own wording — request ids, organization
|
|
// hints, quota phrasing — and was relayed verbatim whenever upstream was not ok.
|
|
t.mock.method(globalThis, 'fetch', async () =>
|
|
Response.json({ error: { message: leak } }, { status: 429 }),
|
|
);
|
|
const summary = await request(
|
|
install(openAiRealtimeProxy()).get('/api/openai/hud-summary'),
|
|
{ method: 'POST', body: JSON.stringify({ context: {} }) },
|
|
);
|
|
assert.equal(summary.json().error, 'OpenAI HUD summary request failed');
|
|
assert.equal(summary.body.includes('req_fixture_1234'), false);
|
|
assert.equal(summary.body.includes('fixture-upstream-secret'), false);
|
|
|
|
// An HTTP error from the client-secret endpoint carries the same upstream
|
|
// detail, while a successful response must still pass the ephemeral secret.
|
|
t.mock.restoreAll();
|
|
t.mock.method(globalThis, 'fetch', async () =>
|
|
Response.json({ error: { message: leak } }, { status: 429 }),
|
|
);
|
|
const rejectedToken = await request(
|
|
install(openAiRealtimeProxy()).get('/api/realtime/token'),
|
|
);
|
|
assert.equal(rejectedToken.status, 429);
|
|
assert.equal(
|
|
rejectedToken.headers['content-type'],
|
|
'application/json; charset=utf-8',
|
|
);
|
|
assert.deepEqual(rejectedToken.json(), {
|
|
error: 'Failed to create Realtime token',
|
|
});
|
|
assert.equal(rejectedToken.body.includes('req_fixture_1234'), false);
|
|
assert.equal(rejectedToken.body.includes('fixture-upstream-secret'), false);
|
|
|
|
// A network fault surfaced a resolver message naming the upstream host.
|
|
t.mock.restoreAll();
|
|
t.mock.method(globalThis, 'fetch', async () => {
|
|
throw Error(`getaddrinfo ENOTFOUND api.openai.com ${leak}`);
|
|
});
|
|
const token = await request(
|
|
install(openAiRealtimeProxy()).get('/api/realtime/token'),
|
|
);
|
|
assert.equal(token.status, 502);
|
|
assert.deepEqual(token.json(), { error: 'Failed to create Realtime token' });
|
|
assert.equal(token.body.includes('api.openai.com'), false);
|
|
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(
|
|
'/api/realtime/debug-log',
|
|
);
|
|
const file = path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl');
|
|
const write = (record) =>
|
|
request(handler, { method: 'POST', body: JSON.stringify(record) });
|
|
|
|
// A malformed record is the caller's fault and a 400; neither answer carries
|
|
// the error text, which for a write failure is an errno and an absolute path.
|
|
const malformed = await request(handler, { method: 'POST', body: '{nope' });
|
|
assert.equal(malformed.status, 400);
|
|
assert.deepEqual(malformed.json(), {
|
|
error: 'Failed to write Realtime debug log',
|
|
});
|
|
|
|
// This limiter has no opt-out, unlike the one the cost-bearing routes share:
|
|
// 120/min per IP, far above what a voice session writes.
|
|
let limited = null;
|
|
let accepted = 0;
|
|
for (let n = 0; n < 130 && !limited; n += 1) {
|
|
const response = await write({ n });
|
|
if (response.status === 429) limited = response;
|
|
else if (response.status === 204) accepted += 1;
|
|
}
|
|
assert.ok(limited, 'the sink refuses a caller past its per-minute ceiling');
|
|
assert.equal(limited.headers['retry-after'], '60');
|
|
assert.deepEqual(limited.json(), { error: 'Rate limit exceeded' });
|
|
// The limiter counts requests rather than successful writes, so the malformed
|
|
// record above already spent one of the 120 slots.
|
|
assert.equal(accepted, 119);
|
|
|
|
// Every accepted record is on disk and parses: the queue serializes appends,
|
|
// so none was lost or truncated by the ones beside it.
|
|
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
assert.equal(lines.length, accepted);
|
|
for (const line of lines) assert.doesNotThrow(() => JSON.parse(line));
|
|
assert.ok(lines.every((line) => JSON.parse(line).loggedAt));
|
|
});
|
|
|
|
test('a debug-log record cannot supply its own timestamp', async (t) => {
|
|
const sourceRoot = root(t);
|
|
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
|
|
'/api/realtime/debug-log',
|
|
);
|
|
const before = Date.now();
|
|
const response = await request(handler, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ loggedAt: '1999-01-01T00:00:00.000Z', note: 'x' }),
|
|
});
|
|
assert.equal(response.status, 204);
|
|
const file = path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl');
|
|
const [line] = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
const record = JSON.parse(line);
|
|
assert.equal(record.note, 'x');
|
|
assert.ok(Date.parse(record.loggedAt) >= before - 1000);
|
|
});
|
|
|
|
test('an oversized debug-log request receives the fixed error response', async (t) => {
|
|
const handler = install(openAiRealtimeProxy({ sourceRoot: root(t) })).get(
|
|
'/api/realtime/debug-log',
|
|
);
|
|
const server = createServer(handler);
|
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
t.after(() => new Promise((resolve) => server.close(resolve)));
|
|
|
|
const response = await fetch(
|
|
`http://127.0.0.1:${server.address().port}/api/realtime/debug-log`,
|
|
{ method: 'POST', body: 'x'.repeat(8 * 1024 * 1024 + 1) },
|
|
);
|
|
assert.equal(response.status, 400);
|
|
assert.deepEqual(await response.json(), {
|
|
error: 'Failed to write Realtime debug log',
|
|
});
|
|
});
|
|
|
|
test('the debug log rotates instead of growing without bound', async (t) => {
|
|
const sourceRoot = root(t);
|
|
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
|
|
'/api/realtime/debug-log',
|
|
);
|
|
const file = path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl');
|
|
|
|
// 8 MB bounds one request body; nothing bounded the file until now, so a
|
|
// single page could grow it for as long as the dev server ran. Each record
|
|
// here is ~1 MB, well inside the body cap.
|
|
const pad = 'x'.repeat(1024 * 1024);
|
|
// Seventy-two records force two rotations. The second one replaces an
|
|
// existing `.1`, which requires an explicit removal on Windows.
|
|
for (let n = 0; n < 72; n += 1) {
|
|
assert.equal(
|
|
(
|
|
await request(handler, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ n, pad }),
|
|
})
|
|
).status,
|
|
204,
|
|
);
|
|
}
|
|
|
|
const live = statSync(file).size;
|
|
const previous = statSync(`${file}.1`).size;
|
|
assert.ok(live <= 32 * 1024 * 1024, `live log under the ceiling (${live})`);
|
|
assert.ok(
|
|
live + previous <= 64 * 1024 * 1024,
|
|
'both generations together stay within twice the ceiling',
|
|
);
|
|
// Unrotated, these records would be ~75 MB in one file.
|
|
assert.ok(live + previous < 64 * 1024 * 1024);
|
|
assert.ok(!existsSync(`${file}.2`), 'exactly one generation is retained');
|
|
});
|
|
|
|
test('the voice instructions name only tools the voice session offers', async () => {
|
|
const { coreTools } = await import('../tools/index.js');
|
|
const tools = new Set(standaloneVoiceTools().map((tool) => tool.name));
|
|
const known = new Set([
|
|
...coreTools.map((tool) => tool.name),
|
|
...GEV_REALTIME_TOOLS.map((tool) => tool.name),
|
|
]);
|
|
const named = new Set(
|
|
realtimeInstructions().match(/\b[a-z]+(?:_[a-z]+)+\b/g),
|
|
);
|
|
const missing = [...named].filter(
|
|
(name) => known.has(name) && !tools.has(name),
|
|
);
|
|
assert.deepEqual(missing, []);
|
|
});
|
|
|
|
test('the voice instructions send vessels, however heard, to the ships layer', () => {
|
|
const text = realtimeInstructions();
|
|
assert.match(text, /"vessels" is easily heard as "visuals"/);
|
|
assert.match(text, /set_layer_visibility\{layerId:"ais-live-vessels"\}/);
|
|
});
|