"use strict";
var import_node_events = require("node:events");
var import_vitest = require("vitest");
var import_chat_context = require("../llm/chat_context.cjs");
var import_llm = require("../llm/llm.cjs");
var import_stt = require("../stt/stt.cjs");
var import_amd = require("./amd.cjs");
const speechStart = (amd) => amd.onUserSpeechStarted();
const speechEnd = (amd, silenceDurationMs = 0) => amd.onUserSpeechEnded(silenceDurationMs);
const pushTranscript = (amd, text, source = "stt") => amd.onTranscript(text, source);
const makeEotInfo = (newTranscript) => ({
  newTranscript,
  transcriptConfidence: 1,
  transcriptionDelay: 0,
  endOfUtteranceDelay: 0,
  startedSpeakingAt: void 0,
  stoppedSpeakingAt: void 0
});
class StaticLLM extends import_llm.LLM {
  constructor(response) {
    super();
    this.response = response;
  }
  label() {
    return "static-llm";
  }
  chat({
    chatCtx: _chatCtx,
    toolCtx: _toolCtx,
    connOptions: _connOptions
  }) {
    const response = this.response;
    return {
      async *[Symbol.asyncIterator]() {
        if (response instanceof Error) {
          throw response;
        }
        yield {
          id: "static",
          delta: { role: "assistant", content: response }
        };
      }
    };
  }
}
class MockSession extends import_node_events.EventEmitter {
  llm;
  pauseReplyAuthorization = import_vitest.vi.fn();
  resumeReplyAuthorization = import_vitest.vi.fn();
  interrupt = import_vitest.vi.fn(() => ({ await: Promise.resolve() }));
}
const asAgentSession = (session) => session;
(0, import_vitest.describe)("AMD", () => {
  (0, import_vitest.it)("should classify voicemail and interrupt queued speech", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(
      JSON.stringify({
        category: import_amd.AMDCategory.MACHINE_VM,
        reason: "The transcript is a voicemail greeting."
      })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), { llm, detectionTimeoutMs: 50 });
    const onPrediction = import_vitest.vi.fn();
    amd.on("amd_prediction", onPrediction);
    const promise = amd.execute();
    pushTranscript(amd, "Please leave a message after the tone");
    await (0, import_vitest.expect)(promise).resolves.toMatchObject({
      type: "amd_prediction",
      category: import_amd.AMDCategory.MACHINE_VM,
      isMachine: true
    });
    (0, import_vitest.expect)(session.pauseReplyAuthorization).toHaveBeenCalledTimes(1);
    (0, import_vitest.expect)(session.resumeReplyAuthorization).toHaveBeenCalled();
    (0, import_vitest.expect)(session.interrupt).toHaveBeenCalledWith({ force: true });
    (0, import_vitest.expect)(onPrediction).toHaveBeenCalledTimes(1);
    (0, import_vitest.expect)(onPrediction.mock.calls[0][0]).toMatchObject({
      type: "amd_prediction",
      category: import_amd.AMDCategory.MACHINE_VM
    });
  });
  (0, import_vitest.it)("onEndOfTurn signals skip-reply after a machine verdict", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(
      JSON.stringify({ category: import_amd.AMDCategory.MACHINE_VM, reason: "voicemail greeting" })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), { llm, detectionTimeoutMs: 50 });
    const promise = amd.execute();
    pushTranscript(amd, "Please leave a message after the tone");
    await promise;
    (0, import_vitest.expect)(amd.onEndOfTurn(makeEotInfo("Please leave a message after the tone"))).toBe(true);
  });
  (0, import_vitest.it)("onEndOfTurn does not skip-reply for a human verdict", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(
      JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "live person" })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), { llm, detectionTimeoutMs: 50 });
    const promise = amd.execute();
    pushTranscript(amd, "hello there");
    await promise;
    (0, import_vitest.expect)(amd.onEndOfTurn(makeEotInfo("hello there"))).toBe(false);
  });
  (0, import_vitest.it)("onEndOfTurn does not skip-reply when interruptOnMachine is false", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(
      JSON.stringify({ category: import_amd.AMDCategory.MACHINE_VM, reason: "voicemail greeting" })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      detectionTimeoutMs: 50,
      interruptOnMachine: false
    });
    const promise = amd.execute();
    pushTranscript(amd, "Please leave a message after the tone");
    await promise;
    (0, import_vitest.expect)(amd.onEndOfTurn(makeEotInfo("Please leave a message after the tone"))).toBe(false);
  });
  (0, import_vitest.it)("onEndOfTurn does not skip-reply while a machine verdict is committed but not yet emitted", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(
      JSON.stringify({ category: import_amd.AMDCategory.MACHINE_VM, reason: "voicemail greeting" })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      // keep the post-speech silence gate closed long enough to call onEndOfTurn
      // while the machine verdict is committed but still gated (not emitted).
      machineSilenceThresholdMs: 1e3,
      maxEndpointingDelayMs: 5e3,
      detectionTimeoutMs: 5e3,
      suppressCompatibilityWarning: true
    });
    const promise = amd.execute();
    speechStart(amd);
    pushTranscript(amd, "Please leave a message after the tone");
    speechEnd(amd, 0);
    await new Promise((r) => setTimeout(r, 50));
    (0, import_vitest.expect)(amd.onEndOfTurn(makeEotInfo("Please leave a message after the tone"))).toBe(false);
    const result = await promise;
    (0, import_vitest.expect)(result.category).toBe(import_amd.AMDCategory.MACHINE_VM);
    (0, import_vitest.expect)(amd.onEndOfTurn(makeEotInfo("Please leave a message after the tone"))).toBe(true);
  }, 5e3);
  (0, import_vitest.it)("should forward predictions to session._onAmdPrediction", async () => {
    const onAmdPrediction = import_vitest.vi.fn();
    const session = Object.assign(new MockSession(), { _onAmdPrediction: onAmdPrediction });
    const llm = new StaticLLM(
      JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "live person" })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), { llm, detectionTimeoutMs: 50 });
    const promise = amd.execute();
    pushTranscript(amd, "hello there");
    await promise;
    (0, import_vitest.expect)(onAmdPrediction).toHaveBeenCalledTimes(1);
    (0, import_vitest.expect)(onAmdPrediction.mock.calls[0][0]).toMatchObject({
      type: "amd_prediction",
      category: import_amd.AMDCategory.HUMAN
    });
  });
  (0, import_vitest.it)("should classify unavailable mailbox as machine", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(
      JSON.stringify({
        category: import_amd.AMDCategory.MACHINE_UNAVAILABLE,
        reason: "The mailbox is unavailable and cannot accept messages."
      })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), { llm, detectionTimeoutMs: 50 });
    const promise = amd.execute();
    pushTranscript(amd, "The mailbox you are trying to reach is unavailable");
    await (0, import_vitest.expect)(promise).resolves.toMatchObject({
      category: import_amd.AMDCategory.MACHINE_UNAVAILABLE,
      isMachine: true
    });
  });
  (0, import_vitest.it)("should resume authorization when detection fails", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(new Error("boom"));
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), { llm });
    const promise = amd.execute();
    pushTranscript(amd, "Hello?");
    await (0, import_vitest.expect)(promise).rejects.toThrow("boom");
    (0, import_vitest.expect)(session.resumeReplyAuthorization).toHaveBeenCalled();
  });
  (0, import_vitest.it)("should settle the execute promise when aclose is called", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "test" }));
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), { llm });
    const promise = amd.execute();
    await amd.aclose();
    await (0, import_vitest.expect)(promise).rejects.toThrow("AMD closed");
    (0, import_vitest.expect)(session.resumeReplyAuthorization).toHaveBeenCalled();
  });
  (0, import_vitest.it)("should settle from a save_prediction tool call", async () => {
    class ToolCallLLM extends import_llm.LLM {
      label() {
        return "tool-call-llm";
      }
      chat({}) {
        return {
          async *[Symbol.asyncIterator]() {
            yield {
              id: "tc",
              delta: {
                role: "assistant",
                toolCalls: [
                  new import_chat_context.FunctionCall({
                    callId: "call_1",
                    name: "save_prediction",
                    args: JSON.stringify({ label: import_amd.AMDCategory.MACHINE_IVR })
                  })
                ]
              }
            };
          }
        };
      }
    }
    const session = new MockSession();
    const llm = new ToolCallLLM();
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), { llm, detectionTimeoutMs: 50 });
    const promise = amd.execute();
    pushTranscript(amd, "Press 1 for sales, 2 for support");
    await (0, import_vitest.expect)(promise).resolves.toMatchObject({
      category: import_amd.AMDCategory.MACHINE_IVR,
      reason: "llm",
      isMachine: true
    });
    (0, import_vitest.expect)(session.interrupt).toHaveBeenCalledWith({ force: true });
  });
  (0, import_vitest.it)("should accept the new tunable parameters", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(
      JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "live person" })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      humanSpeechThresholdMs: 1e3,
      humanSilenceThresholdMs: 250,
      machineSilenceThresholdMs: 750,
      prompt: "custom prompt",
      participantIdentity: "caller-1",
      suppressCompatibilityWarning: true,
      detectionTimeoutMs: 50
    });
    const promise = amd.execute();
    pushTranscript(amd, "Hello?");
    await (0, import_vitest.expect)(promise).resolves.toMatchObject({ category: import_amd.AMDCategory.HUMAN });
  });
  (0, import_vitest.it)("should not fire short_greeting when a transcript arrives late", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(
      JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "llm-verified" })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      humanSilenceThresholdMs: 100,
      machineSilenceThresholdMs: 300,
      detectionTimeoutMs: 5e3,
      suppressCompatibilityWarning: true
    });
    const promise = amd.execute();
    speechStart(amd);
    speechEnd(amd, 0);
    await new Promise((resolve) => setTimeout(resolve, 40));
    pushTranscript(amd, "hello there");
    const result = await promise;
    (0, import_vitest.expect)(result.category).toBe(import_amd.AMDCategory.HUMAN);
    (0, import_vitest.expect)(result.reason).toBe("llm-verified");
    (0, import_vitest.expect)(result.transcript).toBe("hello there");
  }, 5e3);
  (0, import_vitest.it)("should still fire short_greeting when no transcript arrives", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "unused" }));
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      humanSilenceThresholdMs: 100,
      machineSilenceThresholdMs: 300,
      detectionTimeoutMs: 5e3,
      suppressCompatibilityWarning: true
    });
    const promise = amd.execute();
    speechStart(amd);
    speechEnd(amd, 0);
    const result = await promise;
    (0, import_vitest.expect)(result.category).toBe(import_amd.AMDCategory.HUMAN);
    (0, import_vitest.expect)(result.reason).toBe("short_greeting");
  }, 5e3);
  (0, import_vitest.it)("should expose speechDurationMs and delayMs in the result", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "live" }));
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      humanSilenceThresholdMs: 50,
      machineSilenceThresholdMs: 200,
      detectionTimeoutMs: 5e3,
      suppressCompatibilityWarning: true
    });
    const promise = amd.execute();
    speechStart(amd);
    await new Promise((resolve) => setTimeout(resolve, 80));
    speechEnd(amd, 0);
    const result = await promise;
    (0, import_vitest.expect)(result.speechDurationMs).toBeGreaterThanOrEqual(70);
    (0, import_vitest.expect)(result.delayMs).toBeGreaterThanOrEqual(0);
  }, 5e3);
  (0, import_vitest.it)("should register and clear session._amd via _setAmd", async () => {
    const setAmd = import_vitest.vi.fn();
    const session = Object.assign(new MockSession(), { _setAmd: setAmd });
    const llm = new StaticLLM(JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "live" }));
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), { llm });
    (0, import_vitest.expect)(setAmd).toHaveBeenCalledWith(amd);
    setAmd.mockClear();
    await amd.aclose();
    (0, import_vitest.expect)(setAmd).toHaveBeenCalledWith(null);
  });
  (0, import_vitest.it)("should fall back to session.llm when no cloud creds are available", async () => {
    import_vitest.vi.stubEnv("LIVEKIT_URL", "");
    try {
      const session = new MockSession();
      const llm = new StaticLLM(JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "session" }));
      llm.on("error", () => {
      });
      session.llm = llm;
      const amd = new import_amd.AMD(asAgentSession(session), { detectionTimeoutMs: 50 });
      const promise = amd.execute();
      pushTranscript(amd, "Hello?");
      await (0, import_vitest.expect)(promise).resolves.toMatchObject({
        category: import_amd.AMDCategory.HUMAN,
        reason: import_vitest.expect.any(String)
      });
    } finally {
      import_vitest.vi.unstubAllEnvs();
    }
  });
  (0, import_vitest.it)("should throw when no cloud creds and session has no compatible LLM", () => {
    import_vitest.vi.stubEnv("LIVEKIT_URL", "");
    try {
      const session = new MockSession();
      (0, import_vitest.expect)(() => new import_amd.AMD(asAgentSession(session))).toThrow(/no LLM available/);
    } finally {
      import_vitest.vi.unstubAllEnvs();
    }
  });
  (0, import_vitest.it)("should not close caller-owned LLM in aclose()", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "unused" }));
    llm.on("error", () => {
    });
    const acloseSpy = import_vitest.vi.spyOn(llm, "aclose");
    const amd = new import_amd.AMD(asAgentSession(session), { llm });
    await amd.aclose();
    (0, import_vitest.expect)(acloseSpy).not.toHaveBeenCalled();
  });
  (0, import_vitest.it)("should consume transcripts from a dedicated STT pump (source = amd_stt)", async () => {
    class FakeSpeechStream {
      events = [];
      resolved = false;
      pushFrame() {
      }
      flush() {
      }
      endInput() {
      }
      close() {
      }
      pushEvent(ev) {
        this.events.push(ev);
      }
      async next() {
        if (this.events.length > 0) {
          return { done: false, value: this.events.shift() };
        }
        if (this.resolved) {
          return { done: true, value: void 0 };
        }
        await new Promise((r) => setTimeout(r, 5));
        if (this.events.length > 0) {
          return { done: false, value: this.events.shift() };
        }
        this.resolved = true;
        return { done: true, value: void 0 };
      }
      [Symbol.asyncIterator]() {
        return this;
      }
    }
    class FakeSTT extends import_stt.STT {
      label = "fake-stt";
      streamInstance = new FakeSpeechStream();
      constructor() {
        super({ streaming: true, interimResults: false });
      }
      _recognize() {
        throw new Error("unused");
      }
      stream() {
        return this.streamInstance;
      }
    }
    const session = new MockSession();
    session._subscribeAudioStream = () => void 0;
    const llm = new StaticLLM(
      JSON.stringify({ category: import_amd.AMDCategory.MACHINE_VM, reason: "voicemail" })
    );
    llm.on("error", () => {
    });
    const stt = new FakeSTT();
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      stt,
      detectionTimeoutMs: 200,
      suppressCompatibilityWarning: true
    });
    const promise = amd.execute();
    pushTranscript(amd, "this should be ignored", "stt");
    const result = await promise;
    (0, import_vitest.expect)(result.reason).toBe("detection_timeout");
    (0, import_vitest.expect)(result.category).toBe(import_amd.AMDCategory.UNCERTAIN);
  }, 5e3);
  (0, import_vitest.it)("should extend silence window via postpone_termination", async () => {
    let callCount = 0;
    class PostponeLLM extends import_llm.LLM {
      label() {
        return "postpone-llm";
      }
      chat({}) {
        callCount += 1;
        const isFirst = callCount === 1;
        return {
          async *[Symbol.asyncIterator]() {
            yield {
              id: `tc-${callCount}`,
              delta: {
                role: "assistant",
                toolCalls: [
                  isFirst ? new import_chat_context.FunctionCall({
                    callId: "p1",
                    name: "postpone_termination",
                    args: JSON.stringify({ seconds: 0.05 })
                  }) : new import_chat_context.FunctionCall({
                    callId: "s1",
                    name: "save_prediction",
                    args: JSON.stringify({ label: import_amd.AMDCategory.MACHINE_IVR })
                  })
                ]
              }
            };
          }
        };
      }
    }
    const session = new MockSession();
    const llm = new PostponeLLM();
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      detectionTimeoutMs: 5e3,
      // small end-of-turn backstop so the machine verdict's eot gate opens
      // quickly without a session turn detector (mirrors python on_user_speech_ended)
      maxEndpointingDelayMs: 100,
      suppressCompatibilityWarning: true
    });
    const promise = amd.execute();
    speechStart(amd);
    speechEnd(amd, 0);
    pushTranscript(amd, "Press 1 for sales");
    const result = await promise;
    (0, import_vitest.expect)(result.category).toBe(import_amd.AMDCategory.MACHINE_IVR);
    (0, import_vitest.expect)(callCount).toBeGreaterThanOrEqual(2);
  }, 5e3);
  (0, import_vitest.it)("no_speech_timeout settles as UNCERTAIN (not a machine)", async () => {
    const session = new MockSession();
    session.llm = new StaticLLM(JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "x" }));
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm: session.llm,
      noSpeechTimeoutMs: 30,
      detectionTimeoutMs: 5e3,
      suppressCompatibilityWarning: true
    });
    const result = await amd.execute();
    (0, import_vitest.expect)(result.reason).toBe("no_speech_timeout");
    (0, import_vitest.expect)(result.category).toBe(import_amd.AMDCategory.UNCERTAIN);
    (0, import_vitest.expect)(result.isMachine).toBe(false);
    (0, import_vitest.expect)(session.interrupt).not.toHaveBeenCalled();
  });
  (0, import_vitest.it)("waitUntilFinished gates a machine verdict on end-of-turn", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(
      JSON.stringify({ category: import_amd.AMDCategory.MACHINE_VM, reason: "voicemail greeting" })
    );
    llm.on("error", () => {
    });
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      waitUntilFinished: true,
      // long backstop so the only fast path to eot is the explicit turn-detector signal
      maxEndpointingDelayMs: 5e3,
      detectionTimeoutMs: 5e3,
      machineSilenceThresholdMs: 20,
      suppressCompatibilityWarning: true
    });
    const promise = amd.execute();
    speechStart(amd);
    pushTranscript(amd, "Please leave a message after the tone");
    speechEnd(amd, 0);
    await new Promise((r) => setTimeout(r, 100));
    let settled = false;
    void promise.then(() => {
      settled = true;
    });
    await new Promise((r) => setTimeout(r, 0));
    (0, import_vitest.expect)(settled).toBe(false);
    (0, import_vitest.expect)(amd.onEndOfTurn(makeEotInfo("Please leave a message after the tone"))).toBe(true);
    const result = await promise;
    (0, import_vitest.expect)(result.category).toBe(import_amd.AMDCategory.MACHINE_VM);
  }, 5e3);
  (0, import_vitest.it)("subtracts already-elapsed silence from the silence timer (onUserSpeechEnded)", async () => {
    const session = new MockSession();
    const llm = new StaticLLM(JSON.stringify({ category: import_amd.AMDCategory.HUMAN, reason: "x" }));
    const amd = new import_amd.AMD(asAgentSession(session), {
      llm,
      humanSilenceThresholdMs: 300,
      detectionTimeoutMs: 5e3,
      suppressCompatibilityWarning: true
    });
    const promise = amd.execute();
    speechStart(amd);
    await new Promise((r) => setTimeout(r, 300));
    const endedAt = Date.now();
    speechEnd(amd, 250);
    const result = await promise;
    (0, import_vitest.expect)(result.category).toBe(import_amd.AMDCategory.HUMAN);
    (0, import_vitest.expect)(result.reason).toBe("short_greeting");
    (0, import_vitest.expect)(Date.now() - endedAt).toBeLessThan(250);
  }, 5e3);
});
//# sourceMappingURL=amd.test.cjs.map