Skip to content

Commit 3414023

Browse files
committed
test: cover OpenAI initial-greeting suppression and restore (#85846)
1 parent 680308a commit 3414023

2 files changed

Lines changed: 269 additions & 0 deletions

File tree

extensions/openai/realtime-voice-provider.test.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,176 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
864864
);
865865
});
866866

867+
it("preserves autoRespondToAudio from raw provider config", () => {
868+
const provider = buildOpenAIRealtimeVoiceProvider();
869+
const resolved = provider.resolveConfig?.({
870+
cfg: {} as never,
871+
rawConfig: {
872+
providers: {
873+
openai: {
874+
autoRespondToAudio: false,
875+
},
876+
},
877+
},
878+
});
879+
880+
expect(resolved).toEqual({ autoRespondToAudio: false });
881+
});
882+
883+
it("suppresses server-VAD auto-response during the initial greeting and restores it on response.done", async () => {
884+
const provider = buildOpenAIRealtimeVoiceProvider();
885+
const bridge = provider.createBridge({
886+
providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
887+
autoRespondToAudio: false,
888+
restoreAutoRespondToAudioAfterInitialGreeting: true,
889+
instructions: "Greet the caller.",
890+
onAudio: vi.fn(),
891+
onClearAudio: vi.fn(),
892+
});
893+
const connecting = bridge.connect();
894+
const socket = FakeWebSocket.instances[0];
895+
if (!socket) {
896+
throw new Error("expected bridge to create a websocket");
897+
}
898+
899+
socket.readyState = FakeWebSocket.OPEN;
900+
socket.emit("open");
901+
socket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
902+
await connecting;
903+
904+
expectRecordFields(
905+
requireNestedRecord(requireSession(socket, 0), ["audio", "input", "turn_detection"]),
906+
"initial turn detection",
907+
{
908+
create_response: false,
909+
interrupt_response: false,
910+
},
911+
);
912+
913+
bridge.triggerGreeting?.("Say hi for issue 85846.");
914+
915+
const sentTypesAfterGreeting = parseSent(socket).map((event) => event.type);
916+
expect(sentTypesAfterGreeting).toEqual([
917+
"session.update",
918+
"conversation.item.create",
919+
"response.create",
920+
]);
921+
922+
socket.emit(
923+
"message",
924+
Buffer.from(JSON.stringify({ type: "response.created", response: { id: "resp_1" } })),
925+
);
926+
socket.emit(
927+
"message",
928+
Buffer.from(JSON.stringify({ type: "response.done", response: { id: "resp_1" } })),
929+
);
930+
931+
const finalTypes = parseSent(socket).map((event) => event.type);
932+
expect(finalTypes).toEqual([
933+
"session.update",
934+
"conversation.item.create",
935+
"response.create",
936+
"session.update",
937+
]);
938+
expectRecordFields(
939+
requireNestedRecord(requireSession(socket, 3), ["audio", "input", "turn_detection"]),
940+
"restored turn detection",
941+
{
942+
create_response: true,
943+
interrupt_response: true,
944+
},
945+
);
946+
947+
socket.emit(
948+
"message",
949+
Buffer.from(JSON.stringify({ type: "response.created", response: { id: "resp_2" } })),
950+
);
951+
socket.emit(
952+
"message",
953+
Buffer.from(JSON.stringify({ type: "response.done", response: { id: "resp_2" } })),
954+
);
955+
956+
expect(parseSent(socket).filter((event) => event.type === "session.update")).toHaveLength(2);
957+
});
958+
959+
it("restores server-VAD auto-response after an initial greeting is cancelled", async () => {
960+
const provider = buildOpenAIRealtimeVoiceProvider();
961+
const bridge = provider.createBridge({
962+
providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
963+
autoRespondToAudio: false,
964+
restoreAutoRespondToAudioAfterInitialGreeting: true,
965+
onAudio: vi.fn(),
966+
onClearAudio: vi.fn(),
967+
});
968+
const connecting = bridge.connect();
969+
const socket = FakeWebSocket.instances[0];
970+
if (!socket) {
971+
throw new Error("expected bridge to create a websocket");
972+
}
973+
974+
socket.readyState = FakeWebSocket.OPEN;
975+
socket.emit("open");
976+
socket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
977+
await connecting;
978+
979+
bridge.triggerGreeting?.("Say hi.");
980+
socket.emit(
981+
"message",
982+
Buffer.from(JSON.stringify({ type: "response.created", response: { id: "resp_1" } })),
983+
);
984+
socket.emit(
985+
"message",
986+
Buffer.from(JSON.stringify({ type: "response.cancelled", response: { id: "resp_1" } })),
987+
);
988+
989+
const finalTypes = parseSent(socket).map((event) => event.type);
990+
expect(finalTypes.filter((type) => type === "session.update")).toHaveLength(2);
991+
expectRecordFields(
992+
requireNestedRecord(requireSession(socket, finalTypes.lastIndexOf("session.update")), [
993+
"audio",
994+
"input",
995+
"turn_detection",
996+
]),
997+
"restored turn detection",
998+
{
999+
create_response: true,
1000+
interrupt_response: true,
1001+
},
1002+
);
1003+
});
1004+
1005+
it("does not send a restore session.update when restoreAutoRespondToAudioAfterInitialGreeting is not set", async () => {
1006+
const provider = buildOpenAIRealtimeVoiceProvider();
1007+
const bridge = provider.createBridge({
1008+
providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
1009+
autoRespondToAudio: false,
1010+
onAudio: vi.fn(),
1011+
onClearAudio: vi.fn(),
1012+
});
1013+
const connecting = bridge.connect();
1014+
const socket = FakeWebSocket.instances[0];
1015+
if (!socket) {
1016+
throw new Error("expected bridge to create a websocket");
1017+
}
1018+
1019+
socket.readyState = FakeWebSocket.OPEN;
1020+
socket.emit("open");
1021+
socket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
1022+
await connecting;
1023+
1024+
bridge.triggerGreeting?.("Say hi.");
1025+
socket.emit(
1026+
"message",
1027+
Buffer.from(JSON.stringify({ type: "response.created", response: { id: "resp_1" } })),
1028+
);
1029+
socket.emit(
1030+
"message",
1031+
Buffer.from(JSON.stringify({ type: "response.done", response: { id: "resp_1" } })),
1032+
);
1033+
1034+
expect(parseSent(socket).filter((event) => event.type === "session.update")).toHaveLength(1);
1035+
});
1036+
8671037
it("can disable realtime response interruption while keeping audio responses enabled", async () => {
8681038
const provider = buildOpenAIRealtimeVoiceProvider();
8691039
const bridge = provider.createBridge({

src/talk/session-runtime.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,103 @@ describe("realtime voice bridge session runtime", () => {
289289
expect(onReady).toHaveBeenCalledWith(session);
290290
expect(onToolCall).toHaveBeenCalledWith(event, session);
291291
});
292+
293+
it("suppresses initial OpenAI auto-response when triggering an explicit greeting", () => {
294+
let request: Parameters<RealtimeVoiceProviderPlugin["createBridge"]>[0] | undefined;
295+
const provider: RealtimeVoiceProviderPlugin = {
296+
id: "openai",
297+
label: "OpenAI",
298+
isConfigured: () => true,
299+
createBridge: (nextRequest) => {
300+
request = nextRequest;
301+
return makeBridge();
302+
},
303+
};
304+
305+
createRealtimeVoiceBridgeSession({
306+
provider,
307+
providerConfig: {},
308+
audioSink: { sendAudio: vi.fn() },
309+
initialGreetingInstructions: "Say hello",
310+
triggerGreetingOnReady: true,
311+
});
312+
313+
const bridgeRequest = expectBridgeRequest(request);
314+
expect(bridgeRequest.autoRespondToAudio).toBe(false);
315+
expect(bridgeRequest.restoreAutoRespondToAudioAfterInitialGreeting).toBe(true);
316+
});
317+
318+
it("respects an explicit autoRespondToAudio:false even when triggering an OpenAI greeting", () => {
319+
let request: Parameters<RealtimeVoiceProviderPlugin["createBridge"]>[0] | undefined;
320+
const provider: RealtimeVoiceProviderPlugin = {
321+
id: "openai",
322+
label: "OpenAI",
323+
isConfigured: () => true,
324+
createBridge: (nextRequest) => {
325+
request = nextRequest;
326+
return makeBridge();
327+
},
328+
};
329+
330+
createRealtimeVoiceBridgeSession({
331+
provider,
332+
providerConfig: {},
333+
audioSink: { sendAudio: vi.fn() },
334+
autoRespondToAudio: false,
335+
initialGreetingInstructions: "Say hello",
336+
triggerGreetingOnReady: true,
337+
});
338+
339+
const bridgeRequest = expectBridgeRequest(request);
340+
expect(bridgeRequest.autoRespondToAudio).toBe(false);
341+
expect(bridgeRequest.restoreAutoRespondToAudioAfterInitialGreeting).toBeUndefined();
342+
});
343+
344+
it("does not enable suppression for non-OpenAI providers triggering greetings", () => {
345+
let request: Parameters<RealtimeVoiceProviderPlugin["createBridge"]>[0] | undefined;
346+
const provider: RealtimeVoiceProviderPlugin = {
347+
id: "google",
348+
label: "Google",
349+
isConfigured: () => true,
350+
createBridge: (nextRequest) => {
351+
request = nextRequest;
352+
return makeBridge();
353+
},
354+
};
355+
356+
createRealtimeVoiceBridgeSession({
357+
provider,
358+
providerConfig: {},
359+
audioSink: { sendAudio: vi.fn() },
360+
initialGreetingInstructions: "Say hello",
361+
triggerGreetingOnReady: true,
362+
});
363+
364+
const bridgeRequest = expectBridgeRequest(request);
365+
expect(bridgeRequest.autoRespondToAudio).toBeUndefined();
366+
expect(bridgeRequest.restoreAutoRespondToAudioAfterInitialGreeting).toBeUndefined();
367+
});
368+
369+
it("does not enable suppression when no initial greeting is triggered on OpenAI", () => {
370+
let request: Parameters<RealtimeVoiceProviderPlugin["createBridge"]>[0] | undefined;
371+
const provider: RealtimeVoiceProviderPlugin = {
372+
id: "openai",
373+
label: "OpenAI",
374+
isConfigured: () => true,
375+
createBridge: (nextRequest) => {
376+
request = nextRequest;
377+
return makeBridge();
378+
},
379+
};
380+
381+
createRealtimeVoiceBridgeSession({
382+
provider,
383+
providerConfig: {},
384+
audioSink: { sendAudio: vi.fn() },
385+
});
386+
387+
const bridgeRequest = expectBridgeRequest(request);
388+
expect(bridgeRequest.autoRespondToAudio).toBeUndefined();
389+
expect(bridgeRequest.restoreAutoRespondToAudioAfterInitialGreeting).toBeUndefined();
390+
});
292391
});

0 commit comments

Comments
 (0)