import ReconnectingWebSocket from "reconnecting-websocket"; import { TranscriptionServiceFactory } from "../src/services/transcription/TranscriptionServiceFactory"; import { OracleTranscriptionAdapter } from "../src/services/transcription/adapters/OracleTranscriptionAdapter"; import { BatchTranscriptionAdapter } from "../src/services/transcription/adapters/BatchTranscriptionAdapter"; import { WhisperTranscriptionAdapter } from "../src/services/transcription/adapters/WhisperTranscriptionAdapter"; import { MockWebSocket } from "./helpers/MockWebSocket"; import { advanceTimersAndFlush } from "./helpers/testUtils"; describe("TranscriptionServiceFactory", () => { const originalWebSocket = global.WebSocket; beforeEach(() => { jest.useFakeTimers(); MockWebSocket.reset(); Object.defineProperty(global, "WebSocket", { value: MockWebSocket, writable: true, configurable: true, }); }); afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); Object.defineProperty(global, "WebSocket", { value: originalWebSocket, writable: true, configurable: true, }); }); it("creates a Whisper adapter backed by ReconnectingWebSocket", async () => { const service = await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test", }, resilience: { connectTimeoutMs: 50, }, }); expect(service).toBeInstanceOf(WhisperTranscriptionAdapter); expect((service as any).websocket).toBeInstanceOf(ReconnectingWebSocket); await advanceTimersAndFlush(0); expect(MockWebSocket.instances).toHaveLength(1); }); it("passes websocket protocols through when configured as a string", async () => { await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test", }, protocols: "soap", }); await advanceTimersAndFlush(0); expect(MockWebSocket.instances).toHaveLength(1); expect(MockWebSocket.instances[0]?.protocols).toBe("soap"); }); it("passes websocket protocols through when configured as an array", async () => { await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test", }, protocols: ["soap", "json"], }); await advanceTimersAndFlush(0); expect(MockWebSocket.instances).toHaveLength(1); expect(MockWebSocket.instances[0]?.protocols).toEqual(["soap", "json"]); }); it("passes configured external_id through to the connection URL", async () => { await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test?existing=true", }, external_id: "external-123", }); await advanceTimersAndFlush(0); expect(MockWebSocket.instances).toHaveLength(1); const wsUrl = new URL(MockWebSocket.instances[0]!.url); expect(wsUrl.searchParams.get("existing")).toBe("true"); expect(wsUrl.searchParams.get("transcription_language")).toBe("english"); expect(wsUrl.searchParams.get("x-external-id")).toBe("external-123"); }); it("sends record=true when the recording flag is enabled", async () => { await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test", }, record: true, }); await advanceTimersAndFlush(0); const wsUrl = new URL(MockWebSocket.latest().url); expect(wsUrl.searchParams.get("record")).toBe("true"); }); it("sends record=false when the recording flag is disabled", async () => { await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test", }, record: false, }); await advanceTimersAndFlush(0); const wsUrl = new URL(MockWebSocket.latest().url); expect(wsUrl.searchParams.get("record")).toBe("false"); }); it("omits the record parameter when the recording flag is undefined", async () => { await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test", }, record: undefined, }); await advanceTimersAndFlush(0); const wsUrl = new URL(MockWebSocket.latest().url); expect(wsUrl.searchParams.has("record")).toBe(false); expect(wsUrl.search).not.toContain("record"); }); it("lets the record config override a record query already present in the endpoint", async () => { await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test?record=false", }, record: true, }); await advanceTimersAndFlush(0); const overridden = new URL(MockWebSocket.latest().url); expect(overridden.searchParams.getAll("record")).toEqual(["true"]); MockWebSocket.reset(); await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test?record=false", }, }); await advanceTimersAndFlush(0); const preserved = new URL(MockWebSocket.latest().url); expect(preserved.searchParams.getAll("record")).toEqual(["false"]); }); it("url-encodes external_id in the x-external-id query parameter", async () => { await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test?existing=true", }, external_id: "visit 12/3&x=y", }); await advanceTimersAndFlush(0); const rawUrl = MockWebSocket.latest().url; expect(rawUrl).toContain("x-external-id=visit+12%2F3%26x%3Dy"); const wsUrl = new URL(rawUrl); expect(wsUrl.searchParams.get("x-external-id")).toBe("visit 12/3&x=y"); expect(wsUrl.searchParams.get("existing")).toBe("true"); }); it("repeats record and x-external-id on every reconnection", async () => { await TranscriptionServiceFactory.create("sofya_as_service", { language: "en-US", endpoint: { endpoint: "ws://test", }, external_id: "external-123", record: true, resilience: { connectTimeoutMs: 50, reconnectBaseDelayMs: 10, reconnectMaxDelayMs: 10, minConnectionUptimeMs: 0, }, }); await advanceTimersAndFlush(0); expect(MockWebSocket.instances).toHaveLength(1); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await advanceTimersAndFlush(0); firstSocket.fail(1006, "connection lost"); await advanceTimersAndFlush(10); expect(MockWebSocket.instances.length).toBeGreaterThanOrEqual(2); MockWebSocket.instances.forEach((socket) => { const wsUrl = new URL(socket.url); expect(wsUrl.searchParams.get("record")).toBe("true"); expect(wsUrl.searchParams.get("x-external-id")).toBe("external-123"); }); }); it("builds a resolved whisper connection context for batch defaults", async () => { const service = await TranscriptionServiceFactory.create("sofya_as_service", { language: "pt-BR", translation_lang: "en-US", token: "jwt-token", headers: { "x-client-id": "demo", }, endpoint: { endpoint: "wss://scribe.sofya.health/api/realtime?existing=true", }, external_id: "external-123", }); const context = (service as any).resolvedConnectionContext; expect(context).toEqual( expect.objectContaining({ authToken: "jwt-token", batchBaseUrl: "https://scribe.sofya.health/", batchDefaultUrl: "https://scribe.sofya.health/api/transcriber", externalId: "external-123", headers: { "x-client-id": "demo", }, transcriptionLanguage: "portuguese", translationLanguage: "english", }) ); const realtimeUrl = new URL(context.realtimeUrl); expect(realtimeUrl.searchParams.get("existing")).toBe("true"); expect(realtimeUrl.searchParams.get("transcription_language")).toBe( "portuguese" ); expect(realtimeUrl.searchParams.get("translation_language")).toBe("english"); expect(realtimeUrl.searchParams.get("x-external-id")).toBe("external-123"); }); it("creates a batch adapter for explicit whisper batch mode without opening a websocket", async () => { const service = await TranscriptionServiceFactory.create("sofya_as_service", { mode: "batch", language: "pt-BR", translation_lang: "en-US", token: "jwt-token", headers: { "x-client-id": "demo", }, endpoint: { endpoint: "https://scribe.sofya.health/api/transcriber?existing=true", }, external_id: "external-123", }); expect(service).toBeInstanceOf(BatchTranscriptionAdapter); expect(MockWebSocket.instances).toHaveLength(0); const context = (service as any).resolvedConnectionContext; expect(context).toEqual( expect.objectContaining({ authToken: "jwt-token", batchBaseUrl: "https://scribe.sofya.health/", batchDefaultUrl: "https://scribe.sofya.health/api/transcriber?existing=true&transcription_language=portuguese&translation_language=english&x-external-id=external-123", externalId: "external-123", transcriptionLanguage: "portuguese", translationLanguage: "english", }) ); }); it("preserves the explicit batch endpoint path for primary batch mode", async () => { const service = await TranscriptionServiceFactory.create("sofya_as_service", { mode: "batch", language: "en-US", endpoint: { endpoint: "https://scribe.sofya.health/v2/custom-batch?existing=true", }, }); const context = (service as any).resolvedConnectionContext; expect(context.batchDefaultUrl).toBe( "https://scribe.sofya.health/v2/custom-batch?existing=true&transcription_language=english" ); }); it("rejects primary batch mode for non-whisper providers", async () => { await expect( TranscriptionServiceFactory.create("oracle", { mode: "batch", language: "en-US", token: "token", compartmentId: "compartment", region: "us-phoenix-1", endpoint: { endpoint: "https://oracle.example.com/api/transcriber", }, }) ).rejects.toThrow("Primary batch mode is only supported for Whisper providers."); }); it("keeps Oracle providers on the native Oracle adapter path", async () => { const service = await TranscriptionServiceFactory.create("oracle", { language: "en-US", token: "token", compartmentId: "compartment", region: "us-phoenix-1", }); expect(service).toBeInstanceOf(OracleTranscriptionAdapter); }); it("exposes batch reprocess as unsupported for Oracle providers", async () => { const service = await TranscriptionServiceFactory.create("oracle", { language: "en-US", token: "token", compartmentId: "compartment", region: "us-phoenix-1", }); expect(service.getBatchReprocessStatus?.()).toEqual( expect.objectContaining({ state: "unsupported", supported: false, }) ); expect(service.getBatchReprocessRemainingTime?.()).toBeNull(); await expect(service.reprocessAudio?.()).rejects.toMatchObject({ code: "UNSUPPORTED_PROVIDER", }); }); });