Files
gods-eye-view/src/voice/speech.test.mjs
T
2026-10-28 17:55:20 +04:00

475 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
SPEECH_BUILDERS,
attachVoiceResult,
planStepLabel,
progressLine,
progressStepLabel,
spokenLabel,
} from './speech.js';
import { countWords, findHedges } from './speechLint.js';
import { setLocale } from '../i18n/index.js';
function envelope(name, result, args) {
return attachVoiceResult(name, result, args);
}
function assertSpeakable(say) {
assert.ok(countWords(say) <= 25, `too long: ${say}`);
assert.deepEqual(findHedges(say), [], `hedged: ${say}`);
}
test('layer toggles confirm the resulting state and fold only material feed tags', () => {
const on = envelope(
'set_layer_visibility',
{
ok: true,
action: 'set_layer_visibility',
layerId: 'flights',
label: 'Flights',
enabled: true,
feedState: 'stale',
},
{ layerId: 'flights', enabled: true },
);
assert.equal(on.say, 'Flights on, stale.');
const fallback = envelope(
'set_layer_visibility',
{
ok: true,
label: 'Flights',
enabled: true,
feedState: 'fallback',
source: 'adsb.lol',
},
{ enabled: true },
);
assert.equal(
fallback.say,
'Flights on.',
'a healthy fallback feed is valid data, shown not spoken',
);
assert.deepEqual(fallback.display.sources, [{ label: 'adsb.lol' }]);
const unavailable = envelope(
'set_layer_visibility',
{
ok: true,
label: 'Active Fires',
enabled: true,
feedState: 'unavailable',
},
{ enabled: true },
);
assert.equal(
unavailable.say,
'Active Fires on, but its feed is unavailable.',
);
const off = envelope(
'set_layer_visibility',
{ ok: true, label: 'Flights', enabled: false },
{ enabled: false },
);
assert.equal(off.say, 'Flights off.');
const failed = envelope(
'set_layer_visibility',
{
ok: false,
layerId: 'traffic',
error: 'Could not enable the requested layer',
},
{ layerId: 'traffic', enabled: true },
);
assert.equal(failed.say, "Couldn't turn on traffic.");
assert.equal(failed.ok, false, 'the envelope never changes the outcome');
});
test('aircraft identity always covers operator, type and route from returned fields only', () => {
const full = envelope('get_entity_context', {
ok: true,
selected: {
id: 'a1b2c3',
layerId: 'flights',
name: 'UAL428',
source: 'OpenSky',
properties: {
callsign: 'UAL428',
operator: 'United Airlines',
type: 'B738',
routeOrigin: 'KIAH',
routeDestination: 'KSFO',
registration: 'N12345',
},
},
});
assert.equal(
full.identityLine,
'UAL428, United Airlines, B738, KIAH to KSFO.',
);
assert.equal(
full.say,
null,
'the identity line is not forced onto other questions',
);
assert.ok(
full.display.lines.includes('Registration N12345'),
'registration is shown, not spoken',
);
assert.deepEqual(full.referents, [
{ n: 1, id: 'a1b2c3', label: 'UAL428', layerId: 'flights' },
]);
const sparse = envelope('get_entity_context', {
ok: true,
selected: {
id: 'ae01',
layerId: 'military',
name: 'RCH123',
properties: { callsign: 'RCH123' },
},
});
assert.equal(
sparse.identityLine,
'RCH123, operator unknown, type unknown, no route on file.',
);
assertSpeakable(sparse.identityLine);
const routeString = envelope('get_entity_context', {
ok: true,
selected: {
id: 'x',
layerId: 'flights',
properties: {
callsign: 'DAL1',
operator: 'Delta',
type: 'A321',
route: 'KATL-KLAX',
},
},
});
assert.equal(
routeString.identityLine,
'DAL1, Delta, A321, KATL to KLAX.',
'endpoint codes stay verbatim',
);
const place = envelope('get_entity_context', {
ok: true,
selected: {
id: 'dc',
layerId: 'local-datacenters',
name: 'Austin DC-7',
properties: {},
},
});
assert.equal(
place.say,
null,
'non-aircraft answers are composed by the model',
);
assert.equal(place.display.title, 'Austin DC-7');
const inView = { ok: true, scope: 'in_view', visible: [] };
assert.equal(
envelope('get_entity_context', inView),
inView,
'no selection leaves the result unchanged',
);
});
test('nearest-aircraft results name the aircraft, distance and altitude briefly', () => {
const ok = envelope('select_nearest_aircraft', {
ok: true,
action: 'select_nearest_aircraft',
location: 'Austin',
layerId: 'flights',
label: 'SWA1940',
feed: { state: 'nominal', source: 'OpenSky', count: 212 },
aircraft: {
id: 'a0b1',
callsign: 'SWA1940',
altitudeM: 9449,
distanceKm: 12.4,
},
});
assert.equal(ok.say, 'Selected SWA1940, 12 km from Austin, 31,000 feet.');
assertSpeakable(ok.say);
assert.equal(ok.referents[0].id, 'a0b1');
const empty = envelope('select_nearest_aircraft', {
ok: false,
stage: 'nearest',
layerId: 'flights',
location: { label: 'Austin' },
feed: { state: 'nominal' },
});
assert.equal(empty.say, 'No airborne aircraft loaded near Austin yet.');
const down = envelope('select_nearest_aircraft', {
ok: false,
stage: 'nearest',
feed: { state: 'unavailable', source: 'OpenSky' },
});
assert.equal(down.say, 'The aircraft feed is unavailable.');
const stale = envelope('select_nearest_aircraft', {
ok: true,
location: 'Austin',
feed: { state: 'stale' },
aircraft: { callsign: 'AAL1', distanceKm: 3.26 },
});
assert.equal(stale.say, 'Selected AAL1, 3.3 km from Austin, stale.');
});
test('frame_overhead keeps the essential scope audible and lists referents', () => {
const framed = envelope('frame_overhead', {
ok: true,
layerId: 'flights',
radiusKm: 150,
count: 14,
detectionEnabled: true,
nearest: [
{ id: 'a', label: 'UAL1' },
{ id: 'b', label: null },
{ id: null, label: null },
],
});
assert.equal(framed.say, 'Framed 14 aircraft within 150 km.');
assert.deepEqual(
framed.referents.map((r) => r.label),
['UAL1', 'b'],
);
const capped = envelope('frame_overhead', {
ok: true,
layerId: 'ais-live-vessels',
radiusKm: 120,
count: 80,
nearest: [],
});
assert.equal(capped.say, 'Framed at least 80 ships within 120 km.');
const off = envelope('frame_overhead', {
ok: false,
layerId: 'satellites',
error: 'The satellites layer is not enabled',
});
assert.equal(off.say, 'The satellites layer is off. Turn it on?');
const empty = envelope('frame_overhead', {
ok: false,
layerId: 'flights',
radiusKm: 150,
count: 0,
error: 'No flights within 150 km of the current view',
});
assert.equal(empty.say, 'No aircraft within 150 km.');
// The runner's real refusal envelope never claims the sky is empty.
const refused = envelope('frame_overhead', {
ok: false,
action: 'frame_overhead',
error: 'Camera navigation is unavailable in the current view',
});
assert.equal(refused.say, "Couldn't frame the contacts right now.");
const noPolicy = envelope('frame_overhead', {
ok: false,
action: 'frame_overhead',
error: 'Camera navigation policy unavailable',
});
assert.doesNotMatch(noPolicy.say, /^No /);
const cancelled = { ok: false, cancelled: true, action: 'frame_overhead' };
assert.equal(envelope('frame_overhead', cancelled), cancelled);
});
test('fly_to confirms the destination; cancellation stays silent', () => {
assert.equal(
envelope('fly_to_location', { ok: true, label: 'Tokyo' }).say,
'Flying to Tokyo.',
);
assert.equal(
envelope('fly_to_location', { ok: true, label: 'Austin', arrived: true })
.say,
'Over Austin.',
);
assert.equal(
envelope('fly_to_location', { ok: false, cancelled: true, label: 'Austin' })
.say,
undefined,
'a cancelled call keeps its exact outcome',
);
assert.equal(
envelope('fly_to_location', { ok: false, query: 'Atlantis' }).say,
"Couldn't find Atlantis.",
);
});
test('view state speaks enabled layers only and is a silent lookup by default', () => {
const view = envelope('get_current_view_state', {
ok: true,
camera: { heightM: 40210 },
style: 'thermal',
layers: [
{
id: 'flights',
name: 'Flights',
enabled: true,
count: 120,
feedState: 'stale',
},
{
id: 'local-firms',
name: 'Fires',
enabled: true,
count: 8,
feedState: 'nominal',
},
],
});
assert.equal(
view.say,
'Camera 40 km up. Thermal style. Flights and Fires on. Flights stale.',
);
assert.equal(view.schedule, 'silent');
assertSpeakable(view.say);
});
test('annotation speech names only what failed and never announces drawing', () => {
const clean = envelope('annotate_map', {
ok: true,
drawn: 2,
failed: 0,
items: [
{ ok: true, id: 'anno-1', label: 'Capitol' },
{ ok: true, id: 'anno-2', target: 'Zilker Park' },
],
});
assert.equal(clean.say, null);
assert.equal(clean.display.title, 'Marked 2 places');
assert.deepEqual(
clean.referents.map((r) => r.label),
['Capitol', 'Zilker Park'],
);
const partial = envelope('annotate_map', {
ok: true,
partial: true,
failedLabels: ['Flibbergibbet Building'],
items: [{ ok: true, label: 'Capitol' }, { ok: false }],
});
assert.equal(partial.say, "Couldn't place Flibbergibbet Building.");
const route = envelope('annotate_map', {
ok: true,
routeFallback: true,
items: [{ ok: true, label: 'A → B' }],
});
assert.equal(
route.say,
'That line is straight-line distance, not a street route.',
);
const pending = envelope('annotate_map', {
ok: true,
outlinePending: true,
items: [{ ok: true, label: 'Presidio' }],
});
assert.equal(pending.say, null);
assert.deepEqual(pending.display.notes, ['Tracing outlines']);
});
test('analyst speech preserves exact counts, lower bounds and partial status', () => {
const raw = {
ok: true,
action: 'analyst_query',
count: 3,
complete: true,
scopeLabel: 'anywhere in the loaded data',
coverage: { layersQueried: [{ layerKey: 'flights' }] },
};
assert.equal(
attachVoiceResult('analyst_query', raw).say,
'3 aircraft anywhere in the loaded data.',
);
assert.equal(
attachVoiceResult('analyst_query', {
...raw,
count: 250_000,
complete: false,
partial: true,
unanswered: ['military'],
}).say,
'At least 250,000 aircraft anywhere in the loaded data. Partial; military not answered.',
);
});
test('builders never break an action and callers can override analyst speech', () => {
assert.equal(typeof SPEECH_BUILDERS.analyst_query, 'function');
const raw = { ok: true, action: 'analyst_query', count: 3 };
const hooked = attachVoiceResult(
'analyst_query',
raw,
{},
{ analyst_query: (r) => ({ say: `${r.count} found.` }) },
);
assert.equal(hooked.say, '3 found.');
const throwing = attachVoiceResult(
'x',
raw,
{},
{
x: () => {
throw new Error('boom');
},
},
);
assert.equal(throwing, raw);
assert.equal(attachVoiceResult('set_layer_visibility', null), null);
});
test('labels are bounded and cleaned before they are spoken', () => {
assert.equal(
spokenLabel(' Texas\u0000 State\n Capitol '),
'Texas State Capitol',
);
assert.equal(spokenLabel('x'.repeat(80)).length, 48);
assert.equal(
planStepLabel('annotate_map', {
annotations: [
{ target: 'Capitol' },
{ label: 'Zilker' },
{ target: 'UT' },
],
}),
'Mark Capitol and Zilker +1',
);
assert.equal(
planStepLabel('fly_to_location', { query: 'Tokyo' }),
'Fly to Tokyo',
);
assert.equal(
progressStepLabel('resolve', 'Capitol'),
'Finding places: Capitol',
);
for (const step of [
'resolve',
'search',
'fly',
'layer',
'refresh',
'nearest',
'still',
]) {
const line = progressLine(step, 'Austin');
assertSpeakable(line);
assert.ok(countWords(line) <= 12);
}
assert.equal(
progressLine('outline', 'Presidio'),
null,
'background steps stay on screen',
);
});
test('progress step labels join through the catalog colon', () => {
setLocale('fr');
try {
assert.equal(
progressStepLabel('resolve', 'Capitol'),
'Recherche de lieux\u202f: Capitol',
);
} finally {
setLocale('en');
}
});