{"version":3,"sources":["../../../src/inference/interruption/interruption_speech_boundary.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { AudioFrame } from '@livekit/rtc-node';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { MockWebSocket } from './_mock_ws.js';\nimport { AdaptiveInterruptionDetector } from './interruption_detector.js';\nimport { InterruptionStreamBase, InterruptionStreamSentinel } from './interruption_stream.js';\nimport type { OverlappingSpeechEvent } from './types.js';\n\nvi.mock('ws', async () => {\n  const { MockWebSocket } = await import('./_mock_ws.js');\n  return { default: MockWebSocket, WebSocket: MockWebSocket };\n});\n\nfunction makeAudioFrame(): AudioFrame {\n  const samples = new Int16Array(1600);\n  return new AudioFrame(samples, 16000, 1, samples.length);\n}\n\nfunction requestIds(ws: MockWebSocket): number[] {\n  return ws.sent\n    .filter((frame): frame is Uint8Array => frame instanceof Uint8Array)\n    .map((frame) => {\n      const view = new DataView(frame.buffer, frame.byteOffset, 8);\n      return view.getUint32(0, true) + view.getUint32(4, true) * 0x100000000;\n    });\n}\n\nasync function openStream(): Promise<{\n  stream: InterruptionStreamBase;\n  reader: ReadableStreamDefaultReader<OverlappingSpeechEvent>;\n  ws: MockWebSocket;\n}> {\n  const detector = new AdaptiveInterruptionDetector({\n    baseUrl: 'http://localhost:9999',\n    apiKey: 'test-key',\n    apiSecret: 'test-secret',\n    threshold: 0.5,\n  });\n  const stream = new InterruptionStreamBase(detector, {});\n  const reader = stream.stream().getReader();\n\n  await vi.waitFor(() => expect(MockWebSocket.instances).toHaveLength(1));\n  const ws = MockWebSocket.instances[0]!;\n  ws.simulateOpen();\n  await vi.waitFor(() => expect(ws.sent).toHaveLength(1));\n\n  return { stream, reader, ws };\n}\n\nasync function startAgentOverlap(\n  stream: InterruptionStreamBase,\n  ws: MockWebSocket,\n  expectedRequestCount: number,\n): Promise<number> {\n  await stream.pushFrame(InterruptionStreamSentinel.agentSpeechStarted());\n  await stream.pushFrame(InterruptionStreamSentinel.overlapSpeechStarted(0, Date.now()));\n  await stream.pushFrame(makeAudioFrame());\n  await vi.waitFor(() => expect(requestIds(ws)).toHaveLength(expectedRequestCount));\n  return requestIds(ws).at(-1)!;\n}\n\nasync function endAgentOverlap(\n  stream: InterruptionStreamBase,\n  reader: ReadableStreamDefaultReader<OverlappingSpeechEvent>,\n): Promise<void> {\n  const event = reader.read();\n  await stream.pushFrame(InterruptionStreamSentinel.overlapSpeechEnded(Date.now(), true));\n  await stream.pushFrame(InterruptionStreamSentinel.agentSpeechEnded());\n  await expect(event).resolves.toMatchObject({ value: { isInterruption: false } });\n}\n\nbeforeEach(() => {\n  MockWebSocket.instances.length = 0;\n});\n\ndescribe('interruption request speech boundaries', () => {\n  it('ignores a late verdict from a previous agent speech', async () => {\n    const { stream, reader, ws } = await openStream();\n\n    try {\n      const oldRequestId = await startAgentOverlap(stream, ws, 1);\n      await endAgentOverlap(stream, reader);\n      const newRequestId = await startAgentOverlap(stream, ws, 2);\n\n      const verdict = reader.read();\n      ws.simulateMessage({\n        type: 'bargein_detected',\n        created_at: oldRequestId,\n        probabilities: [0.99, 0.99],\n      });\n      ws.simulateMessage({\n        type: 'bargein_detected',\n        created_at: newRequestId,\n        probabilities: [0.75, 0.75],\n      });\n\n      await expect(verdict).resolves.toMatchObject({\n        value: {\n          isInterruption: true,\n          probability: 0.75,\n          speechInput: expect.any(Int16Array),\n        },\n      });\n    } finally {\n      await reader.cancel();\n      await stream.close();\n    }\n  });\n\n  it('ignores a late inference result from a previous agent speech', async () => {\n    const { stream, reader, ws } = await openStream();\n\n    try {\n      const oldRequestId = await startAgentOverlap(stream, ws, 1);\n      await endAgentOverlap(stream, reader);\n      const newRequestId = await startAgentOverlap(stream, ws, 2);\n\n      ws.simulateMessage({\n        type: 'inference_done',\n        created_at: oldRequestId,\n        probabilities: [0.99, 0.99],\n      });\n      ws.simulateMessage({\n        type: 'inference_done',\n        created_at: newRequestId,\n        probabilities: [0.25, 0.25],\n      });\n\n      const result = reader.read();\n      await stream.pushFrame(InterruptionStreamSentinel.overlapSpeechEnded(Date.now()));\n\n      await expect(result).resolves.toMatchObject({\n        value: {\n          isInterruption: false,\n          probability: 0.25,\n          speechInput: expect.any(Int16Array),\n        },\n      });\n    } finally {\n      await reader.cancel();\n      await stream.close();\n    }\n  });\n});\n"],"mappings":"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","names":["MockWebSocket"]}