refactor(voice): isolate gemini live provider core

This commit is contained in:
manjunath committed 2026-10-19 21:40:40 +05:30
commit 10916227cb
655 files changed
+45946 -26483

No files matched your search

+187 -113
View File
@@ -6,9 +6,10 @@ import {
voiceCardView,
} from './voiceCardPresentation.js';
import { createVoiceSession } from './session.js';
import { createRealtimeSession } from './realtimeSession.js';
import { realtimeSessionEvent } from './realtimeEvents.js';
import { createVoiceCommands } from './sessionCommands.js';
import { VoiceCardControls } from './voiceCard.js';
import { describeUnsavedWork } from '../i18n/index.js';
function deferred() {
let resolve;
@@ -301,17 +302,24 @@ test('common button and annotation bindings work with an alternate adapter and c
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, 1);
assert.equal(calls, 2);
} finally {
globalThis.window = previous;
}
@@ -435,122 +443,188 @@ test('a late rejection cannot settle a newer turn card', async () => {
f.session.destroy();
});
test('a supplied adapter retains cost controls without a provider selector', () => {
const previous = globalThis.window;
globalThis.window = new EventTarget();
const button = new EventTarget();
button.setAttribute = () => {};
function startReentrancyFixture() {
let starts = 0;
let stops = 0;
const settings = [];
const session = createVoiceSession({
runner: async () => ({ ok: true }),
createAdapter: ({ emit }) => ({
async start(options) {
starts++;
settings.push(options);
emit({ type: 'state', state: 'listening' });
},
stop() {
stops++;
},
sendText() {},
sendMapEvent() {},
}),
});
return { session, settings, starts: () => starts, stops: () => stops };
}
for (const action of ['stop', 'destroy']) {
test(`initial connecting observer ${action} prevents adapter acquisition`, async () => {
const f = startReentrancyFixture();
const unsubscribe = f.session.subscribe((event) => {
if (event.type === 'state' && event.state === 'connecting') {
unsubscribe();
f.session[action]();
}
});
await f.session.start();
assert.equal(
f.starts(),
0,
'cancelled attempt must not call adapter.start',
);
assert.equal(f.stops(), 1);
assert.equal(f.session.state, 'idle');
if (action === 'stop') {
await f.session.start();
assert.equal(f.starts(), 1, 'later explicit Start remains usable');
assert.equal(f.session.state, 'listening');
}
f.session.destroy();
});
}
test('preserveStatus Stop still prevents adapter acquisition', async () => {
const f = startReentrancyFixture();
const unsubscribe = f.session.subscribe((event) => {
if (event.type === 'state' && event.state === 'connecting') {
unsubscribe();
f.session.stop({ preserveStatus: true });
}
});
await f.session.start();
assert.equal(f.starts(), 0);
assert.equal(
f.session.state,
'connecting',
'preserved state is not ownership',
);
f.session.stop();
await f.session.start();
assert.equal(f.starts(), 1);
f.session.destroy();
});
test('Stop and explicit reentrant successor start acquire only for the successor', async () => {
const f = startReentrancyFixture();
let successor;
const unsubscribe = f.session.subscribe((event) => {
if (event.type === 'state' && event.state === 'connecting') {
unsubscribe();
f.session.stop();
successor = f.session.start();
}
});
await f.session.start();
await successor;
assert.equal(f.starts(), 1);
assert.equal(f.session.state, 'listening');
f.session.destroy();
});
test('ordinary start forwards caller settings once and remains stoppable', async () => {
const f = startReentrancyFixture();
const settings = { pushToTalk: true };
await f.session.start(settings);
assert.equal(f.settings.length, 1);
assert.equal(f.settings[0], settings);
assert.equal(f.starts(), 1);
assert.equal(f.session.state, 'listening');
f.session.stop();
assert.equal(f.session.state, 'idle');
f.session.destroy();
});
for (const emitsIdle of [false, true]) {
for (const outcome of ['normal', 'preserveStatus', 'throw']) {
test(`stop preserves custom adapter contract: emitsIdle=${emitsIdle}, ${outcome}`, async (t) => {
let adapter;
const calls = [];
const events = [];
const failure = new Error('Controlled stop failure');
const session = createVoiceSession({
runner: async () => ({}),
createAdapter: ({ emit }) =>
(adapter = {
async start() {
emit({ type: 'state', state: 'listening' });
},
stop(settings = {}) {
calls.push({ receiver: this, settings });
if (settings.removeUi) return;
if (emitsIdle && !settings.preserveStatus)
emit({ type: 'state', state: 'idle' });
if (outcome === 'throw') throw failure;
return 'adapter-only return';
},
sendText() {},
sendMapEvent() {},
}),
});
t.after(() => session.destroy());
session.subscribe((event) => events.push(event));
await session.start();
events.length = 0;
const settings =
outcome === 'preserveStatus' ? { preserveStatus: true } : {};
if (outcome === 'throw')
assert.throws(
() => session.stop(settings),
(error) => error === failure,
);
else assert.equal(session.stop(settings), undefined);
assert.equal(calls[0].receiver, adapter);
assert.equal(calls[0].settings, settings);
assert.equal(
session.state,
outcome === 'preserveStatus' ? 'listening' : 'idle',
);
assert.equal(
events.filter(
(event) => event.type === 'state' && event.state === 'idle',
).length,
outcome === 'preserveStatus' ? 0 : 1,
);
});
}
}
test('actual default OpenAI adapter stop publishes one common idle', (t) => {
const events = [];
const ui = {
root: { dataset: {}, remove() {} },
button,
root: {
dataset: {},
querySelectorAll: () => [],
classList: { remove() {} },
remove() {},
},
status: {},
detail: {},
providerField: { hidden: true, remove() {} },
providerSelect: new EventTarget(),
tierButton: { hidden: true },
costValue: { hidden: true },
};
let controls;
try {
controls = createVoiceCommands({
runner: async () => ({ ok: true }),
createControl: () => ui,
createSession: () => ({
capabilities: { costControls: true, pushToTalk: true },
start() {},
stop() {},
sendText() {},
sendMapEvent() {},
}),
});
assert.equal(ui.providerField.hidden, true);
assert.equal(ui.tierButton.hidden, false);
assert.equal(ui.costValue.hidden, false);
} finally {
controls?.session.destroy();
globalThis.window = previous;
}
});
test('session stamps host controls instead of accepting a tool caller capability claim', async () => {
for (const available of [false, true]) {
let hooks;
let received;
const session = createVoiceSession({
providerDropdown: () => available,
runner: async (_name, _args, options) => {
received = options;
return { ok: true };
},
createAdapter(value) {
hooks = value;
return {
start() {
hooks.emit({ type: 'state', state: 'listening' });
},
stop() {},
sendText() {},
sendMapEvent() {},
};
},
});
await session.start();
await hooks.runAction(
'get_current_view_state',
{},
{ providerDropdown: !available },
);
assert.equal(received.providerDropdown, available);
session.destroy();
}
});
test('provider policy revokes resources while preserving committed results and suppressing progress', async () => {
const gate = deferred();
const policy = new AbortController();
let received;
const events = [];
const f = fixture(async (_name, _args, options) => {
received = options;
await gate.promise;
options.progress({ step: 'late', label: 'late' });
return { ok: true, changed: true, action: 'committed' };
const runner = async () => ({});
const session = createVoiceSession({
runner,
createAdapter: (hooks) =>
createRealtimeSession({ ...hooks, runner, ui, debugSink() {} }),
});
f.session.subscribe((event) => events.push(event));
await f.session.start();
const pending = f.hooks.runAction(
'committed',
{},
{ providerPolicySignal: policy.signal },
);
policy.abort();
assert.equal(received.signal.aborted, true);
assert.equal(received.isCurrent(), false);
gate.resolve();
assert.equal((await pending).changed, true);
t.after(() => session.destroy());
session.subscribe((event) => events.push(event));
// Exercise the real stop path without acquiring microphone/WebRTC resources.
session.adapter.controller.setStatus('listening', 'Controlled active state');
events.length = 0;
session.stop();
assert.equal(
events.some((e) => e.type === 'progress'),
false,
events.filter((event) => event.type === 'state' && event.state === 'idle')
.length,
1,
);
f.session.destroy();
});
test('Stop still revokes a committed result while provider policy signal exists', async () => {
const gate = deferred();
const policy = new AbortController();
const f = fixture(async () => {
await gate.promise;
return { ok: true, changed: true };
});
await f.session.start();
const pending = f.hooks.runAction(
'committed',
{},
{ providerPolicySignal: policy.signal },
);
policy.abort();
f.session.stop();
gate.resolve();
await assert.rejects(pending, { name: 'AbortError' });
f.session.destroy();
assert.equal(session.state, 'idle');
assert.equal(session.adapter.controller.status, 'idle');
});