Files
gods-eye-view/src/voice/session.test.mjs
T
German KhokhlovandClaude Opus 5.5 20f4cf3763 i18n: fix the LGTM review findings and tidy the branch texts
- i18n scanner: lastError sink, no false sinks on defaults and ternaries
- shared src/i18n/marker.js for modules that only mark strings
- wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js
- drop review IDs, change history and stale references from comments and docs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 21:00:28 +04:00

444 lines
12 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
initialVoiceCardState,
reduceVoiceCard,
voiceCardView,
} from './voiceCardPresentation.js';
import { createVoiceSession } from './session.js';
import { realtimeSessionEvent } from './realtimeEvents.js';
import { createVoiceCommands } from './sessionCommands.js';
import { describeUnsavedWork } from '../i18n/index.js';
function deferred() {
let resolve;
const promise = new Promise((done) => {
resolve = done;
});
return { promise, resolve };
}
function fixture(
runner = async (name, args) => ({ ok: true, name, ...args }),
signal,
) {
let hooks;
const sent = [];
let stops = 0;
const session = createVoiceSession({
runner,
signal,
createAdapter(options) {
hooks = options;
return {
start() {
hooks.emit({ type: 'state', state: 'listening' });
},
stop() {
stops++;
hooks.emit({ type: 'state', state: 'idle' });
},
sendText(text) {
sent.push(['text', text]);
return true;
},
sendMapEvent(event) {
sent.push(['map', event]);
return true;
},
};
},
});
return {
session,
hooks,
sent,
get stops() {
return stops;
},
};
}
test('alternate protocol shares session events and action execution without Realtime', async () => {
const f = fixture();
const events = [];
f.session.subscribe(() => {
throw new Error('observer');
});
const unsubscribe = f.session.subscribe((event) => events.push(event));
await f.session.start();
assert.equal(f.session.isActive(), true);
f.hooks.emit({
type: 'transcript',
role: 'user',
text: 'London',
final: true,
});
assert.deepEqual(
await f.hooks.runAction('fly_to_location', { query: 'London' }),
{ ok: true, name: 'fly_to_location', query: 'London' },
);
f.hooks.emit({ type: 'completion', status: 'completed' });
f.session.sendText('Next');
f.session.sendMapEvent({
type: 'map_annotation_outline',
status: 'resolved',
});
assert.deepEqual(
events.map((event) => event.type),
[
'state',
'state',
'transcript',
'action-call',
'action-result',
'completion',
],
);
assert.equal(f.sent.length, 2);
unsubscribe();
f.session.stop();
assert.equal(f.session.state, 'idle');
await f.session.start();
assert.equal(f.session.state, 'listening');
f.session.destroy();
assert.equal(f.session.disposed, true);
assert.equal(f.session.sendText('stale'), false);
});
test('stop, interruption and lifetime abort cancel actions and suppress stale results', async () => {
for (const cancel of ['stop', 'interruption', 'lifetime']) {
const result = deferred();
const lifetime = new AbortController();
let observed;
const f = fixture(async (_name, _args, options) => {
observed = options;
return result.promise;
}, lifetime.signal);
const events = [];
f.session.subscribe((event) => events.push(event));
await f.session.start();
const pending = f.hooks.runAction('fly_to_location', {});
if (cancel === 'stop') f.session.stop();
if (cancel === 'interruption')
f.hooks.emit({ type: 'interruption', reason: 'user-speech' });
if (cancel === 'lifetime') lifetime.abort();
assert.equal(observed.signal.aborted, true);
assert.equal(observed.isCurrent(), false);
result.resolve({ ok: true });
await assert.rejects(pending, { name: 'AbortError' });
assert.equal(
events.some((event) => event.type === 'action-result'),
false,
);
f.session.destroy();
}
});
test('caller cancellation and sibling actions preserve independent ownership', async () => {
const first = deferred();
const f = fixture(async (name) =>
name === 'first' ? first.promise : { ok: true },
);
await f.session.start();
const signal = new AbortController();
const pending = f.hooks.runAction('first', {}, { signal: signal.signal });
signal.abort();
assert.deepEqual(await f.hooks.runAction('second', {}), { ok: true });
first.resolve({ ok: true });
await assert.rejects(pending, { name: 'AbortError' });
f.session.destroy();
});
test('stale connect failure cannot stop a replacement session', async () => {
let reject;
let calls = 0;
let hooks;
const session = createVoiceSession({
runner: async () => {},
createAdapter(options) {
hooks = options;
return {
start() {
if (++calls === 1)
return new Promise((_resolve, fail) => {
reject = fail;
});
hooks.emit({ type: 'state', state: 'listening' });
},
stop() {},
sendText() {},
sendMapEvent() {},
};
},
});
const old = session.start();
session.stop();
await session.start();
reject(new Error('old connection failed'));
await old;
assert.equal(session.state, 'listening');
session.destroy();
});
test('wire events project only normalized transcript, interruption and completion fields', () => {
assert.deepEqual(
realtimeSessionEvent({
type: 'response.output_audio_transcript.delta',
delta: 'Hello',
item_id: 'i',
response_id: 'r',
extra: 'ignored',
}),
{
type: 'transcript',
role: 'assistant',
text: 'Hello',
final: false,
itemId: 'i',
responseId: 'r',
},
);
assert.deepEqual(
realtimeSessionEvent({
type: 'conversation.item.input_audio_transcription.completed',
transcript: 'Go',
}),
{
type: 'transcript',
role: 'user',
text: 'Go',
final: true,
itemId: null,
},
);
assert.equal(
realtimeSessionEvent({ type: 'response.output_text.done', text: 'Done' })
.final,
true,
);
assert.equal(
realtimeSessionEvent({ type: 'input_audio_buffer.speech_started' }).type,
'interruption',
);
assert.equal(
realtimeSessionEvent({
type: 'response.done',
response: { id: 'r', status: 'cancelled' },
}).status,
'cancelled',
);
assert.equal(
realtimeSessionEvent({
type: 'session.created',
session: { secret: 'excluded' },
}),
null,
);
});
test('common button and annotation bindings work with an alternate adapter and clean up', async () => {
const previous = globalThis.window;
globalThis.window = {};
try {
const button = new EventTarget();
button.setAttribute = () => {};
let removed = 0;
let subscribed = null;
let calls = 0;
const events = [];
const ui = {
button,
root: {
dataset: {},
remove() {
removed++;
},
},
status: {},
detail: {},
tierButton: {},
costValue: {},
helpDetail: {},
};
const lifetime = new AbortController();
const controls = createVoiceCommands({
runner: async () => {
calls++;
return { ok: true };
},
signal: lifetime.signal,
createControl: () => ui,
annotations: {
onOutlineEvent(listener) {
subscribed = listener;
return () => {
subscribed = null;
};
},
},
createSession({ emit, runAction }) {
return {
async start() {
emit({ type: 'state', state: 'listening', detail: 'Ready' });
await runAction('fly_to_location', { query: 'London' });
},
stop() {
emit({ type: 'state', state: 'idle' });
},
sendText() {},
sendMapEvent(event) {
events.push(event);
},
};
},
});
button.dispatchEvent(new Event('click'));
await new Promise((done) => setTimeout(done, 0));
assert.equal(calls, 1);
assert.equal(ui.tierButton.hidden, true);
assert.equal(ui.costValue.hidden, true);
assert.equal(ui.helpDetail.textContent, 'Activate to toggle voice');
assert.equal(ui.status.textContent, 'LISTENING');
assert.equal(ui.detail.textContent, 'Ready');
// Switching language (a reload) asks first during a session.
assert.deepEqual(describeUnsavedWork(), ['the active voice session']);
subscribed({ status: 'resolved' });
assert.deepEqual(events, [
{ type: 'map_annotation_outline', status: 'resolved' },
]);
button.dispatchEvent(new Event('click'));
assert.equal(controls.state, 'idle');
assert.deepEqual(describeUnsavedWork(), []);
button.dispatchEvent(new Event('click'));
await new Promise((done) => setTimeout(done, 0));
assert.deepEqual(describeUnsavedWork(), ['the active voice session']);
lifetime.abort();
assert.deepEqual(describeUnsavedWork(), [], 'disposal unregisters');
assert.equal(subscribed, null);
assert.equal(removed, 1);
button.dispatchEvent(new Event('click'));
assert.equal(calls, 2);
} finally {
globalThis.window = previous;
}
});
test('late actions cannot run after stop and failed startup closes the adapter', async () => {
let calls = 0;
const f = fixture(async () => {
calls++;
});
await f.session.start();
f.session.stop();
await assert.rejects(f.hooks.runAction('fly_to_location', {}), {
name: 'AbortError',
});
assert.equal(calls, 0);
f.session.destroy();
let stopped = 0;
const session = createVoiceSession({
runner: async () => {},
createAdapter({ emit }) {
return {
start() {
emit({ type: 'state', state: 'connecting' });
throw new Error('Connection refused');
},
stop() {
stopped++;
emit({ type: 'state', state: 'idle' });
},
sendText() {},
sendMapEvent() {},
};
},
});
await session.start();
assert.equal(session.state, 'error');
assert.equal(stopped, 1);
session.destroy();
});
test('action events carry the call id; progress after cancellation is dropped', async () => {
let report;
const f = fixture(async (name, args, options) => {
options.progress({ step: 'resolve', label: 'Zilker' });
report = options.progress;
return { ok: true };
});
const events = [];
f.session.subscribe((event) => events.push(event));
await f.session.start();
await f.hooks.runAction('annotate_map', {}, { callId: 'call_7' });
const actionEvents = events.filter(
(e) => e.type.startsWith('action-') || e.type === 'progress',
);
assert.deepEqual(
actionEvents.map((e) => [e.type, e.callId]),
[
['action-call', 'call_7'],
['progress', 'call_7'],
['action-result', 'call_7'],
],
);
f.hooks.emit({ type: 'interruption' });
report({ step: 'outline' });
assert.equal(events.filter((e) => e.type === 'progress').length, 1);
});
test('a rejected current action settles its card and preserves the rejection', async () => {
const failure = new Error('Unknown panel');
const f = fixture(async () => {
throw failure;
});
let card = initialVoiceCardState();
const events = [];
f.session.subscribe((event) => {
events.push(event);
card = reduceVoiceCard(card, event);
});
await f.session.start();
await assert.rejects(
f.hooks.runAction(
'set_panel_open',
{ panelId: 'missing' },
{ callId: 'failed-call' },
),
(error) => error === failure,
);
f.hooks.emit({ type: 'completion', status: 'completed' });
assert.equal(voiceCardView(card).busy, false);
assert.equal(card.steps[0].status, 'failed');
const result = events.find((event) => event.type === 'action-result');
assert.equal(result.callId, 'failed-call');
assert.equal(result.result.ok, false);
f.session.destroy();
});
test('a late rejection cannot settle a newer turn card', async () => {
let reject;
const f = fixture(
() =>
new Promise((_, fail) => {
reject = fail;
}),
);
const events = [];
f.session.subscribe((event) => events.push(event));
await f.session.start();
const pending = f.hooks.runAction(
'set_panel_open',
{},
{ callId: 'old-call' },
);
f.hooks.emit({ type: 'interruption', reason: 'user-speech' });
reject(new Error('late failure'));
await assert.rejects(pending, /late failure/);
assert.equal(
events.some((event) => event.type === 'action-result'),
false,
);
f.session.destroy();
});