import { average, averageFromStreamingSampleStats, createStreamingSampleStats, percentile, p95FromStreamingSampleStats, pushBoundedValue, recordStreamingSample, } from "../src/services/transcription/metrics/stats"; import { createInitialBufferingMetrics, finalizeBufferingMetrics, updateBufferingMetrics, } from "../src/services/transcription/metrics/bufferingMetrics"; import { createInitialRecoveryMetrics, finalizeRecoveryMetrics, updateRecoveryMetrics, } from "../src/services/transcription/metrics/recoveryMetrics"; import { createInitialSessionMetrics, markSessionError, markSessionPause, markSessionReady, markSessionResume, markSessionStop, resetSessionMetrics, } from "../src/services/transcription/metrics/sessionMetrics"; import { createInitialTranscriptUiMetrics, markTranscriptUtteranceStart, recordTranscriptDiarization, recordTranscriptFinal, recordTranscriptPartial, resetTranscriptUiMetrics, } from "../src/services/transcription/metrics/transcriptUiMetrics"; describe("metrics helpers", () => { it("calculates averages and percentiles defensively", () => { expect(average([])).toBeNull(); expect(average([2, 4, 6])).toBe(4); expect(percentile([], 0.95)).toBeNull(); expect(percentile([10, 1, 5], 0)).toBe(1); expect(percentile([10, 1, 5], 0.5)).toBe(5); expect(percentile([10, 1, 5], 0.95)).toBe(10); expect(percentile([10, 1, 5], 2.5)).toBe(10); expect(percentile([10, 1, 5], -1)).toBe(1); expect(pushBoundedValue([1, 2, 3], 4, 3)).toEqual([2, 3, 4]); const streaming = createStreamingSampleStats(); for (let value = 1; value <= 100; value += 1) { recordStreamingSample(streaming, value); } expect(averageFromStreamingSampleStats(streaming)).toBe(50.5); expect(p95FromStreamingSampleStats(streaming)).toBeGreaterThanOrEqual(90); expect(p95FromStreamingSampleStats(streaming)).toBeLessThanOrEqual(100); }); it("tracks session initialization, pause/resume, stop, and reset transitions", () => { let state = createInitialSessionMetrics(1_000, true); state = markSessionReady(state, 1_040); state = resetSessionMetrics(state, 1_100); state = markSessionPause(state, 1_200); state = markSessionPause(state, 1_220); state = markSessionResume(state, 1_400); state = markSessionError(state); state = markSessionError(state, true); state = markSessionStop(state, 1_800); expect(state).toEqual( expect.objectContaining({ supported: true, initializationLatencyMs: 40, sessionStartedAt: 1_100, sessionStoppedAt: 1_800, sessionDurationMs: 500, pauseCount: 1, resumeCount: 1, pausedDurationMs: 200, errorCount: 2, fatalErrorCount: 1, }) ); const reset = resetSessionMetrics(state, 1_900); expect(reset).toEqual( expect.objectContaining({ supported: true, initializationLatencyMs: 40, sessionStartedAt: 1_900, sessionStoppedAt: null, sessionDurationMs: null, pauseCount: 0, resumeCount: 0, pausedDurationMs: 0, errorCount: 0, fatalErrorCount: 0, }) ); }); it("aggregates transcript partial, final, diarization, and latency samples", () => { let state = createInitialTranscriptUiMetrics(true); state = markTranscriptUtteranceStart(state, 1_000); state = recordTranscriptPartial(state, "hello wo", 1_100); state = recordTranscriptPartial(state, "hello world", 1_150); state = recordTranscriptDiarization(state); state = recordTranscriptFinal(state, "hello world", 1_400); expect(state).toEqual( expect.objectContaining({ supported: true, currentPartialText: null, lastFinalText: "hello world", partialCount: 2, finalCount: 1, diarizationCount: 1, utteranceCount: 1, lastFirstPartialLatencyMs: expect.any(Number), avgFirstPartialLatencyMs: expect.any(Number), p95FirstPartialLatencyMs: expect.any(Number), lastFinalLatencyMs: expect.any(Number), avgFinalLatencyMs: expect.any(Number), p95FinalLatencyMs: expect.any(Number), lastStabilizationLatencyMs: expect.any(Number), avgStabilizationLatencyMs: expect.any(Number), p95StabilizationLatencyMs: expect.any(Number), }) ); }); it("clears transcript UI state on session reset", () => { const state = recordTranscriptFinal( recordTranscriptPartial( markTranscriptUtteranceStart(createInitialTranscriptUiMetrics(true), 1_000), "partial", 1_100 ), "final", 1_300 ); expect(state).toEqual( expect.objectContaining({ currentPartialText: null, lastFinalText: "final", partialCount: 1, finalCount: 1, utteranceCount: 1, }) ); const reset = resetTranscriptUiMetrics(state); expect(reset).toEqual( expect.objectContaining({ supported: true, currentPartialText: null, lastFinalText: null, partialCount: 0, finalCount: 0, diarizationCount: 0, utteranceCount: 0, lastFirstPartialLatencyMs: null, avgFirstPartialLatencyMs: null, p95FirstPartialLatencyMs: null, lastFinalLatencyMs: null, avgFinalLatencyMs: null, p95FinalLatencyMs: null, lastStabilizationLatencyMs: null, avgStabilizationLatencyMs: null, p95StabilizationLatencyMs: null, }) ); }); it("tracks transcript, buffering, and recovery aggregates without retaining raw samples", () => { let transcriptState = createInitialTranscriptUiMetrics(true); for (let index = 0; index < 1500; index += 1) { transcriptState = markTranscriptUtteranceStart(transcriptState, index * 10); transcriptState = recordTranscriptPartial( transcriptState, `partial-${index}`, index * 10 + 1 ); transcriptState = recordTranscriptFinal( transcriptState, `final-${index}`, index * 10 + 5 ); } expect(transcriptState.firstPartialLatencyStats.count).toBe(1500); expect(transcriptState.finalLatencyStats.count).toBe(1500); expect(transcriptState.stabilizationLatencyStats.count).toBe(1500); let bufferingState = createInitialBufferingMetrics(); for (let index = 1; index <= 1500; index += 1) { bufferingState = updateBufferingMetrics(bufferingState, { at: index * 10, isBufferingAudio: true, isDrainingAudio: false, pendingBufferedAudioBytes: index, persistedBufferedAudioBytes: 0, persistedBufferedAudioSegments: 0, totalBufferedAudioBytes: index, lastDrainProgressAt: null, drainCycles: 0, drainExitReason: null, finishDeliveryState: null, }); } bufferingState = finalizeBufferingMetrics(bufferingState, 15_500); expect(bufferingState.bufferGrowthRateStats.count).toBeGreaterThan(0); let recoveryState = createInitialRecoveryMetrics(); for (let index = 0; index < 800; index += 1) { recoveryState = updateRecoveryMetrics(recoveryState, { type: "reconnecting", at: index * 20, attempt: 1, delay: 10, }); recoveryState = updateRecoveryMetrics(recoveryState, { type: "reconnected", at: index * 20 + 10, }); } recoveryState = finalizeRecoveryMetrics(recoveryState); expect(recoveryState.recoveryDurationStats.count).toBe(800); }); });