mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
refactor(voice): isolate gemini live provider core
This commit is contained in:
commit
10916227cb
655 files changed
+45946
-26483
No files matched your search
+187
-113
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user