import { readRealtimeSource } from './testSupport/readRealtimeSource.mjs'; import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js'; import { realtimeInstructions } from '../server/providers/openai/instructions.js'; import { readShellSource } from './testSupport/readShellSource.mjs'; import { expandApplicationHtml } from '../build/application-html.js'; import { readLayerSource } from './testSupport/readLayerSource.mjs'; import { readStylesheet } from './testSupport/readStylesheet.mjs'; import { readFileSync as readRadioSource } from 'node:fs'; const radioBindings = readRadioSource(new URL('./ui/radioBindings.js', import.meta.url), 'utf8'); const radioPresentation = readRadioSource(new URL('./ui/radioPresentation.js', import.meta.url), 'utf8'); const radioControlsSource = readRadioSource(new URL('./ui/radioControls.js', import.meta.url), 'utf8'); import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import test from 'node:test'; const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8')); const ui = readShellSource(); const radio = ['playback', 'interaction'].map(name => readFileSync(new URL(`./layers/radio/${name}.js`, import.meta.url), 'utf8') ).join('\n').replace(/layerState\.|parts\.\w+\./g, ''); const rocketLaunches = readLayerSource(new URL('./data/rocketLaunches.js', import.meta.url), 'utf8'); const realtime = readRealtimeSource(); const voice = ['./voice/actionSchemas.js', '../server/providers/openai/toolDescriptions.js', '../server/providers/openai/instructions.js'].map(file => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n'); const css = readStylesheet(new URL('../style.css', import.meta.url)); function realtimeTools() { return GEV_REALTIME_TOOLS; } test('Realtime schema exposes the authoritative 31-tool inventory', () => { const tools = realtimeTools(); assert.equal(tools.length, 31); const names = tools.map((tool) => tool.name); assert.equal(new Set(names).size, 31, 'tool names are unique'); assert.ok(names.includes('set_context_mode')); assert.ok(names.includes('control_cockpit')); assert.ok(names.includes('set_voice_inactivity_timeout')); assert.ok(names.includes('select_nearest_aircraft')); assert.ok(names.includes('control_radio')); assert.ok(names.includes('next_satellite_pass')); assert.ok(names.includes('next_iss_pass')); // Every tool closes its parameter object: an open schema lets the model // invent arguments the runner silently drops. for (const tool of tools) { assert.equal(tool.type, 'function', `${tool.name} is not a function tool`); assert.equal( tool.parameters?.additionalProperties, false, `${tool.name} does not close additionalProperties`, ); assert.ok(tool.description, `${tool.name} has no description`); } }); test('the counting contract is stated in the Realtime instructions', () => { // Owner ruling: "near" has one meaning per state, and every count names its // scope. Instruction text is the only place the narration rules can live, so // it is pinned — a silent trim here is a silent behaviour change. const start = voice.indexOf("'COUNTING CONTRACT"); assert.ok(start >= 0, 'the counting contract instruction is missing'); // One instruction per source line; the string carries escaped quotes, so take // the line rather than trying to match a quoted literal. const text = voice.slice(start, voice.indexOf('\n', start)); assert.match(text, /Contacts is ACTIVE/, 'rule 1: active means the Contacts window'); assert.match(text, /contactsWindow/, 'rule 1 names its mechanism'); assert.match(text, /call set_context_mode\{mode:"contacts"\} first/); assert.match(text, /contactsWindow\.aircraft/); assert.match(text, /Contacts OFF, "nearby" means in view/, 'rule 2: off means in view'); assert.match(text, /EVERY count names its scope in words/, 'rule 3'); assert.match(text, /scopeLabel/, 'rule 3 names its mechanism'); assert.match(text, /never a bare number/, 'rule 3 is stated as a prohibition too'); assert.match(text, /VERBATIM/, 'rule 4: no estimating'); // Rule 5, the loaded-data caveat, is now one word inside the spoken scope // ("37 loaded flights over Texas"); the longer note stays on screen. const analyst = voice.slice( voice.indexOf("'For ANALYTICAL questions"), voice.indexOf('\n', voice.indexOf("'For ANALYTICAL questions")), ); assert.match(analyst, /"37 loaded flights over Texas"/, 'rule 5: loaded scope stays audible'); }); test('Context panel opening stays distinct from Contacts activation', () => { const text = realtimeInstructions().split('\n').find(line => line.startsWith('Panels:')); assert.ok(text, 'panel-routing instruction is missing'); assert.match(text, /"Open Context" is only set_panel_open\{panelId:"global-context-panel",open:true\}/); assert.match(text, /opening Context or selecting\/viewing an aircraft never enters them/); assert.match(text, /"Open Contacts" is set_context_mode contacts/); assert.match(text, /expands Context itself/); }); test('nearest-aircraft selection stays out of Contacts and Cockpit', () => { const start = voice.indexOf("'For a request to enable an aircraft layer and SELECT or FIND"); assert.ok(start >= 0, 'nearest-aircraft selection routing instruction is missing'); const text = voice.slice(start, voice.indexOf('\n', start)); assert.match(text, /Turn on flights and select the closest aircraft to Austin/); assert.match(text, /call select_nearest_aircraft once/); assert.match(text, /atomically turns on the requested aircraft layer first/); assert.match(text, /waits for location arrival/); assert.match(text, /refreshes that layer for the destination viewport/); assert.match(text, /filters out landed\/on-ground records/); assert.match(text, /nearest airborne result/); assert.match(text, /healthy fallback feed is valid data/i); assert.match(text, /Do not also call fly_to_location, set_layer_visibility, analyst_query, track_entity/); const routing = realtimeInstructions().split('\n').find(line => line.startsWith('Panels:')); assert.match(routing, /Contacts, Space Missions and Cockpit need explicit naming/); assert.match(text, /set_context_mode, or control_cockpit/); const byName = new Map(realtimeTools().map((tool) => [tool.name, tool])); assert.match(byName.get('set_context_mode').description, /only when explicitly requested/); assert.match(routing, /selecting\/viewing an aircraft never enters them/); assert.match(byName.get('control_cockpit').description, /Explicit Cockpit requests only/); assert.equal( byName.get('fly_to_location').parameters.properties.waitForArrival.type, 'boolean', ); const nearest = byName.get('select_nearest_aircraft'); assert.deepEqual(nearest.parameters.required, ['layerId']); assert.deepEqual(nearest.parameters.properties.layerId.enum, ['flights', 'military']); assert.match(nearest.description, /Atomically/); assert.match(nearest.description, /exclude on-ground records/); assert.match(nearest.description, /fallback feeds remain usable/); }); test('the two Context/Cockpit tools pin their enums and required arguments', () => { const byName = new Map(realtimeTools().map((tool) => [tool.name, tool])); const contextMode = byName.get('set_context_mode'); assert.deepEqual(contextMode.parameters.required, ['mode']); assert.deepEqual( contextMode.parameters.properties.mode.enum, ['off', 'contacts', 'flights', 'space-missions', 'missions'], ); const cockpit = byName.get('control_cockpit'); assert.deepEqual(cockpit.parameters.required, ['action']); assert.deepEqual( cockpit.parameters.properties.action.enum, ['enter', 'exit', 'previous', 'next', 'prev', 'status'], ); assert.deepEqual( cockpit.parameters.properties.targetLayer.enum, ['flights', 'military', 'ais-live-vessels', 'military-installations'], 'the layer filter must match the four Context cohorts exactly', ); // aircraftClass is deliberately open (free-form class names), but still typed. assert.equal(cockpit.parameters.properties.aircraftClass.type, 'string'); assert.equal(cockpit.parameters.properties.aircraftClass.enum, undefined); }); test('the edited existing tools changed exactly as intended', () => { const byName = new Map(realtimeTools().map((tool) => [tool.name, tool])); // Edit 1: the Context panel became voice-addressable alongside set_context_mode. const panel = byName.get('set_panel_open'); assert.deepEqual( panel.parameters.properties.panelId.enum, ['data-panel', 'location-bar', 'control-panel', 'cctv-panel', 'radio-panel', 'scene-panel', 'pp-toggles', 'global-context-panel', 'weather-panel', 'recent-imagery-panel', 'voice-settings', 'voice-preview', 'provider-settings'], ); assert.deepEqual(panel.parameters.required, ['panelId', 'open']); // Edit 2: description only — the view state now reports Context and Cockpit. const viewState = byName.get('get_current_view_state'); assert.match(viewState.description, /Context, Cockpit/); assert.deepEqual(viewState.parameters.properties, {}); // Edit 3: dependent multi-tool navigation can wait for the destination view. const location = byName.get('fly_to_location'); assert.equal(location.parameters.properties.waitForArrival.type, 'boolean'); assert.match(location.parameters.properties.waitForArrival.description, /arrived=true/); }); test('no unchanged Realtime tool definition drifts silently', () => { // Context/Cockpit parity, the dependent-location wait edit, and the retired // `bing-road` stack leaving `set_map_stack`'s enum are the known schema // changes. Everything else must be byte-identical: an unnoticed edit // to a shipped tool changes // model behavior in production with nothing in review to catch it. // // If this fails and the change was deliberate, re-derive the digest and say // in the mic-test brief which tools moved — the session cache busts on any // schema change. const TOUCHED = new Set([ 'set_cyber_sonar', 'set_context_mode', 'control_cockpit', 'set_panel_open', 'set_voice_inactivity_timeout', 'get_current_view_state', 'fly_to_location', 'select_nearest_aircraft', 'set_map_stack', 'analyst_query', 'next_iss_pass', 'next_satellite_pass', // Local ADS-B adds one layer enum value and its common-name mapping. 'set_layer_visibility', // Layer enums generated from the voice layer manifest, and the corrected // track_entity layer hint. 'show_data_layers_menu', 'get_entity_context', 'track_entity', // Point-and-ask: 'pointer' sentinels and referent arguments. 'annotate_map', ]); const unchanged = realtimeTools() .filter((tool) => !TOUCHED.has(tool.name)) .sort((a, b) => a.name.localeCompare(b.name)) .map((tool) => structuredClone(tool)) .filter((tool) => tool.name !== 'set_cyber_sonar'); // Cyber adds one HUD choice and the sonar tool; retain the existing pin for every legacy field. const hudLayout = unchanged.find((tool) => tool.name === 'set_hud').parameters.properties.layout; assert.deepEqual(hudLayout.enum, ['tactical', 'operator', 'minimal', 'cyber']); hudLayout.enum = hudLayout.enum.filter((layout) => layout !== 'cyber'); // Globe rotation adds one motion and clarifies the two affected descriptions. // Check those edits explicitly, then keep the legacy pin for every other field. const camera = unchanged.find((tool) => tool.name === 'move_camera'); assert.deepEqual(camera.parameters.properties.motion.enum, [ 'orbit', 'pan', 'tilt', 'rotate', 'rotate_globe', 'stop', ]); assert.equal( camera.description, 'Direct the camera like a drone operator: orbit the current view target, pan, tilt, or rotate — one bounded nudge (mode=once) or continuous motion until stopped (mode=continuous). Continuous motion also stops on any manual camera input or when a navigation tool runs. Say the RESULTING state when confirming ("Orbiting slowly"). Globe rotation (rotate_globe) spins Earth right continuously by default; explicit direction, mode and speed take precedence. It checks fit and may refuse.', ); assert.equal( camera.parameters.properties.direction.description, 'Required for pan/tilt/rotate; orbit/rotate_globe default right.', ); assert.equal(camera.parameters.properties.mode.description, 'once = bounded eased nudge; continuous = until stop/manual input. rotate_globe defaults continuous; other motions default once.'); camera.parameters.properties.mode.description = 'once = bounded eased nudge (default); continuous = until stop/manual input.'; camera.parameters.properties.motion.enum = camera.parameters.properties.motion.enum.filter( (motion) => motion !== 'rotate_globe', ); camera.description = 'Direct the camera like a drone operator: orbit the current view target, pan, tilt, or rotate — one bounded nudge (mode=once) or continuous motion until stopped (mode=continuous). Continuous motion also stops on any manual camera input or when a navigation tool runs. Say the RESULTING state when confirming ("Orbiting slowly").'; camera.parameters.properties.direction.description = 'Required except for orbit (defaults right/clockwise) and stop.'; // Snow is retired from set_visual_style; restore it to keep the legacy pin. const visualStyle = unchanged.find( (tool) => tool.name === 'set_visual_style', ).parameters.properties.style; assert.deepEqual(visualStyle.enum, [ 'normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir', ]); visualStyle.enum = [...visualStyle.enum, 'snow']; assert.equal(unchanged.length, 14); const digest = createHash('sha256') .update(JSON.stringify(unchanged)) .digest('hex') .slice(0, 16); // Analyst additions and ISS wording correction are explicitly excluded above; all other tool definitions retain their pin. assert.equal(digest, '25d1b902ecaa22fb', 'an unchanged Realtime tool definition drifted'); }); test('Radio volume and mission speed share the Sharpen slider visual language', () => { for (const id of ['cockpit-radio-volume', 'context-radio-mini-volume', 'radio-volume', 'sdr-volume']) { assert.match( html, new RegExp(`id="${id}"[^>]*class="gev-quantitative-slider"[^>]*type="range"`), ); } assert.match( rocketLaunches, /id="space-mission-replay-speed" class="gev-quantitative-slider" type="range" min="0\.25" max="4" step="0\.25" value="1"/, ); assert.match(rocketLaunches, /class="gev-slider-value"[^>]*data-mission-replay-speed-output/); assert.match(css, /\.gev-quantitative-slider\s*\{[\s\S]*?min-width: 0;[\s\S]*?height: 18px;/); assert.match(css, /\.gev-quantitative-slider::-webkit-slider-runnable-track\s*\{[\s\S]*?height: 3px;[\s\S]*?background: rgba\(255, 255, 255, 0\.08\);/); assert.match(css, /\.gev-quantitative-slider::-webkit-slider-thumb\s*\{[\s\S]*?width: 10px;[\s\S]*?height: 10px;[\s\S]*?border-radius: 50%;[\s\S]*?background: var\(--accent\);/); assert.match(css, /\.gev-quantitative-slider:focus-visible\s*\{[\s\S]*?outline: 1px solid/); assert.match(css, /\.gev-quantitative-slider:disabled\s*\{[\s\S]*?opacity: 0\.42;[\s\S]*?cursor: not-allowed;/); assert.match(css, /\.gev-slider-value\s*\{[\s\S]*?color: var\(--accent\);[\s\S]*?font-size: 9px;/); assert.doesNotMatch(css, /#space-mission-panel \[data-mission-replay-speed\]::-webkit-slider-thumb/); }); test('Radio is nested inside Context with separate disclosure and power controls', () => { const contextStart = html.indexOf('id="global-context-panel"'); const radioStart = html.indexOf('id="radio-panel"'); const contextEnd = html.indexOf('\n ', contextStart); assert.ok(contextStart >= 0 && radioStart > contextStart && radioStart < contextEnd); assert.match(html, /id="radio-panel"[^>]*data-panel-id="radio-panel"/); assert.match(html, /aria-label="Radio playback"/); assert.match(html, /id="context-radio-toggle-btn"[^>]*aria-expanded="false"[^>]*aria-controls="context-radio-mini"/); assert.doesNotMatch(html, /id="context-radio-toggle-btn"[^>]*aria-pressed=/); assert.match(html, /id="context-radio-mini"[^>]*aria-label="Compact Radio controls"[^>]*hidden/); assert.match(html, /id="context-radio-mini-enable-btn"[^>]*aria-pressed="false"/); assert.match(html, /id="context-radio-details-btn"[^>]*aria-expanded="false"[^>]*aria-controls="radio-panel"/); assert.match(html, /id="context-radio-details-btn"[\s\S]*?