"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var import_rtc_node = require("@livekit/rtc-node");
var import_web = require("node:stream/web");
var import_vitest = require("vitest");
var import_exceptions = require("../../_exceptions.cjs");
var import_chat_context = require("../../llm/chat_context.cjs");
var import_log = require("../../log.cjs");
var import_audio_recognition = require("../../voice/audio_recognition.cjs");
var import_mock_ws = require("./_mock_ws.cjs");
var import_defaults = require("./defaults.cjs");
var import_interruption_detector = require("./interruption_detector.cjs");
var import_interruption_stream = require("./interruption_stream.cjs");
import_vitest.vi.mock("ws", async () => {
  const { MockWebSocket: MockWebSocket2 } = await import("./_mock_ws.cjs");
  return { default: MockWebSocket2, WebSocket: MockWebSocket2 };
});
(0, import_log.initializeLogger)({ pretty: false, level: "silent" });
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function waitForInstance(timeoutMs = 2e3) {
  const start = performance.now();
  while (import_mock_ws.MockWebSocket.instances.length === 0) {
    if (performance.now() - start > timeoutMs) {
      throw new Error("WebSocket instance was never constructed");
    }
    await sleep(5);
  }
  return import_mock_ws.MockWebSocket.instances[import_mock_ws.MockWebSocket.instances.length - 1];
}
async function waitForInstanceCount(n, timeoutMs = 2e3) {
  const start = performance.now();
  while (import_mock_ws.MockWebSocket.instances.length < n) {
    if (performance.now() - start > timeoutMs) {
      throw new Error(`expected ${n} WebSocket instances, saw ${import_mock_ws.MockWebSocket.instances.length}`);
    }
    await sleep(5);
  }
  return import_mock_ws.MockWebSocket.instances[n - 1];
}
async function waitFor(predicate, timeoutMs = 2e3) {
  const start = performance.now();
  while (!predicate()) {
    if (performance.now() - start > timeoutMs) {
      throw new Error("condition not met within timeout");
    }
    await sleep(5);
  }
}
function stillAlive(errPromise, ms = 30) {
  return Promise.race([errPromise, sleep(ms).then(() => "alive")]);
}
function sessionCreateSettings(ws) {
  return JSON.parse(String(ws.sent[0])).settings;
}
function makeAudioFrame(numSamples = 1600, sampleRate = 16e3) {
  const data = new Int16Array(numSamples);
  return new import_rtc_node.AudioFrame(data, sampleRate, 1, numSamples);
}
function createDetector(opts = {}) {
  return new import_interruption_detector.AdaptiveInterruptionDetector({
    baseUrl: "http://localhost:9999",
    apiKey: "test-key",
    apiSecret: "test-secret",
    ...opts
  });
}
async function readError(stream) {
  const reader = stream.stream().getReader();
  try {
    for (; ; ) {
      const { done } = await reader.read();
      if (done) return void 0;
    }
  } catch (e) {
    return e;
  } finally {
    try {
      reader.releaseLock();
    } catch {
    }
  }
}
(0, import_vitest.beforeEach)(() => {
  import_mock_ws.MockWebSocket.instances.length = 0;
});
(0, import_vitest.afterEach)(() => {
  import_vitest.vi.clearAllMocks();
});
(0, import_vitest.describe)("interruption WebSocket transport failover", () => {
  (0, import_vitest.it)("surfaces a non-retryable APITimeoutError on connection timeout", async () => {
    const detector = createDetector();
    const stream = new import_interruption_stream.InterruptionStreamBase(detector, { timeout: 50 });
    const err = await readError(stream);
    (0, import_vitest.expect)(err).toBeInstanceOf(import_exceptions.APITimeoutError);
    (0, import_vitest.expect)(err.retryable).toBe(false);
    await stream.close();
  });
  (0, import_vitest.it)("surfaces a non-retryable APIStatusError on connection 429", async () => {
    const detector = createDetector();
    const stream = new import_interruption_stream.InterruptionStreamBase(detector, {});
    const errPromise = readError(stream);
    const ws = await waitForInstance();
    ws.simulateUnexpectedResponse(429);
    const err = await errPromise;
    (0, import_vitest.expect)(err).toBeInstanceOf(import_exceptions.APIStatusError);
    (0, import_vitest.expect)(err.statusCode).toBe(429);
    (0, import_vitest.expect)(err.retryable).toBe(false);
    await stream.close();
  });
  (0, import_vitest.it)("surfaces a non-retryable 408 APIStatusError when inference responses time out", async () => {
    const inferenceTimeout = 50;
    const detector = createDetector({ inferenceTimeout });
    const stream = new import_interruption_stream.InterruptionStreamBase(detector, {});
    const errPromise = readError(stream);
    const ws = await waitForInstance();
    ws.simulateOpen();
    await sleep(5);
    await stream.pushFrame(import_interruption_stream.InterruptionStreamSentinel.agentSpeechStarted());
    await stream.pushFrame(import_interruption_stream.InterruptionStreamSentinel.overlapSpeechStarted(500, Date.now()));
    await stream.pushFrame(makeAudioFrame());
    await sleep(inferenceTimeout + 40);
    await stream.pushFrame(makeAudioFrame());
    const err = await errPromise;
    (0, import_vitest.expect)(err).toBeInstanceOf(import_exceptions.APIStatusError);
    (0, import_vitest.expect)(err.statusCode).toBe(408);
    (0, import_vitest.expect)(err.retryable).toBe(false);
    await stream.close();
  });
  (0, import_vitest.it)("errors when session.created reports no threshold and the user did not override", async () => {
    const detector = createDetector();
    const stream = new import_interruption_stream.InterruptionStreamBase(detector, {});
    const errPromise = readError(stream);
    const ws = await waitForInstance();
    ws.simulateOpen();
    await sleep(5);
    ws.simulateMessage({ type: "session.created" });
    const err = await errPromise;
    (0, import_vitest.expect)(err).toBeInstanceOf(import_exceptions.APIStatusError);
    (0, import_vitest.expect)(err.statusCode).toBe(500);
    (0, import_vitest.expect)(err.retryable).toBe(false);
    await stream.close();
  });
  (0, import_vitest.it)("does not error when session.created carries a default_threshold", async () => {
    const detector = createDetector();
    const stream = new import_interruption_stream.InterruptionStreamBase(detector, {});
    const errPromise = readError(stream);
    const ws = await waitForInstance();
    ws.simulateOpen();
    await sleep(5);
    ws.simulateMessage({ type: "session.created", default_threshold: 0.42 });
    await sleep(5);
    await stream.close();
    (0, import_vitest.expect)(await errPromise).toBeUndefined();
  });
  (0, import_vitest.it)("closes the underlying WebSocket on teardown even after the transport errored", async () => {
    const inferenceTimeout = 50;
    const detector = createDetector({ inferenceTimeout });
    const stream = new import_interruption_stream.InterruptionStreamBase(detector, {});
    const errPromise = readError(stream);
    const ws = await waitForInstance();
    ws.simulateOpen();
    await sleep(5);
    await stream.pushFrame(import_interruption_stream.InterruptionStreamSentinel.agentSpeechStarted());
    await stream.pushFrame(import_interruption_stream.InterruptionStreamSentinel.overlapSpeechStarted(500, Date.now()));
    await stream.pushFrame(makeAudioFrame());
    await sleep(inferenceTimeout + 40);
    await stream.pushFrame(makeAudioFrame());
    await errPromise;
    (0, import_vitest.expect)(ws.readyState).toBe(import_mock_ws.MockWebSocket.OPEN);
    await stream.close();
    (0, import_vitest.expect)(ws.readyState).toBe(3);
  });
});
(0, import_vitest.describe)("interruption updateOptions reconnect", () => {
  (0, import_vitest.it)("reconnects in place with the updated threshold without erroring the stream", async () => {
    const detector = createDetector();
    const stream = new import_interruption_stream.InterruptionStreamBase(detector, {});
    const errPromise = readError(stream);
    const ws1 = await waitForInstance();
    ws1.simulateOpen();
    await waitFor(() => ws1.sent.length > 0);
    (0, import_vitest.expect)("threshold" in sessionCreateSettings(ws1)).toBe(false);
    await stream.updateOptions({ threshold: 0.7 });
    const ws2 = await waitForInstanceCount(2);
    (0, import_vitest.expect)(ws2).not.toBe(ws1);
    (0, import_vitest.expect)(ws1.readyState).toBe(3);
    ws2.simulateOpen();
    await waitFor(() => ws2.sent.length > 0);
    (0, import_vitest.expect)(sessionCreateSettings(ws2).threshold).toBe(0.7);
    await (0, import_vitest.expect)(stillAlive(errPromise)).resolves.toBe("alive");
    await stream.close();
  });
  (0, import_vitest.it)("does not error the stream across more reconnects than the failover budget", async () => {
    const detector = createDetector();
    const stream = new import_interruption_stream.InterruptionStreamBase(detector, {});
    const errPromise = readError(stream);
    const ws1 = await waitForInstance();
    ws1.simulateOpen();
    await waitFor(() => ws1.sent.length > 0);
    const rounds = import_defaults.apiConnectDefaults.maxRetries + 2;
    for (let i = 0; i < rounds; i++) {
      await stream.updateOptions({ threshold: 0.5 + (i + 1) * 0.01 });
      const ws = await waitForInstanceCount(i + 2);
      ws.simulateOpen();
      await waitFor(() => ws.sent.length > 0);
    }
    (0, import_vitest.expect)(import_mock_ws.MockWebSocket.instances.length).toBe(rounds + 1);
    await (0, import_vitest.expect)(stillAlive(errPromise)).resolves.toBe("alive");
    await stream.close();
  });
  (0, import_vitest.it)("errors the stream when the post-updateOptions reconnect genuinely fails", async () => {
    const detector = createDetector();
    const stream = new import_interruption_stream.InterruptionStreamBase(detector, {});
    const errPromise = readError(stream);
    const ws1 = await waitForInstance();
    ws1.simulateOpen();
    await waitFor(() => ws1.sent.length > 0);
    await stream.updateOptions({ threshold: 0.7 });
    const ws2 = await waitForInstanceCount(2);
    ws2.simulateUnexpectedResponse(429);
    const err = await errPromise;
    (0, import_vitest.expect)(err).toBeInstanceOf(import_exceptions.APIStatusError);
    (0, import_vitest.expect)(err.statusCode).toBe(429);
    await stream.close();
  });
});
function createHooks() {
  return {
    onInterruption: import_vitest.vi.fn(),
    onStartOfSpeech: import_vitest.vi.fn(),
    onVADInferenceDone: import_vitest.vi.fn(),
    onEndOfSpeech: import_vitest.vi.fn(),
    onInterimTranscript: import_vitest.vi.fn(),
    onFinalTranscript: import_vitest.vi.fn(),
    onPreemptiveGeneration: import_vitest.vi.fn(),
    onAgentBackchannelOpportunity: import_vitest.vi.fn(),
    retrieveChatCtx: () => import_chat_context.ChatContext.empty(),
    onEndOfTurn: import_vitest.vi.fn(async () => true)
  };
}
(0, import_vitest.describe)("interruption failover error emission", () => {
  (0, import_vitest.it)("emits exactly one unrecoverable error for a non-retryable transport failure", async () => {
    const transportError = new import_exceptions.APIStatusError({
      message: "WebSocket connection rejected with status 429",
      options: { statusCode: 429, retryable: false }
    });
    (0, import_vitest.expect)(transportError.retryable).toBe(false);
    const errors = [];
    const erroringStream = {
      stream: () => new import_web.ReadableStream({
        start(controller) {
          controller.error(transportError);
        }
      }),
      pushFrame: async () => {
      },
      close: async () => {
      }
    };
    const mockDetector = {
      label: "mock-detector",
      createStream: () => erroringStream,
      emitError: (e) => errors.push(e)
    };
    const recognition = new import_audio_recognition.AudioRecognition({
      recognitionHooks: createHooks(),
      interruptionDetection: mockDetector
    });
    const ac = new AbortController();
    const task = recognition.createInterruptionTask(mockDetector, ac.signal);
    const start = performance.now();
    while (errors.length === 0 && performance.now() - start < 2e3) {
      await sleep(5);
    }
    ac.abort();
    await task;
    (0, import_vitest.expect)(errors.filter((e) => e.recoverable)).toHaveLength(0);
    (0, import_vitest.expect)(errors.filter((e) => !e.recoverable)).toHaveLength(1);
  });
});
//# sourceMappingURL=interruption_failover.test.cjs.map