























@@ -22,6 +22,8 @@ const {
2222sent: string[] = [];
2323closed = false;
2424terminated = false;
25+deferClose = false;
26+deferredClose: (() => void) | undefined;
2527args: unknown[];
26282729constructor(...args: unknown[]) {
@@ -49,13 +51,24 @@ const {
4951close(code?: number, reason?: string): void {
5052this.closed = true;
5153this.readyState = MockWebSocket.CLOSED;
52-this.emit("close", code ?? 1000, Buffer.from(reason ?? ""));
54+const emitClose = () => this.emit("close", code ?? 1000, Buffer.from(reason ?? ""));
55+if (this.deferClose) {
56+this.deferredClose = emitClose;
57+return;
58+}
59+emitClose();
5360}
54615562terminate(): void {
5663this.terminated = true;
5764this.close(1006, "terminated");
5865}
66+67+emitDeferredClose(): void {
68+const emitClose = this.deferredClose;
69+this.deferredClose = undefined;
70+emitClose?.();
71+}
5972}
60736174return {
@@ -924,11 +937,13 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
924937it("treats pre-ready auth errors as a single startup failure", async () => {
925938const provider = buildOpenAIRealtimeVoiceProvider();
926939const onError = vi.fn();
940+const onClose = vi.fn();
927941const bridge = provider.createBridge({
928942providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
929943onAudio: vi.fn(),
930944onClearAudio: vi.fn(),
931945 onError,
946+ onClose,
932947});
933948const connecting = bridge.connect();
934949const socket = FakeWebSocket.instances[0];
@@ -959,10 +974,58 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
959974960975await expect(connecting).rejects.toThrow("Incorrect API key provided");
961976expect(onError).not.toHaveBeenCalled();
977+expect(onClose).not.toHaveBeenCalled();
962978expect(socket.closed).toBe(true);
963979expect(bridge.isConnected()).toBe(false);
964980});
965981982+it("keeps a retried connection ready after delayed startup failure close", async () => {
983+const provider = buildOpenAIRealtimeVoiceProvider();
984+const onClose = vi.fn();
985+const bridge = provider.createBridge({
986+providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
987+onAudio: vi.fn(),
988+onClearAudio: vi.fn(),
989+ onClose,
990+});
991+const failedConnect = bridge.connect();
992+const failedSocket = FakeWebSocket.instances[0];
993+if (!failedSocket) {
994+throw new Error("expected bridge to create a websocket");
995+}
996+failedSocket.deferClose = true;
997+998+failedSocket.readyState = FakeWebSocket.OPEN;
999+failedSocket.emit("open");
1000+failedSocket.emit(
1001+"message",
1002+Buffer.from(
1003+JSON.stringify({
1004+type: "error",
1005+error: { message: "Incorrect API key provided" },
1006+}),
1007+),
1008+);
1009+1010+await expect(failedConnect).rejects.toThrow("Incorrect API key provided");
1011+expect(failedSocket.deferredClose).toBeDefined();
1012+1013+const retryConnect = bridge.connect();
1014+const retrySocket = FakeWebSocket.instances[1];
1015+if (!retrySocket) {
1016+throw new Error("expected bridge retry to create a websocket");
1017+}
1018+retrySocket.readyState = FakeWebSocket.OPEN;
1019+retrySocket.emit("open");
1020+retrySocket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
1021+await retryConnect;
1022+1023+expect(bridge.isConnected()).toBe(true);
1024+failedSocket.emitDeferredClose();
1025+expect(bridge.isConnected()).toBe(true);
1026+expect(onClose).not.toHaveBeenCalled();
1027+});
1028+9661029it("rejects connection when the socket closes before session readiness", async () => {
9671030const provider = buildOpenAIRealtimeVoiceProvider();
9681031const bridge = provider.createBridge({
@@ -984,6 +1047,33 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
9841047expect(bridge.isConnected()).toBe(false);
9851048});
98610491050+it("does not report startup timeout shutdown as a clean close", async () => {
1051+vi.useFakeTimers();
1052+const provider = buildOpenAIRealtimeVoiceProvider();
1053+const onClose = vi.fn();
1054+const bridge = provider.createBridge({
1055+providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
1056+onAudio: vi.fn(),
1057+onClearAudio: vi.fn(),
1058+ onClose,
1059+});
1060+const connecting = bridge.connect();
1061+const socket = FakeWebSocket.instances[0];
1062+if (!socket) {
1063+throw new Error("expected bridge to create a websocket");
1064+}
1065+1066+const timeoutAssertion = expect(connecting).rejects.toThrow(
1067+"OpenAI realtime connection timeout",
1068+);
1069+await vi.advanceTimersByTimeAsync(10_000);
1070+await timeoutAssertion;
1071+expect(socket.terminated).toBe(true);
1072+expect(onClose).not.toHaveBeenCalled();
1073+expect(FakeWebSocket.instances).toHaveLength(1);
1074+expect(bridge.isConnected()).toBe(false);
1075+});
1076+9871077it("can disable automatic audio turn responses for agent-routed voice loops", async () => {
9881078const provider = buildOpenAIRealtimeVoiceProvider();
9891079const bridge = provider.createBridge({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。