"use strict";
var import_vitest = require("vitest");
var import_chat_context = require("../llm/chat_context.cjs");
var import_log = require("../log.cjs");
var import_stt = require("../stt/stt.cjs");
var import_audio_recognition = require("./audio_recognition.cjs");
function createHooks() {
  const hooks = {
    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(),
    onEndOfTurn: import_vitest.vi.fn(async () => true),
    onPreemptiveGeneration: import_vitest.vi.fn(),
    onAgentBackchannelOpportunity: import_vitest.vi.fn(),
    onUserTurnExceeded: import_vitest.vi.fn(),
    retrieveChatCtx: () => import_chat_context.ChatContext.empty()
  };
  return hooks;
}
async function flushTasks() {
  await new Promise((resolve) => setTimeout(resolve, 0));
}
async function waitFor(check, timeoutMs = 200) {
  const startedAt = Date.now();
  while (!check()) {
    if (Date.now() - startedAt > timeoutMs) {
      throw new Error("timed out waiting for condition");
    }
    await flushTasks();
  }
}
function createRecognition(sttNode, hooks = createHooks()) {
  return {
    hooks,
    recognition: new import_audio_recognition.AudioRecognition({
      recognitionHooks: hooks,
      stt: sttNode,
      minEndpointingDelay: 0,
      maxEndpointingDelay: 0
    })
  };
}
(0, import_vitest.describe)("AudioRecognition STT pipeline handoff", () => {
  (0, import_log.initializeLogger)({ pretty: false, level: "silent" });
  (0, import_vitest.it)("reuses an injected STT pipeline instead of opening a second STT stream", async () => {
    let sttNodeCalls = 0;
    const sttNode = async () => {
      sttNodeCalls += 1;
      return new ReadableStream({
        start() {
        }
      });
    };
    const pipeline = new import_audio_recognition.STTPipeline(sttNode);
    const { recognition } = createRecognition(sttNode);
    try {
      await recognition.start({ sttPipeline: pipeline });
      await waitFor(() => sttNodeCalls === 1);
      (0, import_vitest.expect)(sttNodeCalls).toBe(1);
    } finally {
      await recognition.close();
      await pipeline.close();
    }
  });
  (0, import_vitest.it)("detaches the pipeline so a new consumer can receive subsequent STT events", async () => {
    let controller;
    const sttNode = async () => new ReadableStream({
      start(ctrl) {
        controller = ctrl;
      }
    });
    const pipeline = new import_audio_recognition.STTPipeline(sttNode);
    const first = createRecognition(sttNode);
    const second = createRecognition(sttNode);
    try {
      await first.recognition.start({ sttPipeline: pipeline });
      await waitFor(() => controller !== void 0);
      const detachedPipeline = await first.recognition.detachSttPipeline();
      await first.recognition.close();
      await second.recognition.start({ sttPipeline: detachedPipeline });
      await flushTasks();
      controller == null ? void 0 : controller.enqueue({
        type: import_stt.SpeechEventType.FINAL_TRANSCRIPT,
        alternatives: [{ text: "reused pipeline", confidence: 0.9 }]
      });
      await waitFor(() => second.hooks.onFinalTranscript.mock.calls.length === 1);
      (0, import_vitest.expect)(first.hooks.onFinalTranscript).not.toHaveBeenCalled();
      (0, import_vitest.expect)(second.hooks.onFinalTranscript).toHaveBeenCalledTimes(1);
    } finally {
      controller == null ? void 0 : controller.close();
      await first.recognition.close();
      await second.recognition.close();
      await pipeline.close();
    }
  });
  (0, import_vitest.it)("resets handoff-sensitive STT state when attaching a pipeline", async () => {
    const sttNode = async () => new ReadableStream({
      start() {
      }
    });
    const pipeline = new import_audio_recognition.STTPipeline(sttNode);
    const { recognition } = createRecognition(sttNode);
    recognition.transcriptBuffer = [
      { type: import_stt.SpeechEventType.FINAL_TRANSCRIPT, alternatives: [{ text: "stale transcript" }] }
    ];
    recognition.ignoreUserTranscriptUntil = Date.now();
    recognition._inputStartedAt = Date.now();
    try {
      await recognition.start({ sttPipeline: pipeline });
      (0, import_vitest.expect)(recognition.transcriptBuffer).toEqual([]);
      (0, import_vitest.expect)(recognition.ignoreUserTranscriptUntil).toBeUndefined();
      (0, import_vitest.expect)(recognition._inputStartedAt).toBeUndefined();
    } finally {
      await recognition.close();
      await pipeline.close();
    }
  });
  (0, import_vitest.it)("recreates the owned STT pipeline when clearing the user turn", async () => {
    let sttNodeCalls = 0;
    const sttNode = async () => {
      sttNodeCalls += 1;
      return new ReadableStream({
        start() {
        }
      });
    };
    const { recognition } = createRecognition(sttNode);
    try {
      await recognition.start();
      await waitFor(() => sttNodeCalls === 1);
      recognition.clearUserTurn();
      await waitFor(() => sttNodeCalls === 2);
    } finally {
      await recognition.close();
    }
  });
  (0, import_vitest.it)("keeps an STT pipeline alive across overlapping clearUserTurn calls", async () => {
    let sttNodeCalls = 0;
    const sttNode = async () => {
      sttNodeCalls += 1;
      return new ReadableStream({
        start() {
        }
      });
    };
    const { recognition } = createRecognition(sttNode);
    try {
      await recognition.start();
      await waitFor(() => sttNodeCalls === 1);
      recognition.clearUserTurn();
      recognition.clearUserTurn();
      await flushTasks();
      await flushTasks();
      await flushTasks();
      (0, import_vitest.expect)(recognition.sttPipeline).toBeDefined();
    } finally {
      await recognition.close();
    }
  });
  (0, import_vitest.it)("does not accumulate user turn limit state after clearing the user turn", async () => {
    const hooks = createHooks();
    const recognition = new import_audio_recognition.AudioRecognition({
      recognitionHooks: hooks,
      minEndpointingDelay: 0,
      maxEndpointingDelay: 0,
      userTurnLimit: { maxWords: 5, maxDuration: null }
    });
    try {
      recognition.checkUserTurnLimit("one two three");
      (0, import_vitest.expect)(recognition.userTurnTracker.words).toBe(3);
      (0, import_vitest.expect)(hooks.onUserTurnExceeded).not.toHaveBeenCalled();
      recognition.clearUserTurn();
      recognition.checkUserTurnLimit("four five six");
      (0, import_vitest.expect)(recognition.userTurnTracker.words).toBe(3);
      (0, import_vitest.expect)(hooks.onUserTurnExceeded).not.toHaveBeenCalled();
    } finally {
      await recognition.close();
    }
  });
  (0, import_vitest.it)("does not recreate a new pipeline after ownership was detached for handoff", async () => {
    let sttNodeCalls = 0;
    const sttNode = async () => {
      sttNodeCalls += 1;
      return new ReadableStream({
        start() {
        }
      });
    };
    const { recognition } = createRecognition(sttNode);
    try {
      await recognition.start();
      await waitFor(() => sttNodeCalls === 1);
      await recognition.detachSttPipeline();
      recognition.clearUserTurn();
      await flushTasks();
      await flushTasks();
      await flushTasks();
      (0, import_vitest.expect)(sttNodeCalls).toBe(1);
      (0, import_vitest.expect)(recognition.sttPipeline).toBeUndefined();
    } finally {
      await recognition.close();
    }
  });
});
//# sourceMappingURL=audio_recognition_handoff.test.cjs.map