import ReconnectingWebSocket from "reconnecting-websocket"; import { WhisperTranscriptionAdapter } from "../src/services/transcription/adapters/WhisperTranscriptionAdapter"; import { BATCH_REPROCESS_TTL_MS } from "../src/services/transcription/batchReprocess"; import { MockWebSocket } from "./helpers/MockWebSocket"; import { advanceTimersAndFlush, flushMicrotasks } from "./helpers/testUtils"; const createTransport = (options: Record = {}) => new ReconnectingWebSocket("ws://test", [], { WebSocket: MockWebSocket, connectionTimeout: 50, minReconnectionDelay: 10, maxReconnectionDelay: 10, minUptime: 0, maxRetries: 3, ...options, }); const createAudioFrame = () => new Float32Array(128).fill(0.25); const setNavigatorOnline = (online: boolean) => { Object.defineProperty(window.navigator, "onLine", { configurable: true, value: online, }); window.dispatchEvent(new Event(online ? "online" : "offline")); }; const createResolvedConnectionContext = () => ({ realtimeUrl: "wss://scribe.sofya.health/api/realtime?existing=true&transcription_language=portuguese&translation_language=english&x-external-id=session-123", batchBaseUrl: "https://scribe.sofya.health/", batchDefaultUrl: "https://scribe.sofya.health/api/transcriber", authToken: "jwt-token", externalId: "session-123", transcriptionLanguage: "portuguese", translationLanguage: "english", headers: { "x-client-id": "sdk-demo", "Content-Type": "application/json", }, }); const createResolvedConnectionContextWithApiKey = ( overrides: Partial> = {} ) => { const base = createResolvedConnectionContext(); return { ...base, ...overrides, headers: { ...(base.headers ?? {}), "x-api-key": "sdk-api-key", ...(overrides.headers ?? {}), }, }; }; const createDebugAuditFixture = (metadata: Record | null = null) => ({ schemaVersion: 2, kind: "sofya-transcriber-debug-audit", sessionId: "sofya-debug-session-1", label: "sdk-run", generatedAt: new Date(1_700_000_000_000).toISOString(), createdAt: new Date(1_700_000_000_000).toISOString(), startedAt: new Date(1_700_000_000_100).toISOString(), stoppedAt: new Date(1_700_000_000_900).toISOString(), sessionDurationMs: 800, requestedProvider: "sofya_as_service", resolvedProvider: "sofya_as_service", metadata, environment: { userAgent: "jest", locationHref: "http://localhost/", }, report: { schemaVersion: 2, generatedAt: new Date(1_700_000_000_000).toISOString(), session: { sessionId: "sofya-debug-session-1", label: "sdk-run", requestedProvider: "sofya_as_service", resolvedProvider: "sofya_as_service", createdAt: new Date(1_700_000_000_000).toISOString(), startedAt: new Date(1_700_000_000_100).toISOString(), stoppedAt: new Date(1_700_000_000_900).toISOString(), durationMs: 800, hasStarted: true, hasStopped: true, }, summary: { outcome: "healthy", sessionDurationMs: 800, partialTranscriptCount: 1, finalTranscriptCount: 1, diarizationCount: 0, avgFirstPartialLatencyMs: null, p95FirstPartialLatencyMs: null, avgFinalLatencyMs: null, p95FinalLatencyMs: null, avgStabilizationLatencyMs: null, p95StabilizationLatencyMs: null, reconnectCount: null, recoverySuccessCount: null, terminalDisconnectCount: null, avgRecoveryDurationMs: null, p95RecoveryDurationMs: null, peakBufferedAudioBytes: null, bufferingDurationMs: null, drainingDurationMs: null, bufferStallCount: null, drainExitReason: null, finishDeliveryState: null, offlineDurationMs: null, flapCount: null, rttMs: null, downlinkMbps: null, clippingEventCount: null, silenceRatio: null, speechActivityRatio: null, audioCallbackGapCount: null, longestAudioCallbackGapMs: null, }, health: { transcriptUi: { supported: true, status: "healthy", reasons: [] }, session: { supported: true, status: "healthy", reasons: [] }, connection: { supported: true, status: "healthy", reasons: [] }, recovery: { supported: true, status: "healthy", reasons: [] }, buffering: { supported: true, status: "healthy", reasons: [] }, browserNetwork: { supported: true, status: "healthy", reasons: [] }, audioCapture: { supported: true, status: "healthy", reasons: [] }, }, timeline: { phases: [], incidents: [], transcriptMilestones: [], }, findings: [], dashboard: { heroStats: { outcome: "healthy", sessionDurationMs: 800, finalTranscriptCount: 1, reconnectCount: 0, peakBufferedAudioBytes: null, offlineDurationMs: null, }, cards: { transcript: { status: "healthy", headline: "ok", metrics: {} }, connection: { status: "healthy", headline: "ok", metrics: {} }, recovery: { status: "healthy", headline: "ok", metrics: {} }, buffering: { status: "healthy", headline: "ok", metrics: {} }, network: { status: "healthy", headline: "ok", metrics: {} }, audio: { status: "healthy", headline: "ok", metrics: {} }, }, }, }, }); describe("WhisperTranscriptionAdapter", () => { beforeEach(async () => { await WhisperTranscriptionAdapter.__resetSharedAudioRuntimeForTests(); jest.useFakeTimers(); MockWebSocket.reset(); }); afterEach(async () => { jest.clearAllTimers(); await WhisperTranscriptionAdapter.__resetSharedAudioRuntimeForTests(); delete (global as { fetch?: typeof fetch }).fetch; jest.useRealTimers(); }); it("forwards connected and transcription events through the reconnecting transport", async () => { const ws = createTransport(); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { healthCheckIntervalMs: 10, }, }); const connected = jest.fn(); const recognizing = jest.fn(); const recognized = jest.fn(); adapter.on("connected", connected); adapter.on("recognizing", recognizing); adapter.on("recognized", recognized); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); firstSocket.emitMessage( JSON.stringify({ is_partial: true, data: { text: "partial" }, }) ); firstSocket.emitMessage( JSON.stringify({ is_partial: false, data: { text: "final" }, }) ); expect(connected).toHaveBeenCalledTimes(1); expect(recognizing).toHaveBeenCalledWith("partial"); expect(recognized).toHaveBeenCalledWith("final"); }); it("reconnects after an unexpected close without emitting a terminal disconnect", async () => { const ws = createTransport(); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { healthCheckIntervalMs: 10, reconnectBaseDelayMs: 10, reconnectMaxDelayMs: 10, reconnectDelayGrowFactor: 1.3, }, }); const disconnected = jest.fn(); const reconnecting = jest.fn(); const reconnected = jest.fn(); const recognized = jest.fn(); adapter.on("disconnected", disconnected); adapter.on("reconnecting", reconnecting); adapter.on("reconnected", reconnected); adapter.on("recognized", recognized); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); firstSocket.fail(1006, "connection lost"); await flushMicrotasks(); expect(reconnecting).toHaveBeenCalledWith({ attempt: 1, delay: 10 }); expect(disconnected).not.toHaveBeenCalled(); await advanceTimersAndFlush(10); expect(MockWebSocket.instances.length).toBeGreaterThanOrEqual(2); const secondSocket = MockWebSocket.latest(); secondSocket.open(); await flushMicrotasks(); secondSocket.emitMessage( JSON.stringify({ is_partial: false, data: { text: "after reconnect" }, }) ); expect(reconnected).toHaveBeenCalledTimes(1); expect(recognized).toHaveBeenCalledWith("after reconnect"); expect(disconnected).not.toHaveBeenCalled(); }); it("exposes a resilience status snapshot at runtime", async () => { const ws = createTransport({ minReconnectionDelay: 10, maxReconnectionDelay: 10, }); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { healthCheckIntervalMs: 10, reconnectBaseDelayMs: 10, reconnectMaxDelayMs: 10, maxReconnectAttempts: 3, }, }); expect(adapter.getResilienceStatus()).toEqual( expect.objectContaining({ connectionState: "connecting", websocketState: "connecting", browserOnline: true, isRecovering: false, totalBufferedAudioBytes: 0, finishDeliveryState: null, }) ); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); expect(adapter.getResilienceStatus()).toEqual( expect.objectContaining({ connectionState: "connected", websocketState: "open", browserOnline: true, reconnectAttempt: null, finishDeliveryState: null, }) ); firstSocket.fail(1006, "connection lost"); await flushMicrotasks(); expect(adapter.getResilienceStatus()).toEqual( expect.objectContaining({ connectionState: "reconnecting", websocketState: "closed", browserOnline: true, isRecovering: true, reconnectAttempt: 1, nextReconnectDelayMs: 10, remainingReconnectAttempts: 2, finishDeliveryState: null, lastDisconnect: expect.objectContaining({ code: 1006, reason: "connection lost", }), }) ); }); it("keeps retrying through browser offline and reconnects after the browser comes back online", async () => { setNavigatorOnline(true); const ws = createTransport({ connectionTimeout: 20, minReconnectionDelay: 10, maxReconnectionDelay: 10, maxRetries: 1, }); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { connectTimeoutMs: 20, healthCheckIntervalMs: 10, reconnectBaseDelayMs: 10, reconnectMaxDelayMs: 10, maxReconnectAttempts: 1, }, }); const reconnecting = jest.fn(); const reconnected = jest.fn(); const disconnected = jest.fn(); adapter.on("reconnecting", reconnecting); adapter.on("reconnected", reconnected); adapter.on("disconnected", disconnected); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); setNavigatorOnline(false); firstSocket.fail(1006, "connection lost"); await flushMicrotasks(); expect(reconnecting).toHaveBeenCalledWith({ attempt: 1, delay: 10 }); expect(disconnected).not.toHaveBeenCalled(); await advanceTimersAndFlush(10); expect(MockWebSocket.instances.length).toBeGreaterThanOrEqual(2); setNavigatorOnline(true); await flushMicrotasks(); await advanceTimersAndFlush(10); expect(MockWebSocket.instances.length).toBeGreaterThanOrEqual(2); const recoverySocket = MockWebSocket.latest(); recoverySocket.open(); await flushMicrotasks(); expect(reconnected).toHaveBeenCalledTimes(1); expect(disconnected).not.toHaveBeenCalled(); expect(adapter.getResilienceStatus()).toEqual( expect.objectContaining({ connectionState: "connected", browserOnline: true, }) ); }); it("does not emit error for recoverable websocket transport failures", async () => { const ws = createTransport({ minReconnectionDelay: 10, maxReconnectionDelay: 10, }); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { healthCheckIntervalMs: 10, }, }); const errorHandler = jest.fn(); const reconnecting = jest.fn(); adapter.on("error", errorHandler); adapter.on("reconnecting", reconnecting); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); firstSocket.emitError("temporary upstream error"); await flushMicrotasks(); expect(errorHandler).not.toHaveBeenCalled(); expect(reconnecting).toHaveBeenCalled(); }); it("emits disconnected only after reconnect retries are exhausted", async () => { const ws = createTransport({ connectionTimeout: 20, minReconnectionDelay: 10, maxReconnectionDelay: 10, maxRetries: 1, }); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { connectTimeoutMs: 20, healthCheckIntervalMs: 10, reconnectBaseDelayMs: 10, reconnectMaxDelayMs: 10, maxReconnectAttempts: 1, }, }); const disconnected = jest.fn(); const reconnecting = jest.fn(); adapter.on("disconnected", disconnected); adapter.on("reconnecting", reconnecting); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); firstSocket.fail(1006, "connection lost"); await flushMicrotasks(); expect(reconnecting).toHaveBeenCalledWith({ attempt: 1, delay: 10 }); expect(disconnected).not.toHaveBeenCalled(); await advanceTimersAndFlush(20); await advanceTimersAndFlush(100); expect(disconnected).toHaveBeenCalledWith( expect.objectContaining({ code: 1000, reason: "timeout" }) ); }); it("releases runtime resources after a terminal disconnect even without stopTranscription", async () => { const ws = createTransport({ connectionTimeout: 20, minReconnectionDelay: 10, maxReconnectionDelay: 10, maxRetries: 1, }); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { connectTimeoutMs: 20, healthCheckIntervalMs: 10, reconnectBaseDelayMs: 10, reconnectMaxDelayMs: 10, maxReconnectAttempts: 1, }, }); const trackStop = jest.fn(); const inputDisconnect = jest.fn(); const nodeDisconnect = jest.fn(); const portPostMessage = jest.fn(); const portClose = jest.fn(); (adapter as any).mediaStream = { getTracks: () => [{ stop: trackStop }], }; (adapter as any).input = { connect: jest.fn(), disconnect: inputDisconnect, }; (adapter as any).recordingNode = { port: { onmessage: jest.fn(), postMessage: portPostMessage, close: portClose, }, disconnect: nodeDisconnect, }; await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); firstSocket.fail(1006, "connection lost"); await flushMicrotasks(); await advanceTimersAndFlush(20); await advanceTimersAndFlush(100); await flushMicrotasks(); expect(trackStop).toHaveBeenCalledTimes(1); expect(inputDisconnect).toHaveBeenCalledTimes(1); expect(nodeDisconnect).toHaveBeenCalledTimes(1); expect(portPostMessage).toHaveBeenCalledWith({ type: "sofya-stop" }); expect(portClose).toHaveBeenCalledTimes(1); expect((adapter as any).healthCheckTimer).toBeNull(); expect((adapter as any).websocket).toBeNull(); expect((adapter as any).audioContext).toBeNull(); expect((adapter as any).mediaStream).toBeNull(); expect((adapter as any).input).toBeUndefined(); expect((adapter as any).recordingNode).toBeUndefined(); }); it("drops live transport sends after a terminal disconnect", async () => { const ws = createTransport({ connectionTimeout: 20, minReconnectionDelay: 10, maxReconnectionDelay: 10, maxRetries: 1, }); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { connectTimeoutMs: 20, healthCheckIntervalMs: 10, reconnectBaseDelayMs: 10, reconnectMaxDelayMs: 10, maxReconnectAttempts: 1, }, }); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); firstSocket.fail(1006, "connection lost"); await flushMicrotasks(); await advanceTimersAndFlush(20); await advanceTimersAndFlush(100); (adapter as any).postMessage(createAudioFrame()); expect(firstSocket.sentMessages).toHaveLength(0); }); it("reconnects when a live send detects a stalled upstream", async () => { const ws = createTransport(); const reconnectSpy = jest.spyOn(ws, "reconnect"); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { healthCheckIntervalMs: 10, maxBufferedAmountBytes: 64, }, }); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); await advanceTimersAndFlush(0); jest.spyOn(adapter as any, "shouldBufferOfflineAudio").mockReturnValue(false); (ws as any)._ws.bufferedAmount = 128; (adapter as any).postMessage(createAudioFrame()); expect(reconnectSpy).toHaveBeenCalledWith( 4000, "WebSocket upstream stalled and stopped draining audio." ); }); it("uses a dedicated drain stall timeout instead of the connect timeout", async () => { const ws = createTransport({ minReconnectionDelay: 10, maxReconnectionDelay: 10, }); const reconnectSpy = jest.spyOn(ws, "reconnect"); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { connectTimeoutMs: 20, drainStallTimeoutMs: 200, healthCheckIntervalMs: 10, }, }); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); firstSocket.bufferedAmount = 128; const drainPromise = (adapter as any).waitForTransportDrain(ws); await advanceTimersAndFlush(100); expect(reconnectSpy).not.toHaveBeenCalled(); await advanceTimersAndFlush(100); await expect(drainPromise).resolves.toBe("stalled"); expect(reconnectSpy).toHaveBeenCalledWith(4000, "Offline audio drain stalled."); }); it("swallows startup failures to preserve the public contract", async () => { const ws = createTransport(); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, }); const errorHandler = jest.fn(); const consoleErrorSpy = jest .spyOn(console, "error") .mockImplementation(() => undefined); adapter.on("error", errorHandler); const audioContext = (adapter as any).ensureAudioContext(); audioContext.audioWorklet.addModule.mockRejectedValueOnce( new Error("worklet failed") ); await expect( adapter.startTranscription({} as MediaStream) ).resolves.toBeUndefined(); expect(errorHandler).not.toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(Error)); consoleErrorSpy.mockRestore(); }); it("swallows malformed websocket messages without changing the public contract", async () => { const ws = createTransport(); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, }); const errorHandler = jest.fn(); const recognized = jest.fn(); const consoleErrorSpy = jest .spyOn(console, "error") .mockImplementation(() => undefined); adapter.on("error", errorHandler); adapter.on("recognized", recognized); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); firstSocket.emitMessage("not-json"); expect(errorHandler).not.toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(Error)); expect(recognized).not.toHaveBeenCalled(); consoleErrorSpy.mockRestore(); }); it("promotes the latest partial when a final event arrives without text", async () => { const ws = createTransport(); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, }); const recognizing = jest.fn(); const recognized = jest.fn(); adapter.on("recognizing", recognizing); adapter.on("recognized", recognized); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); socket.emitMessage( JSON.stringify({ is_partial: true, data: { text: "partial hypothesis" }, }) ); socket.emitMessage( JSON.stringify({ is_partial: false, data: {}, }) ); expect(recognized).toHaveBeenCalledWith("partial hypothesis"); expect(recognizing).toHaveBeenLastCalledWith(""); }); it("clears the partial caption when a duplicate final is deduped", async () => { const ws = createTransport(); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, }); const recognizing = jest.fn(); const recognized = jest.fn(); adapter.on("recognizing", recognizing); adapter.on("recognized", recognized); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); socket.emitMessage( JSON.stringify({ is_partial: false, data: { text: "patient has mild cough" }, }) ); socket.emitMessage( JSON.stringify({ is_partial: true, data: { text: "patient has mild cough" }, }) ); socket.emitMessage( JSON.stringify({ is_partial: false, data: { text: "patient has mild cough" }, }) ); expect(recognized).toHaveBeenCalledTimes(1); expect(recognized).toHaveBeenCalledWith("patient has mild cough"); expect(recognizing).toHaveBeenLastCalledWith(""); }); const SESSION_A = "6f1c2a4e-3b8d-4e51-9c7a-2d0f8b1e5a33"; // Mensagem `partial`/`final` como o servidor passa a enviar: `session_id` // acompanha toda mensagem (contrato SDK de gravação, item 3). const createTranscriptMessage = ( overrides: Record = {}, session: Record | null = {} ) => JSON.stringify({ is_partial: true, status: "partial", data: { text: "bom dia", words: [], audio_duration: 2.1 }, time: 312.4, ...(session === null ? {} : { session_id: SESSION_A, ...session, }), ...overrides, }); const createSessionAwareAdapter = () => new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContext(), }); it("emits stt_session from the first partial while the partial still becomes recognizing", async () => { const adapter = createSessionAwareAdapter(); const sttSession = jest.fn(); const recognizing = jest.fn(); const recognized = jest.fn(); adapter.on("stt_session", sttSession); adapter.on("recognizing", recognizing); adapter.on("recognized", recognized); const onTranscriptSpy = jest.spyOn( (adapter as any).durableAudioSession, "onTranscriptMessage" ); const partialMetricSpy = jest.spyOn( (adapter as any).metricsCollector, "recordTranscriptPartial" ); expect(adapter.getSttSessionId()).toBeNull(); expect(adapter.getSttSessionIds()).toEqual([]); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); socket.emitMessage(createTranscriptMessage()); expect(sttSession).toHaveBeenCalledTimes(1); expect(sttSession).toHaveBeenCalledWith({ sttSessionId: SESSION_A, externalId: "session-123", connectionAttempt: 1, }); expect(adapter.getSttSessionId()).toBe(SESSION_A); expect(adapter.getSttSessionIds()).toEqual([SESSION_A]); // A mensagem segue o fluxo normal. expect(recognizing).toHaveBeenCalledWith("bom dia"); expect((adapter as any).tempPartialTranscription).toBe("bom dia"); expect(onTranscriptSpy).toHaveBeenCalledTimes(1); expect(partialMetricSpy).toHaveBeenCalledWith("bom dia"); expect(recognized).not.toHaveBeenCalled(); }); it("does not re-emit stt_session for the same id and keeps the final flow intact", async () => { const adapter = createSessionAwareAdapter(); const sttSession = jest.fn(); const recognizing = jest.fn(); const recognized = jest.fn(); adapter.on("stt_session", sttSession); adapter.on("recognizing", recognizing); adapter.on("recognized", recognized); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); socket.emitMessage(createTranscriptMessage()); socket.emitMessage( createTranscriptMessage({ data: { text: "bom dia doutor" } }) ); socket.emitMessage( createTranscriptMessage({ is_partial: false, status: "final", data: { text: "bom dia doutor" }, }) ); expect(sttSession).toHaveBeenCalledTimes(1); expect(recognizing).toHaveBeenCalledWith("bom dia"); expect(recognizing).toHaveBeenCalledWith("bom dia doutor"); expect(recognized).toHaveBeenCalledWith("bom dia doutor"); expect(adapter.getSttSessionIds()).toEqual([SESSION_A]); }); it("accumulates one stt session id per connection across reconnections", async () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resilience: { reconnectBaseDelayMs: 10, reconnectMaxDelayMs: 10, }, }); const sttSession = jest.fn(); adapter.on("stt_session", sttSession); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); firstSocket.emitMessage(createTranscriptMessage({}, { session_id: "session-a" })); firstSocket.emitMessage(createTranscriptMessage({}, { session_id: "session-a" })); firstSocket.fail(1006, "connection lost"); await flushMicrotasks(); await advanceTimersAndFlush(10); const secondSocket = MockWebSocket.latest(); expect(secondSocket).not.toBe(firstSocket); secondSocket.open(); await flushMicrotasks(); secondSocket.emitMessage(createTranscriptMessage({}, { session_id: "session-b" })); secondSocket.emitMessage(createTranscriptMessage({}, { session_id: "session-b" })); expect(sttSession).toHaveBeenCalledTimes(2); expect(sttSession.mock.calls[0][0]).toMatchObject({ sttSessionId: "session-a", externalId: null, connectionAttempt: 1, }); expect(sttSession.mock.calls[1][0]).toMatchObject({ sttSessionId: "session-b", connectionAttempt: 2, }); expect(adapter.getSttSessionIds()).toEqual(["session-a", "session-b"]); expect(adapter.getSttSessionId()).toBe("session-b"); }); it("stays silent and unchanged with messages from servers that do not send session fields", async () => { const adapter = createSessionAwareAdapter(); const sttSession = jest.fn(); const recognizing = jest.fn(); const recognized = jest.fn(); adapter.on("stt_session", sttSession); adapter.on("recognizing", recognizing); adapter.on("recognized", recognized); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); socket.emitMessage(createTranscriptMessage({}, null)); socket.emitMessage( createTranscriptMessage( { is_partial: false, status: "final", data: { text: "bom dia" } }, null ) ); socket.emitMessage( createTranscriptMessage({ session_id: " " }, null) ); expect(sttSession).not.toHaveBeenCalled(); expect(adapter.getSttSessionId()).toBeNull(); expect(adapter.getSttSessionIds()).toEqual([]); expect(recognizing).toHaveBeenCalledWith("bom dia"); expect(recognized).toHaveBeenCalledWith("bom dia"); }); it("ignores unknown extra fields next to session_id and emits only the contract payload", async () => { const adapter = createSessionAwareAdapter(); const sttSession = jest.fn(); const recognizing = jest.fn(); adapter.on("stt_session", sttSession); adapter.on("recognizing", recognizing); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); expect(() => { socket.emitMessage( createTranscriptMessage({ recording: false, recording_key: null, buffer_offset_s: 12.5, future_field: { nested: true }, }) ); }).not.toThrow(); expect(sttSession).toHaveBeenCalledTimes(1); expect(sttSession).toHaveBeenCalledWith({ sttSessionId: SESSION_A, externalId: "session-123", connectionAttempt: 1, }); expect(Object.keys(sttSession.mock.calls[0][0]).sort()).toEqual([ "connectionAttempt", "externalId", "sttSessionId", ]); expect(recognizing).toHaveBeenCalledWith("bom dia"); }); it("reports externalId null when external_id is configured as an empty string", async () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", external_id: "", ws: createTransport(), resolvedConnectionContext: { ...createResolvedConnectionContext(), externalId: "", }, }); const sttSession = jest.fn(); adapter.on("stt_session", sttSession); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); socket.emitMessage(createTranscriptMessage()); expect(sttSession).toHaveBeenCalledWith({ sttSessionId: SESSION_A, externalId: null, connectionAttempt: 1, }); }); it("clears accumulated stt session ids when a new transcription starts", async () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), }); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); socket.emitMessage( createTranscriptMessage({}, { session_id: "session-before-start" }) ); expect(adapter.getSttSessionIds()).toEqual(["session-before-start"]); await adapter.startTranscription({} as MediaStream); expect(adapter.getSttSessionId()).toBeNull(); expect(adapter.getSttSessionIds()).toEqual([]); await advanceTimersAndFlush(0); const nextSocket = MockWebSocket.latest(); expect(nextSocket).not.toBe(socket); nextSocket.open(); await flushMicrotasks(); nextSocket.emitMessage( createTranscriptMessage({}, { session_id: "session-after-start" }) ); expect(adapter.getSttSessionIds()).toEqual(["session-after-start"]); expect(adapter.getSttSessionId()).toBe("session-after-start"); }); it("keeps the stop flow unchanged when the terminal final carries session fields", async () => { const adapter = createSessionAwareAdapter(); const sttSession = jest.fn(); const recognized = jest.fn(); adapter.on("stt_session", sttSession); adapter.on("recognized", recognized); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); // Simula a fase `stopping_finalize_server` com `finish` já enviado, como a // suíte faz para o restante do fluxo de stop. const stopState = { resolve: jest.fn(), timeoutId: 0, finishSent: true, settled: false, }; (adapter as any).intentionalStop = true; (adapter as any).stopFinalizationState = stopState; socket.emitMessage( createTranscriptMessage( { is_partial: false, status: "final", data: { text: "último final" } }, { session_id: "session-terminal" } ) ); expect(stopState.resolve).toHaveBeenCalledTimes(1); expect(recognized).toHaveBeenCalledWith("último final"); expect(sttSession).toHaveBeenCalledTimes(1); expect(sttSession.mock.calls[0][0]).toMatchObject({ sttSessionId: "session-terminal", }); expect(adapter.getSttSessionId()).toBe("session-terminal"); }); it("includes stt_session_ids in the STT audit ingestion payload", async () => { const fetchMock = jest.fn().mockResolvedValue({ ok: true, status: 201, headers: new Headers(), }); (global as { fetch?: typeof fetch }).fetch = fetchMock as unknown as typeof fetch; const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContextWithApiKey(), auditIngestion: {}, }); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); socket.emitMessage(createTranscriptMessage({}, { session_id: "session-a" })); socket.emitMessage(createTranscriptMessage({}, { session_id: "session-b" })); const audit = createDebugAuditFixture(); const warning = await (adapter as any).ingestSttAudit(audit); expect(warning).toBeNull(); const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body as string); expect(requestBody).toEqual({ external_identifier: "session-123", stt_session_ids: ["session-a", "session-b"], audit, }); }); it("treats stop-control frames as non-terminal unless final status/text is present", () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), }); expect( (adapter as any).isTerminalStopMessage({ is_partial: false, status: "ack", data: {}, }) ).toBe(false); expect( (adapter as any).isTerminalStopMessage({ is_partial: false, status: "final", data: {}, }) ).toBe(true); expect( (adapter as any).isTerminalStopMessage({ is_partial: false, data: { text: "final transcript from server" }, }) ).toBe(true); expect( (adapter as any).isTerminalStopMessage({ is_partial: true, status: "final", data: { text: "partial" }, }) ).toBe(false); }); it("treats explicit non-partial frames as terminal once finish was sent", () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), }); expect( (adapter as any).isTerminalStopMessage( { is_partial: false, status: "ack", data: {}, }, true ) ).toBe(true); }); it("runs stop as a single in-flight operation", async () => { const ws = createTransport(); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, }); let resolveStop!: () => void; const deferredStop = new Promise((resolve) => { resolveStop = resolve; }); const performStopSpy = jest .spyOn(adapter as any, "performStopTranscription") .mockReturnValue(deferredStop); const firstStopPromise = adapter.stopTranscription(); const secondStopPromise = adapter.stopTranscription(); expect(performStopSpy).toHaveBeenCalledTimes(1); resolveStop(); await Promise.all([firstStopPromise, secondStopPromise]); }); it("loads the worklet module once and reuses the shared audio context across runs", async () => { const firstAdapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), }); const firstContext = (firstAdapter as any).ensureAudioContext(); await (firstAdapter as any).setupRecordingWorkletNode(); await (firstAdapter as any).closeAudioContext(); const secondAdapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), }); const secondContext = (secondAdapter as any).ensureAudioContext(); await (secondAdapter as any).setupRecordingWorkletNode(); expect(secondContext).toBe(firstContext); expect(firstContext.audioWorklet.addModule).toHaveBeenCalledTimes(1); }); it("cancels idle audio-context shutdown when a new run starts before timeout", async () => { const firstAdapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), }); const sharedContext = (firstAdapter as any).ensureAudioContext(); await (firstAdapter as any).setupRecordingWorkletNode(); await (firstAdapter as any).closeAudioContext(); await advanceTimersAndFlush(14000); expect(sharedContext.close).not.toHaveBeenCalled(); const secondAdapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), }); (secondAdapter as any).ensureAudioContext(); await advanceTimersAndFlush(2000); expect(sharedContext.close).not.toHaveBeenCalled(); }); it("runs restart preflight before bootstrap setup when start is called", async () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), }); const preflightSpy = jest.spyOn(adapter as any, "restartPhaseForStart"); const setupDurableSpy = jest.spyOn(adapter as any, "setupDurableRuntime"); await adapter.startTranscription({} as MediaStream); expect(preflightSpy).toHaveBeenCalledTimes(1); expect(setupDurableSpy).toHaveBeenCalled(); expect(preflightSpy.mock.invocationCallOrder[0]).toBeLessThan( setupDurableSpy.mock.invocationCallOrder[0] ); }); it("guards invalid lifecycle transitions without throwing", () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), }); (adapter as any).lifecyclePhase = "stopping_finalize_server"; expect(() => adapter.pauseTranscription()).not.toThrow(); expect(() => adapter.resumeTranscription()).not.toThrow(); expect((adapter as any).lifecyclePhase).toBe("stopping_finalize_server"); }); it("freezes input phase prevents new buffering and live sends", async () => { const ws = createTransport(); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, }); await advanceTimersAndFlush(0); const socket = MockWebSocket.latest(); socket.open(); await flushMicrotasks(); const appendSpy = jest.spyOn( (adapter as any).durableAudioSession, "appendCaptureChunk" ); const messagesBeforeFreeze = socket.sentMessages.length; await (adapter as any).freezeInputPhase(); (adapter as any).postMessage(createAudioFrame()); expect((adapter as any).lifecyclePhase).toBe("stopping_freeze_input"); expect(appendSpy).not.toHaveBeenCalled(); expect(socket.sentMessages.length).toBe(messagesBeforeFreeze); }); it("tracks recovering to live lifecycle transitions across reconnect", async () => { const ws = createTransport({ minReconnectionDelay: 10, maxReconnectionDelay: 10, }); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, }); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); expect((adapter as any).lifecyclePhase).toBe("idle"); firstSocket.fail(1006, "connection lost"); await flushMicrotasks(); expect((adapter as any).lifecyclePhase).toBe("running_recovering"); await advanceTimersAndFlush(10); const secondSocket = MockWebSocket.latest(); secondSocket.open(); await flushMicrotasks(); expect((adapter as any).lifecyclePhase).toBe("running_live"); }); it("reports batch reprocess readiness after stop and expires after the TTL", async () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContext(), }); const statusEvents = jest.fn(); adapter.on("batch_reprocess_status", statusEvents); expect(adapter.getBatchReprocessStatus()).toEqual( expect.objectContaining({ state: "waiting_for_stop", configured: true, available: false, }) ); const stoppedAt = Date.now(); (adapter as any).latestConsultationStorageStatus = { durableSegments: 1 }; (adapter as any).transitionBatchReprocessStatusAfterStop(stoppedAt); expect(adapter.getBatchReprocessStatus()).toEqual( expect.objectContaining({ state: "ready", configured: true, available: true, expiresAt: stoppedAt + BATCH_REPROCESS_TTL_MS, }) ); expect(adapter.getBatchReprocessRemainingTime()).toBe(BATCH_REPROCESS_TTL_MS); expect(statusEvents).toHaveBeenCalledWith( expect.objectContaining({ state: "ready", }) ); await advanceTimersAndFlush(BATCH_REPROCESS_TTL_MS + 1); expect(adapter.getBatchReprocessStatus()).toEqual( expect.objectContaining({ state: "expired", available: false, }) ); expect(adapter.getBatchReprocessRemainingTime()).toBe(0); expect(statusEvents).toHaveBeenLastCalledWith( expect.objectContaining({ state: "expired", }) ); }); it("derives the default batch endpoint from the realtime connection and supports explicit overrides", () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContext(), }); expect((adapter as any).buildBatchReprocessUrl()).toBe( "https://scribe.sofya.health/api/transcriber?existing=true&transcription_language=portuguese&translation_language=english&x-external-id=session-123" ); const overrideAdapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContext(), batchReprocess: { endpoint: "https://batch.sofya.health/custom?mode=full", }, }); expect((overrideAdapter as any).buildBatchReprocessUrl()).toBe( "https://batch.sofya.health/custom?mode=full&existing=true&transcription_language=portuguese&translation_language=english&x-external-id=session-123" ); }); it("derives STT audit ingestion endpoint from realtime host and supports absolute/relative overrides", () => { const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContextWithApiKey(), auditIngestion: {}, }); expect((adapter as any).buildSttAuditIngestionUrl()).toBe( "https://scribe.sofya.health/v1/stt/audits" ); const absoluteOverride = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContextWithApiKey(), auditIngestion: { endpoint: "https://audit.sofya.health/custom/audits", }, }); expect((absoluteOverride as any).buildSttAuditIngestionUrl()).toBe( "https://audit.sofya.health/custom/audits" ); const relativeOverride = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContextWithApiKey(), auditIngestion: { endpoint: "/custom/audits?mode=full", }, }); expect((relativeOverride as any).buildSttAuditIngestionUrl()).toBe( "https://scribe.sofya.health/custom/audits?mode=full" ); }); it("posts raw audit payload and resolves thread/external identifiers using precedence rules", async () => { const fetchMock = jest.fn().mockResolvedValue({ ok: true, status: 201, headers: new Headers(), }); (global as { fetch?: typeof fetch }).fetch = fetchMock as unknown as typeof fetch; const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContextWithApiKey(), auditIngestion: { headers: { "x-terminal-id": "TERM-001", "x-app-version": "3.2.1", }, }, }); const audit = createDebugAuditFixture({ thread_id: "thread-from-metadata", threadId: "thread-from-camel", external_identifier: "external-from-metadata", externalIdentifier: "external-from-camel", external_id: "external-from-legacy", }); const warning = await (adapter as any).ingestSttAudit(audit); expect(warning).toBeNull(); expect(fetchMock).toHaveBeenCalledTimes(1); const [requestUrl, requestInit] = fetchMock.mock.calls[0]; const requestHeaders = requestInit.headers as Record; const requestBody = JSON.parse(requestInit.body as string); expect(requestUrl).toBe("https://scribe.sofya.health/v1/stt/audits"); expect(requestInit.method).toBe("POST"); expect(requestHeaders).toEqual({ authorization: "Bearer jwt-token", "content-type": "application/json", "x-api-key": "sdk-api-key", "x-app-version": "3.2.1", "x-client-id": "sdk-demo", "x-terminal-id": "TERM-001", }); expect(requestBody).toEqual({ thread_id: "thread-from-metadata", external_identifier: "session-123", audit, }); }); it("uses config identifiers over metadata/resolved defaults for STT audit ingestion", async () => { const fetchMock = jest.fn().mockResolvedValue({ ok: true, status: 201, headers: new Headers(), }); (global as { fetch?: typeof fetch }).fetch = fetchMock as unknown as typeof fetch; const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContextWithApiKey(), auditIngestion: { threadId: "thread-from-config", externalIdentifier: "external-from-config", }, }); const audit = createDebugAuditFixture({ thread_id: "thread-from-metadata", external_identifier: "external-from-metadata", }); const warning = await (adapter as any).ingestSttAudit(audit); expect(warning).toBeNull(); const [, requestInit] = fetchMock.mock.calls[0]; const requestBody = JSON.parse(requestInit.body as string); expect(requestBody.thread_id).toBe("thread-from-config"); expect(requestBody.external_identifier).toBe("external-from-config"); }); it("returns missing_api_key warning and skips STT audit request when x-api-key is absent", async () => { const fetchMock = jest.fn(); (global as { fetch?: typeof fetch }).fetch = fetchMock as unknown as typeof fetch; const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContext(), auditIngestion: {}, }); const warning = await (adapter as any).ingestSttAudit(createDebugAuditFixture()); expect(warning).toEqual( expect.objectContaining({ code: "missing_api_key", endpoint: "https://scribe.sofya.health/v1/stt/audits", }) ); expect(fetchMock).not.toHaveBeenCalled(); }); it("retries transient STT audit failures (network/5xx) with exponential backoff", async () => { const fetchMock = jest .fn() .mockRejectedValueOnce(new TypeError("network down")) .mockResolvedValueOnce({ ok: false, status: 503, headers: new Headers(), }) .mockResolvedValueOnce({ ok: true, status: 200, headers: new Headers(), }); (global as { fetch?: typeof fetch }).fetch = fetchMock as unknown as typeof fetch; const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContextWithApiKey(), auditIngestion: {}, }); const ingestionPromise = (adapter as any).ingestSttAudit(createDebugAuditFixture()); await flushMicrotasks(); expect(fetchMock).toHaveBeenCalledTimes(1); await advanceTimersAndFlush(300); expect(fetchMock).toHaveBeenCalledTimes(2); await advanceTimersAndFlush(600); expect(fetchMock).toHaveBeenCalledTimes(3); await expect(ingestionPromise).resolves.toBeNull(); }); it("does not retry non-transient STT audit errors", async () => { const fetchMock = jest.fn().mockResolvedValue({ ok: false, status: 403, headers: new Headers(), }); (global as { fetch?: typeof fetch }).fetch = fetchMock as unknown as typeof fetch; const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContextWithApiKey(), auditIngestion: {}, }); const warning = await (adapter as any).ingestSttAudit(createDebugAuditFixture()); expect(fetchMock).toHaveBeenCalledTimes(1); expect(warning).toEqual( expect.objectContaining({ code: "http_error", status: 403, attempt: 1, }) ); }); it("inherits realtime auth and query params for batch reprocess and uses a custom response parser", async () => { const parseResponse = jest.fn().mockResolvedValue({ transcript: "final" }); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContext(), batchReprocess: { parseResponse, }, }); const fetchMock = jest.fn().mockResolvedValue({ ok: true, status: 200, headers: new Headers(), }); (global as { fetch?: typeof fetch }).fetch = fetchMock as unknown as typeof fetch; const scheduleStorageDropAfterStop = jest.fn(); (adapter as any).durableAudioSession = { buildFinalUpload: jest .fn() .mockResolvedValue(new Blob([new Uint8Array([1, 2, 3])], { type: "audio/wav" })), scheduleStorageDropAfterStop, }; (adapter as any).lifecyclePhase = "stopped"; (adapter as any).latestConsultationStorageStatus = { durableSegments: 1 }; (adapter as any).transitionBatchReprocessStatusAfterStop(Date.now()); const payload = await adapter.reprocessAudio<{ transcript: string }>(); const [, requestInit] = fetchMock.mock.calls[0]; const requestHeaders = requestInit.headers as Record; const requestBody = requestInit.body as FormData; const uploadedFile = requestBody.get("file") as File; expect(payload).toEqual({ transcript: "final" }); expect(fetchMock).toHaveBeenCalledWith( "https://scribe.sofya.health/api/transcriber?existing=true&transcription_language=portuguese&translation_language=english&x-external-id=session-123", expect.objectContaining({ method: "POST", body: expect.any(FormData), }) ); expect(parseResponse).toHaveBeenCalledWith( expect.objectContaining({ ok: true, status: 200, }) ); expect(requestHeaders).toEqual({ authorization: "Bearer jwt-token", "x-client-id": "sdk-demo", }); expect(uploadedFile).toBeInstanceOf(Blob); expect(uploadedFile.name).toBe("consultation.wav"); expect(uploadedFile.type).toBe("audio/wav"); expect(scheduleStorageDropAfterStop).not.toHaveBeenCalled(); expect(adapter.getBatchReprocessStatus()).toEqual( expect.objectContaining({ state: "ready", lastCompletedAt: expect.any(Number), lastError: null, }) ); expect(adapter.getBatchReprocessRemainingTime()).toBeGreaterThan(0); }); it("rejects concurrent batch reprocess calls while allowing later retries within the TTL window", async () => { let resolveFetch: | ((value: { ok: boolean; status: number; headers: Headers; json: () => Promise<{ transcript: string }>; }) => void) | undefined; const fetchMock = jest.fn().mockImplementation( () => new Promise((resolve) => { resolveFetch = resolve; }) ); (global as { fetch?: typeof fetch }).fetch = fetchMock as unknown as typeof fetch; const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws: createTransport(), resolvedConnectionContext: createResolvedConnectionContext(), }); (adapter as any).durableAudioSession = { buildFinalUpload: jest .fn() .mockResolvedValue(new Blob([new Uint8Array([1, 2, 3])], { type: "audio/wav" })), }; (adapter as any).lifecyclePhase = "stopped"; (adapter as any).latestConsultationStorageStatus = { durableSegments: 1 }; (adapter as any).transitionBatchReprocessStatusAfterStop(Date.now()); const firstRequest = adapter.reprocessAudio<{ transcript: string }>(); await flushMicrotasks(); await expect(adapter.reprocessAudio()).rejects.toMatchObject({ code: "REPROCESS_IN_PROGRESS", }); expect(adapter.getBatchReprocessStatus()).toEqual( expect.objectContaining({ state: "running", lastError: null, }) ); expect(resolveFetch).toBeDefined(); resolveFetch!({ ok: true, status: 200, headers: new Headers(), json: async () => ({ transcript: "first-pass" }), }); await expect(firstRequest).resolves.toEqual({ transcript: "first-pass" }); fetchMock.mockResolvedValueOnce({ ok: true, status: 200, headers: new Headers(), json: async () => ({ transcript: "second-pass" }), }); await expect(adapter.reprocessAudio<{ transcript: string }>()).resolves.toEqual({ transcript: "second-pass", }); }); });