{"version":3,"sources":["../../src/voice/agent_session_transcription_timeout.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n//\n// Session-level coverage for the public `user_transcription_timeout` event.\n//\n// The unit tests in `audio_recognition_transcription_timeout.test.ts` all stop\n// at the `RecognitionHooks` boundary, so none of them reach\n// `AgentActivity.onTranscriptionTimeout` -> `createUserTranscriptionTimeoutEvent`\n// -> `AgentSession.emit`. Python asserts on the real event object in every\n// `tests/test_transcription_timeout.py` scenario; this is the JS counterpart to\n// `test_interim_only_still_fires`, carrying the payload and `created_at`\n// assertions from `test_fires_when_vad_speech_not_transcribed`.\n//\n// Python uses seconds; JS uses milliseconds.\nimport { AudioFrame } from '@livekit/rtc-node';\nimport { ReadableStream, type ReadableStreamDefaultController } from 'node:stream/web';\nimport { describe, expect, it } from 'vitest';\nimport { initializeLogger } from '../log.js';\nimport { FakeSTT } from '../stt/testing/fake_stt.js';\nimport { VAD, type VADEvent, VADEventType, VADStream } from '../vad.js';\nimport { Agent } from './agent.js';\nimport { AgentSession } from './agent_session.js';\nimport { AgentSessionEventTypes, type UserTranscriptionTimeoutEvent } from './events.js';\nimport { AudioInput } from './io.js';\nimport { FakeLLM } from './testing/fake_llm.js';\n\nconst TIMEOUT_MS = 1_000;\nconst SPEECH_DURATION_MS = 800;\n\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\nasync function waitFor(check: () => boolean, timeoutMs: number, label: string): Promise<void> {\n  const startedAt = Date.now();\n  while (!check()) {\n    if (Date.now() - startedAt > timeoutMs) throw new Error(`timed out waiting for ${label}`);\n    await sleep(20);\n  }\n}\n\nfunction silenceFrame(durationMs: number, sampleRate = 16_000): AudioFrame {\n  const samples = Math.floor((sampleRate * durationMs) / 1000);\n  return new AudioFrame(new Int16Array(samples), sampleRate, 1, samples);\n}\n\nfunction vadEvent(\n  type: VADEventType.START_OF_SPEECH | VADEventType.END_OF_SPEECH,\n  options: Partial<VADEvent> = {},\n): VADEvent {\n  return {\n    type,\n    samplesIndex: 0,\n    timestamp: Date.now(),\n    speechDuration: 0,\n    silenceDuration: 0,\n    frames: [],\n    probability: 1,\n    inferenceDuration: 0,\n    speaking: type === VADEventType.START_OF_SPEECH,\n    rawAccumulatedSilence: 0,\n    rawAccumulatedSpeech: 0,\n    ...options,\n  };\n}\n\n/**\n * A VAD whose events the test emits by hand. It subclasses the real\n * VAD/VADStream so `instanceof` checks in AgentActivity hold and the tee'd audio\n * branch stays drained (otherwise backpressure stalls the shared pipeline).\n */\nclass ScriptedVADStream extends VADStream {\n  constructor(vad: VAD) {\n    super(vad);\n    void this.#drain();\n  }\n\n  async #drain(): Promise<void> {\n    try {\n      while (!this.closed) {\n        const { done } = await this.inputReader.read();\n        if (done) break;\n      }\n    } catch {\n      /* stream detached/closed */\n    }\n  }\n\n  emitEvent(ev: VADEvent): void {\n    this.sendVADEvent(ev);\n  }\n}\n\nclass ScriptedVAD extends VAD {\n  label = 'scripted-vad';\n  readonly streams: ScriptedVADStream[] = [];\n\n  constructor() {\n    super({ updateInterval: 32 });\n  }\n\n  stream(): ScriptedVADStream {\n    const stream = new ScriptedVADStream(this);\n    this.streams.push(stream);\n    return stream;\n  }\n}\n\n/** Audio input that lets the test push frames into the session, like a SIP line. */\nclass ScriptedAudioInput extends AudioInput {\n  #controller!: ReadableStreamDefaultController<AudioFrame>;\n\n  constructor() {\n    super();\n    const source = new ReadableStream<AudioFrame>({\n      start: (controller) => {\n        this.#controller = controller;\n      },\n    });\n    this.multiStream.addInputStream(source);\n  }\n\n  push(frame: AudioFrame): void {\n    this.#controller.enqueue(frame);\n  }\n}\n\ndescribe('AgentSession user transcription timeout event', () => {\n  initializeLogger({ pretty: false, level: 'silent' });\n\n  it('emits user_transcription_timeout to a session listener when only an interim transcript arrives', async () => {\n    const vad = new ScriptedVAD();\n    // Interim-only scripted speech: the provider emits an interim result and\n    // never a final one, so the turn stays untranscribed and never commits.\n    const stt = new FakeSTT({\n      capabilities: { streaming: true, interimResults: true },\n      fakeUserSpeeches: [\n        {\n          startTime: 0,\n          endTime: 200,\n          transcript: 'hello how are you',\n          sttDelay: 200,\n          final: false,\n        },\n      ],\n    });\n\n    const session = new AgentSession({\n      stt,\n      vad,\n      llm: new FakeLLM(),\n      transcriptionTimeout: TIMEOUT_MS,\n      turnHandling: { turnDetection: 'vad', interruption: { mode: 'vad' } },\n    });\n\n    const events: UserTranscriptionTimeoutEvent[] = [];\n    session.on(AgentSessionEventTypes.UserTranscriptionTimeout, (ev) => events.push(ev));\n\n    const audioInput = new ScriptedAudioInput();\n    session.input.audio = audioInput;\n    await session.start({ agent: new Agent({ instructions: 'You are a helpful assistant.' }) });\n\n    const pump = setInterval(() => audioInput.push(silenceFrame(50)), 50);\n    let beforeStartOfSpeech = 0;\n    let afterEndOfSpeech = 0;\n    try {\n      await waitFor(() => vad.streams.length > 0, 5_000, 'the VAD stream to open');\n      const stream = vad.streams[0]!;\n\n      beforeStartOfSpeech = Date.now();\n      stream.emitEvent(vadEvent(VADEventType.START_OF_SPEECH));\n      await sleep(SPEECH_DURATION_MS);\n      stream.emitEvent(\n        vadEvent(VADEventType.END_OF_SPEECH, { speechDuration: SPEECH_DURATION_MS }),\n      );\n      afterEndOfSpeech = Date.now();\n\n      await waitFor(() => events.length > 0, 10_000, 'the transcription timeout event');\n    } finally {\n      clearInterval(pump);\n      await session.close().catch(() => {});\n    }\n\n    expect(events).toHaveLength(1);\n    const event = events[0]!;\n    expect(event.type).toBe('user_transcription_timeout');\n    // Accumulated from the VAD END_OF_SPEECH, in milliseconds.\n    expect(event.speechDuration).toBe(SPEECH_DURATION_MS);\n    // Epoch milliseconds pinned to when VAD first detected speech. A seconds\n    // scale would land ~1.7e9 short of this window.\n    expect(event.vadSpeechStartedAt).toBeGreaterThanOrEqual(beforeStartOfSpeech);\n    expect(event.vadSpeechStartedAt).toBeLessThanOrEqual(afterEndOfSpeech);\n    // Python pins `created_at - t_origin` to `2.5 + TIMEOUT`; without virtual\n    // time we can only bound it, but the lower bound still proves the event was\n    // stamped no earlier than a full timeout after the turn started.\n    expect(Number.isFinite(event.createdAt)).toBe(true);\n    expect(event.createdAt).toBeGreaterThanOrEqual(event.vadSpeechStartedAt + TIMEOUT_MS);\n    expect(event.createdAt).toBeLessThanOrEqual(Date.now());\n  }, 30_000);\n});\n"],"mappings":"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","names":[]}