






















@@ -1,7 +1,88 @@
1-import { describe, expect, it } from "vitest";
1+import { beforeEach, describe, expect, it, vi } from "vitest";
22import { buildOpenAIRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js";
334+const { FakeWebSocket } = vi.hoisted(() => {
5+type Listener = (...args: unknown[]) => void;
6+7+class MockWebSocket {
8+static readonly OPEN = 1;
9+static readonly CLOSED = 3;
10+static instances: MockWebSocket[] = [];
11+12+readonly listeners = new Map<string, Listener[]>();
13+readyState = 0;
14+sent: string[] = [];
15+closed = false;
16+17+constructor() {
18+MockWebSocket.instances.push(this);
19+}
20+21+on(event: string, listener: Listener): this {
22+const listeners = this.listeners.get(event) ?? [];
23+listeners.push(listener);
24+this.listeners.set(event, listeners);
25+return this;
26+}
27+28+emit(event: string, ...args: unknown[]): void {
29+for (const listener of this.listeners.get(event) ?? []) {
30+listener(...args);
31+}
32+}
33+34+send(payload: string): void {
35+this.sent.push(payload);
36+}
37+38+close(code?: number, reason?: string): void {
39+this.closed = true;
40+this.readyState = MockWebSocket.CLOSED;
41+this.emit("close", code ?? 1000, Buffer.from(reason ?? ""));
42+}
43+}
44+45+return { FakeWebSocket: MockWebSocket };
46+});
47+48+vi.mock("ws", () => ({
49+default: FakeWebSocket,
50+}));
51+52+type FakeWebSocketInstance = InstanceType<typeof FakeWebSocket>;
53+type SentRealtimeEvent = {
54+type: string;
55+audio?: string;
56+session?: {
57+type?: string;
58+audio?: {
59+input?: {
60+format?: { type?: string };
61+transcription?: {
62+model?: string;
63+language?: string;
64+prompt?: string;
65+};
66+turn_detection?: {
67+type?: string;
68+threshold?: number;
69+prefix_padding_ms?: number;
70+silence_duration_ms?: number;
71+};
72+};
73+};
74+};
75+};
76+77+function parseSent(socket: FakeWebSocketInstance): SentRealtimeEvent[] {
78+return socket.sent.map((payload) => JSON.parse(payload) as SentRealtimeEvent);
79+}
80+481describe("buildOpenAIRealtimeTranscriptionProvider", () => {
82+beforeEach(() => {
83+FakeWebSocket.instances = [];
84+});
85+586it("normalizes OpenAI config defaults", () => {
687const provider = buildOpenAIRealtimeTranscriptionProvider();
788const resolved = provider.resolveConfig?.({
@@ -70,4 +151,88 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
70151const provider = buildOpenAIRealtimeTranscriptionProvider();
71152expect(provider.aliases).toContain("openai-realtime");
72153});
154+155+it("waits for the OpenAI session update before draining audio", async () => {
156+const provider = buildOpenAIRealtimeTranscriptionProvider();
157+const session = provider.createSession({
158+providerConfig: {
159+apiKey: "sk-test", // pragma: allowlist secret
160+language: "en",
161+model: "gpt-4o-transcribe",
162+prompt: "expect OpenClaw product names",
163+silenceDurationMs: 900,
164+vadThreshold: 0.45,
165+},
166+});
167+168+const connecting = session.connect();
169+const socket = FakeWebSocket.instances[0];
170+if (!socket) {
171+throw new Error("expected session to create a websocket");
172+}
173+174+socket.readyState = FakeWebSocket.OPEN;
175+socket.emit("open");
176+session.sendAudio(Buffer.from("before-ready"));
177+178+expect(session.isConnected()).toBe(false);
179+expect(parseSent(socket)).toEqual([
180+{
181+type: "transcription_session.update",
182+session: {
183+type: "transcription",
184+audio: {
185+input: {
186+format: { type: "audio/pcmu" },
187+transcription: {
188+model: "gpt-4o-transcribe",
189+language: "en",
190+prompt: "expect OpenClaw product names",
191+},
192+turn_detection: {
193+type: "server_vad",
194+threshold: 0.45,
195+prefix_padding_ms: 300,
196+silence_duration_ms: 900,
197+},
198+},
199+},
200+},
201+},
202+]);
203+204+socket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
205+await connecting;
206+207+expect(session.isConnected()).toBe(true);
208+expect(parseSent(socket)).toEqual([
209+{
210+type: "transcription_session.update",
211+session: {
212+type: "transcription",
213+audio: {
214+input: {
215+format: { type: "audio/pcmu" },
216+transcription: {
217+model: "gpt-4o-transcribe",
218+language: "en",
219+prompt: "expect OpenClaw product names",
220+},
221+turn_detection: {
222+type: "server_vad",
223+threshold: 0.45,
224+prefix_padding_ms: 300,
225+silence_duration_ms: 900,
226+},
227+},
228+},
229+},
230+},
231+{
232+type: "input_audio_buffer.append",
233+audio: Buffer.from("before-ready").toString("base64"),
234+},
235+]);
236+session.close();
237+});
73238});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。