"use strict";
var import_vitest = require("vitest");
var import_chat_context = require("../llm/chat_context.cjs");
var import_log = require("../log.cjs");
var import_audio_recognition = require("./audio_recognition.cjs");
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)
  };
}
function createRecognition(overrides = {}) {
  return new import_audio_recognition.AudioRecognition({
    recognitionHooks: createHooks(),
    minEndpointingDelay: 0,
    maxEndpointingDelay: 0,
    ...overrides
  });
}
function emitOverlapSpeechEvent(ar, ev) {
  ar.onOverlapSpeechEvent(ev);
}
function overlapSpeechEvent(isInterruption) {
  return {
    type: "overlapping_speech",
    detectedAt: Date.now(),
    isInterruption,
    totalDurationInS: 0,
    predictionDurationInS: 0,
    detectionDelayInS: 0,
    probability: isInterruption ? 1 : 0,
    numRequests: 1
  };
}
(0, import_vitest.describe)("AudioRecognition backchannel boundary", () => {
  (0, import_log.initializeLogger)({ pretty: false, level: "silent" });
  (0, import_vitest.beforeEach)(() => {
    import_vitest.vi.useFakeTimers();
  });
  (0, import_vitest.afterEach)(() => {
    import_vitest.vi.useRealTimers();
  });
  (0, import_vitest.describe)("option normalization", () => {
    (0, import_vitest.it)("accepts a single number and applies it to the start side", async () => {
      const ar = createRecognition({ backchannelBoundary: 250 });
      const cb = import_vitest.vi.fn();
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb;
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(true);
      import_vitest.vi.advanceTimersByTime(249);
      (0, import_vitest.expect)(cb).not.toHaveBeenCalled();
      import_vitest.vi.advanceTimersByTime(1);
      (0, import_vitest.expect)(cb).toHaveBeenCalledTimes(1);
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(false);
      await ar.close();
    });
    (0, import_vitest.it)("accepts a [start, end] tuple and uses the start value for the start timer", async () => {
      const ar = createRecognition({ backchannelBoundary: [100, 999] });
      const cb = import_vitest.vi.fn();
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb;
      import_vitest.vi.advanceTimersByTime(99);
      (0, import_vitest.expect)(cb).not.toHaveBeenCalled();
      import_vitest.vi.advanceTimersByTime(1);
      (0, import_vitest.expect)(cb).toHaveBeenCalledTimes(1);
      await ar.close();
    });
    (0, import_vitest.it)("disables both sides cleanly when set to null", async () => {
      const ar = createRecognition({ backchannelBoundary: null });
      const cb = import_vitest.vi.fn();
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb;
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(false);
      import_vitest.vi.advanceTimersByTime(6e4);
      (0, import_vitest.expect)(cb).not.toHaveBeenCalled();
      await ar.close();
    });
    (0, import_vitest.it)("disables both sides cleanly when undefined", async () => {
      const ar = createRecognition();
      await ar.onStartOfAgentSpeech(Date.now());
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(false);
      await ar.close();
    });
    (0, import_vitest.it)("does not arm the start timer when start cooldown is 0", async () => {
      const ar = createRecognition({ backchannelBoundary: [0, 500] });
      const cb = import_vitest.vi.fn();
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb;
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(false);
      import_vitest.vi.advanceTimersByTime(1e3);
      (0, import_vitest.expect)(cb).not.toHaveBeenCalled();
      await ar.close();
    });
    (0, import_vitest.it)("throws on a negative single number", () => {
      (0, import_vitest.expect)(() => createRecognition({ backchannelBoundary: -1 })).toThrow(/non-negative number/);
    });
    (0, import_vitest.it)("throws on a tuple with a negative element", () => {
      (0, import_vitest.expect)(() => createRecognition({ backchannelBoundary: [100, -1] })).toThrow(
        /non-negative numbers/
      );
    });
  });
  (0, import_vitest.describe)("lifecycle", () => {
    (0, import_vitest.it)("re-arms the start cooldown when onStartOfAgentSpeech is called again", async () => {
      const ar = createRecognition({ backchannelBoundary: 200 });
      const cb1 = import_vitest.vi.fn();
      const cb2 = import_vitest.vi.fn();
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb1;
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(true);
      import_vitest.vi.advanceTimersByTime(50);
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb2;
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(true);
      import_vitest.vi.advanceTimersByTime(199);
      (0, import_vitest.expect)(cb1).not.toHaveBeenCalled();
      (0, import_vitest.expect)(cb2).not.toHaveBeenCalled();
      import_vitest.vi.advanceTimersByTime(1);
      (0, import_vitest.expect)(cb1).not.toHaveBeenCalled();
      (0, import_vitest.expect)(cb2).toHaveBeenCalledTimes(1);
      await ar.close();
    });
    (0, import_vitest.it)("cancelBackchannelBoundary clears both the timer and the callback", async () => {
      const ar = createRecognition({ backchannelBoundary: 500 });
      const cb = import_vitest.vi.fn();
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb;
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(true);
      ar.cancelBackchannelBoundary();
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(false);
      (0, import_vitest.expect)(ar.backchannelBoundaryCallback).toBeUndefined();
      import_vitest.vi.advanceTimersByTime(1e3);
      (0, import_vitest.expect)(cb).not.toHaveBeenCalled();
      await ar.close();
    });
    (0, import_vitest.it)("onEndOfAgentSpeech cancels a pending start-boundary timer", async () => {
      const ar = createRecognition({ backchannelBoundary: [1e3, 500] });
      const cb = import_vitest.vi.fn();
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb;
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(true);
      await ar.onEndOfAgentSpeech(Date.now() + 1e3);
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(false);
      import_vitest.vi.advanceTimersByTime(2e3);
      (0, import_vitest.expect)(cb).not.toHaveBeenCalled();
      await ar.close();
    });
    (0, import_vitest.it)("disableInterruptionDetection() cancels a pending boundary timer", async () => {
      const ar = createRecognition({ backchannelBoundary: 1e3 });
      const cb = import_vitest.vi.fn();
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb;
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(true);
      await ar.disableInterruptionDetection();
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(false);
      import_vitest.vi.advanceTimersByTime(2e3);
      (0, import_vitest.expect)(cb).not.toHaveBeenCalled();
      await ar.close();
    });
    (0, import_vitest.it)("close() cancels a pending boundary timer (no leak on shutdown)", async () => {
      const ar = createRecognition({ backchannelBoundary: 1e3 });
      const cb = import_vitest.vi.fn();
      await ar.onStartOfAgentSpeech(Date.now());
      ar.backchannelBoundaryCallback = cb;
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(true);
      await ar.close();
      (0, import_vitest.expect)(ar.backchannelBoundaryActive).toBe(false);
      import_vitest.vi.advanceTimersByTime(2e3);
      (0, import_vitest.expect)(cb).not.toHaveBeenCalled();
    });
  });
  (0, import_vitest.describe)("overlap events", () => {
    (0, import_vitest.it)("suppresses backchannels during the boundary without blocking interruptions", async () => {
      const hooks = createHooks();
      const ar = new import_audio_recognition.AudioRecognition({
        recognitionHooks: hooks,
        minEndpointingDelay: 0,
        maxEndpointingDelay: 0,
        backchannelBoundary: [50, 0]
      });
      await ar.onStartOfAgentSpeech(Date.now());
      emitOverlapSpeechEvent(ar, overlapSpeechEvent(false));
      (0, import_vitest.expect)(hooks.onInterruption).not.toHaveBeenCalled();
      emitOverlapSpeechEvent(ar, overlapSpeechEvent(true));
      (0, import_vitest.expect)(hooks.onInterruption).toHaveBeenCalledTimes(1);
      import_vitest.vi.advanceTimersByTime(50);
      emitOverlapSpeechEvent(ar, overlapSpeechEvent(false));
      emitOverlapSpeechEvent(ar, overlapSpeechEvent(true));
      (0, import_vitest.expect)(hooks.onInterruption).toHaveBeenCalledTimes(2);
      await ar.close();
    });
  });
});
//# sourceMappingURL=audio_recognition_backchannel.test.cjs.map