@@ -224,6 +224,49 @@ describe("createRealtimeTranscriptionWebSocketSession", () => {
|
224 | 224 | } |
225 | 225 | }); |
226 | 226 | |
| 227 | +it("preserves connect failures when the error callback throws", async () => { |
| 228 | +vi.useFakeTimers(); |
| 229 | +const previousDebugProxyEnabled = process.env.OPENCLAW_DEBUG_PROXY_ENABLED; |
| 230 | +process.env.OPENCLAW_DEBUG_PROXY_ENABLED = "1"; |
| 231 | +const onError = vi.fn((_error: Error) => { |
| 232 | +throw new Error("error observer failed"); |
| 233 | +}); |
| 234 | +const session = createRealtimeTranscriptionWebSocketSession({ |
| 235 | +providerId: "test", |
| 236 | +callbacks: { onError }, |
| 237 | +url: () => new Promise<string>(() => {}), |
| 238 | +connectTimeoutMs: 10, |
| 239 | +connectTimeoutMessage: "test realtime transcription connection timeout", |
| 240 | +readyOnOpen: true, |
| 241 | +sendAudio: (audio, transport) => { |
| 242 | +transport.sendBinary(audio); |
| 243 | +}, |
| 244 | +}); |
| 245 | + |
| 246 | +try { |
| 247 | +const connecting = session.connect(); |
| 248 | +const timeoutAssertion = expect(connecting).rejects.toThrow( |
| 249 | +"test realtime transcription connection timeout", |
| 250 | +); |
| 251 | +await vi.advanceTimersByTimeAsync(10); |
| 252 | + |
| 253 | +await timeoutAssertion; |
| 254 | +expect(session.isConnected()).toBe(false); |
| 255 | +expect(onError).toHaveBeenCalledTimes(1); |
| 256 | +const timeoutError = requireFirstMockArg(onError, "connect timeout error"); |
| 257 | +expect(timeoutError).toBeInstanceOf(Error); |
| 258 | +expect(timeoutError.message).toBe("test realtime transcription connection timeout"); |
| 259 | +} finally { |
| 260 | +session.close(); |
| 261 | +if (previousDebugProxyEnabled === undefined) { |
| 262 | +delete process.env.OPENCLAW_DEBUG_PROXY_ENABLED; |
| 263 | +} else { |
| 264 | +process.env.OPENCLAW_DEBUG_PROXY_ENABLED = previousDebugProxyEnabled; |
| 265 | +} |
| 266 | +vi.useRealTimers(); |
| 267 | +} |
| 268 | +}); |
| 269 | + |
227 | 270 | it("does not open a socket when closed while async connection resolves", async () => { |
228 | 271 | const seenAuthHeaders: Array<string | string[] | undefined> = []; |
229 | 272 | let resolveUrl!: (url: string) => void; |
@@ -309,6 +352,35 @@ describe("createRealtimeTranscriptionWebSocketSession", () => {
|
309 | 352 | session.close(); |
310 | 353 | }); |
311 | 354 | |
| 355 | +it("keeps error callback failures inside websocket message dispatch", async () => { |
| 356 | +const server = await createRealtimeServer({ initialText: "{not json" }); |
| 357 | +const onError = vi.fn((_error: Error) => { |
| 358 | +throw new Error("error observer failed"); |
| 359 | +}); |
| 360 | +const session = createRealtimeTranscriptionWebSocketSession({ |
| 361 | +providerId: "test", |
| 362 | +callbacks: { onError }, |
| 363 | +url: server.url, |
| 364 | +readyOnOpen: true, |
| 365 | +onMessage: () => { |
| 366 | +throw new Error("malformed payload should not reach provider handler"); |
| 367 | +}, |
| 368 | +sendAudio: (audio, transport) => { |
| 369 | +transport.sendBinary(audio); |
| 370 | +}, |
| 371 | +}); |
| 372 | + |
| 373 | +await session.connect(); |
| 374 | +await vi.waitFor(() => { |
| 375 | +expect(onError).toHaveBeenCalledTimes(1); |
| 376 | +}); |
| 377 | +const parseError = requireFirstMockArg(onError, "malformed websocket json error"); |
| 378 | +expect(parseError).toBeInstanceOf(Error); |
| 379 | +expect(parseError.message).toBe("Realtime transcription websocket received malformed JSON."); |
| 380 | +expect(session.isConnected()).toBe(true); |
| 381 | +session.close(); |
| 382 | +}); |
| 383 | + |
312 | 384 | it("reports pre-ready closes separately from connection timeouts", async () => { |
313 | 385 | const server = await createRealtimeServer({ closeOnConnection: true }); |
314 | 386 | const onError = vi.fn(); |
|