jest.mock("../src/api/api", () => ({ authService: jest.fn(), })); import EventEmitter from "events"; import ReconnectingWebSocket from "reconnecting-websocket"; import { SofyaTranscriber } from "../src/services/transcription/SofyaTranscriber"; import { TranscriptionServiceFactory } from "../src/services/transcription/TranscriptionServiceFactory"; import { WhisperTranscriptionAdapter } from "../src/services/transcription/adapters/WhisperTranscriptionAdapter"; import { createUnsupportedTelemetrySnapshot, TelemetryRow, TelemetrySnapshot, } from "../src/services/transcription/metrics"; 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 = (amplitude = 0.25, length = 128) => new Float32Array(length).fill(amplitude); const setNavigatorOnline = (online: boolean) => { Object.defineProperty(window.navigator, "onLine", { configurable: true, value: online, }); window.dispatchEvent(new Event(online ? "online" : "offline")); }; const setConnectionInfo = (connection: { effectiveType?: string; downlink?: number; rtt?: number; }) => { Object.defineProperty(window.navigator, "connection", { configurable: true, value: connection, }); }; const createTelemetrySnapshot = ( overrides: Partial = {} ): TelemetrySnapshot => { const now = 1_700_000_000_000; return { schemaVersion: 2, updatedAt: now, provider: "sofya_as_service", capabilities: { transcriptUi: true, session: true, connection: true, recovery: true, buffering: true, browserNetwork: true, audioCapture: true, }, window: { sessionId: "session-1", startedAt: now, endedAt: null, durationMs: null, }, status: { connectionState: "connecting", websocketState: "connecting", browserOnline: true, isRecovering: false, isBufferingAudio: false, isDrainingAudio: false, transportBufferedAmountBytes: 0, pendingBufferedAudioBytes: 0, persistedBufferedAudioBytes: 0, persistedBufferedAudioSegments: 0, totalBufferedAudioBytes: 0, lastDrainProgressAt: null, drainCycles: 0, drainExitReason: null, reconnectAttempt: null, nextReconnectDelayMs: null, remainingReconnectAttempts: 3, finishDeliveryState: null, lastDisconnectCode: null, lastDisconnectReason: null, networkEffectiveType: null, }, counters: {}, gauges: {}, histograms: {}, derived: { continuity_loss_rate: null, delivery_rate: null, reconnect_success_rate: null, mean_recovery_time_ms: null, stall_ratio: null, buffer_pressure_ratio: null, }, ...overrides, }; }; class MockTelemetryTranscriptionService extends EventEmitter { private currentSnapshot: TelemetrySnapshot; private rows: TelemetryRow[]; public readonly startTranscription = jest.fn(() => { this.currentSnapshot = createTelemetrySnapshot({ updatedAt: Date.now(), counters: {}, }); this.emit("telemetry", this.currentSnapshot); }); public readonly pauseTranscription = jest.fn(); public readonly resumeTranscription = jest.fn(); public readonly stopTranscription = jest.fn(async () => { this.emit("stopped"); }); public readonly getResilienceStatus = jest.fn(() => ({ connectionState: "connected", websocketState: "open", totalBufferedAudioBytes: 0, })); public readonly getTelemetrySnapshot = jest.fn(() => this.currentSnapshot); public readonly getTelemetryRows = jest.fn(() => this.rows.slice()); public readonly clearTelemetryRows = jest.fn(() => { this.rows = []; }); public readonly resetTelemetry = jest.fn(() => { this.currentSnapshot = createTelemetrySnapshot({ updatedAt: Date.now() }); this.rows = []; }); constructor(initialSnapshot: TelemetrySnapshot, rows: TelemetryRow[] = []) { super(); this.currentSnapshot = initialSnapshot; this.rows = rows; } public setTelemetry(snapshot: TelemetrySnapshot) { this.currentSnapshot = snapshot; } public pushRow(row: TelemetryRow) { this.rows.push(row); } } describe("telemetry API contract", () => { const originalWebSocket = global.WebSocket; const originalNavigatorConnection = (window.navigator as any).connection; 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, }); Object.defineProperty(window.navigator, "connection", { configurable: true, value: originalNavigatorConnection, }); }); it("forwards telemetry snapshot and row events through SofyaTranscriber", async () => { const initialSnapshot = createTelemetrySnapshot({ updatedAt: 1_700_000_000_100, counters: { "tx.partial.received": 1, }, }); const row: TelemetryRow = { ts: 1_700_000_000_101, sessionId: "session-1", metric: "tx.partial.received", value: 1, type: "counter", }; const nextSnapshot = createTelemetrySnapshot({ updatedAt: 1_700_000_000_200, counters: { "tx.partial.received": 2, "tx.final.received": 1, }, derived: { continuity_loss_rate: 0, delivery_rate: 1, reconnect_success_rate: null, mean_recovery_time_ms: null, stall_ratio: null, buffer_pressure_ratio: null, }, }); const service = new MockTelemetryTranscriptionService(initialSnapshot, [row]); jest.spyOn(TranscriptionServiceFactory, "create").mockResolvedValue(service as any); const transcriber = new SofyaTranscriber({ provider: "sofya_as_service", endpoint: "ws://metrics-test", config: { language: "en-US", }, }); const telemetryHandler = jest.fn(); const rowHandler = jest.fn(); (transcriber as any).on("telemetry", telemetryHandler); (transcriber as any).on("telemetry_row", rowHandler); await flushMicrotasks(); expect(telemetryHandler).toHaveBeenCalledWith(initialSnapshot); expect((transcriber as any).getTelemetrySnapshot()).toEqual(initialSnapshot); expect((transcriber as any).getTelemetryRows()).toEqual([row]); service.setTelemetry(nextSnapshot); service.emit("telemetry", nextSnapshot); service.emit("telemetry_row", row); await flushMicrotasks(); expect(telemetryHandler).toHaveBeenLastCalledWith(nextSnapshot); expect(rowHandler).toHaveBeenLastCalledWith(row); }); it("resets the visible telemetry snapshot when a fresh transcription session starts", async () => { const initialSnapshot = createTelemetrySnapshot({ counters: { "tx.partial.received": 7, "tx.final.received": 4, }, }); const service = new MockTelemetryTranscriptionService(initialSnapshot); jest.spyOn(TranscriptionServiceFactory, "create").mockResolvedValue(service as any); const transcriber = new SofyaTranscriber({ provider: "sofya_as_service", endpoint: "ws://metrics-test", config: { language: "en-US", }, }); await flushMicrotasks(); expect((transcriber as any).getTelemetrySnapshot()).toEqual(initialSnapshot); transcriber.startTranscription({} as MediaStream); await flushMicrotasks(); expect(service.startTranscription).toHaveBeenCalledTimes(1); expect((transcriber as any).getTelemetrySnapshot()).toEqual( expect.objectContaining({ schemaVersion: 2, counters: {}, }) ); }); it("logs resilience status every 5 seconds during an active transcription session", async () => { const initialSnapshot = createTelemetrySnapshot(); const service = new MockTelemetryTranscriptionService(initialSnapshot); jest.spyOn(TranscriptionServiceFactory, "create").mockResolvedValue(service as any); const transcriber = new SofyaTranscriber({ provider: "sofya_as_service", endpoint: "ws://metrics-test", config: { language: "en-US", }, }); const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {}); await flushMicrotasks(); transcriber.startTranscription({} as MediaStream); await flushMicrotasks(); const callsAfterStart = service.getResilienceStatus.mock.calls.length; jest.advanceTimersByTime(5000); await flushMicrotasks(); expect(service.getResilienceStatus.mock.calls.length).toBe(callsAfterStart + 1); expect(consoleLogSpy).toHaveBeenCalledWith( "[SofyaResilienceStatus]", expect.any(Object) ); await transcriber.stopTranscription(); const callsAfterStop = service.getResilienceStatus.mock.calls.length; jest.advanceTimersByTime(10000); await flushMicrotasks(); expect(service.getResilienceStatus.mock.calls.length).toBe(callsAfterStop); }); it("forwards unsupported-provider telemetry snapshots", async () => { const unsupportedSnapshot = createUnsupportedTelemetrySnapshot( "oracle", 1_700_000_000_300 ); const service = new MockTelemetryTranscriptionService(unsupportedSnapshot); jest.spyOn(TranscriptionServiceFactory, "create").mockResolvedValue(service as any); const transcriber = new SofyaTranscriber({ provider: "oracle" as any, endpoint: "ws://unsupported-telemetry-test", config: { language: "en-US", } as any, }); const telemetryHandler = jest.fn(); (transcriber as any).on("telemetry", telemetryHandler); await flushMicrotasks(); expect(telemetryHandler).toHaveBeenCalledWith(unsupportedSnapshot); expect((transcriber as any).getTelemetrySnapshot()).toEqual( expect.objectContaining({ schemaVersion: 2, capabilities: expect.objectContaining({ transcriptUi: false, session: false, connection: false, }), }) ); }); it("derives transport and transcript telemetry from the Whisper adapter", async () => { setConnectionInfo({ effectiveType: "4g", downlink: 12.5, rtt: 34, }); setNavigatorOnline(true); const ws = createTransport({ minReconnectionDelay: 10, maxReconnectionDelay: 10, minUptime: 0, }); const adapter = new WhisperTranscriptionAdapter({ language: "en-US", ws, resilience: { healthCheckIntervalMs: 10, reconnectBaseDelayMs: 10, reconnectMaxDelayMs: 10, maxReconnectAttempts: 3, }, }); const telemetryHandler = jest.fn(); (adapter as any).on("telemetry", telemetryHandler); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); const firstSnapshot = (adapter as any).getTelemetrySnapshot(); expect(firstSnapshot).toEqual( expect.objectContaining({ schemaVersion: 2, status: expect.objectContaining({ connectionState: "connected", websocketState: "open", browserOnline: true, }), }) ); (adapter as any).postMessage(createAudioFrame(0.2)); firstSocket.emitMessage( JSON.stringify({ is_partial: true, data: { text: "hello wo" }, }) ); firstSocket.emitMessage( JSON.stringify({ is_partial: false, data: { text: "hello world", speakers: [ { start: 0, end: 1, sentence: "hello world", speaker: "speaker-1", }, ], }, }) ); await flushMicrotasks(); const transcriptSnapshot = (adapter as any).getTelemetrySnapshot(); expect(transcriptSnapshot.counters["tx.partial.received"]).toBeGreaterThanOrEqual(1); expect(transcriptSnapshot.counters["tx.final.received"]).toBeGreaterThanOrEqual(1); expect(transcriptSnapshot.gauges["network.rtt_ms"]).toBe(34); firstSocket.fail(1006, "connection lost"); await flushMicrotasks(); const reconnectingSnapshot = (adapter as any).getTelemetrySnapshot(); expect(reconnectingSnapshot.status.connectionState).toBe("reconnecting"); expect(reconnectingSnapshot.counters["ws.reconnects"]).toBeGreaterThanOrEqual(1); await advanceTimersAndFlush(10); const secondSocket = MockWebSocket.latest(); secondSocket.open(); await flushMicrotasks(); const recoveredSnapshot = (adapter as any).getTelemetrySnapshot(); expect(recoveredSnapshot.status.connectionState).toBe("connected"); expect(recoveredSnapshot.counters["recovery.completed.count"]).toBeGreaterThanOrEqual(1); expect(telemetryHandler).toHaveBeenCalled(); }); it("keeps advanced audio-capture metrics opt-in via debug config", async () => { const wsWithoutDebug = createTransport({ minReconnectionDelay: 10, maxReconnectionDelay: 10, minUptime: 0, }); const adapterWithoutDebug = new WhisperTranscriptionAdapter({ language: "en-US", ws: wsWithoutDebug, }); await advanceTimersAndFlush(0); const firstSocket = MockWebSocket.latest(); firstSocket.open(); await flushMicrotasks(); (adapterWithoutDebug as any).postMessage(createAudioFrame(1)); await flushMicrotasks(); const snapshotWithoutDebug = (adapterWithoutDebug as any).getTelemetrySnapshot(); expect(snapshotWithoutDebug.capabilities.audioCapture).toBe(false); expect(snapshotWithoutDebug.counters["audio.capture.clipping_events"] ?? 0).toBe(0); expect(snapshotWithoutDebug.gauges["audio.current_rms"]).toBeUndefined(); const wsWithDebug = createTransport({ minReconnectionDelay: 10, maxReconnectionDelay: 10, minUptime: 0, }); const adapterWithDebug = new WhisperTranscriptionAdapter({ language: "en-US", ws: wsWithDebug, debug: true, }); await advanceTimersAndFlush(0); const secondSocket = MockWebSocket.latest(); secondSocket.open(); await flushMicrotasks(); (adapterWithDebug as any).postMessage(createAudioFrame(1)); await flushMicrotasks(); const snapshotWithDebug = (adapterWithDebug as any).getTelemetrySnapshot(); expect(snapshotWithDebug.capabilities.audioCapture).toBe(true); expect(snapshotWithDebug.counters["audio.capture.clipping_events"] ?? 0).toBeGreaterThan(0); expect(typeof snapshotWithDebug.gauges["audio.current_rms"]).toBe("number"); }); });