"use strict";
var import_rtc_node = require("@livekit/rtc-node");
var import_api = require("@opentelemetry/api");
var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
var import_sdk_trace_node = require("@opentelemetry/sdk-trace-node");
var import_web = require("node:stream/web");
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_telemetry = require("../telemetry/index.cjs");
var import_vad = require("../vad.cjs");
var import_agent_session = require("./agent_session.cjs");
var import_audio_recognition = require("./audio_recognition.cjs");
function setupInMemoryTracing() {
  const exporter = new import_sdk_trace_base.InMemorySpanExporter();
  const provider = new import_sdk_trace_node.NodeTracerProvider();
  provider.addSpanProcessor(new import_sdk_trace_base.SimpleSpanProcessor(exporter));
  provider.register();
  (0, import_telemetry.setTracerProvider)(provider);
  return { exporter };
}
function spanByName(spans, name) {
  return spans.find((s) => s.name === name);
}
function createFakeSession(rootSpanContext = import_api.ROOT_CONTEXT) {
  return {
    _agentState: "listening",
    _roomIO: {
      linkedParticipant: { sid: "p3", identity: "charlie", kind: import_rtc_node.ParticipantKind.AGENT }
    },
    _setUserAwayTimer: import_vitest.vi.fn(),
    _cancelUserAwayTimer: import_vitest.vi.fn(),
    _userSpeakingSpan: void 0,
    _userState: "listening",
    emit: import_vitest.vi.fn(),
    rootSpanContext
  };
}
class FakeVADStream extends Object {
  // We intentionally avoid extending the real VADStream (it is not exported as a value in JS output
  // in some bundling contexts). Instead we emulate the async iterator shape used by AudioRecognition.
  events;
  idx = 0;
  constructor(events) {
    super();
    this.events = events;
  }
  updateInputStream() {
  }
  detachInputStream() {
  }
  close() {
  }
  [Symbol.asyncIterator]() {
    return this;
  }
  async next() {
    if (this.idx >= this.events.length) {
      return { done: true, value: void 0 };
    }
    const value = this.events[this.idx++];
    return { done: false, value };
  }
}
class FakeVAD extends import_vad.VAD {
  label = "fake-vad";
  events;
  constructor(events) {
    super({ updateInterval: 1 });
    this.events = events;
  }
  stream() {
    return new FakeVADStream(this.events);
  }
}
const alwaysTrueTurnDetector = {
  model: "test-turn-detector",
  provider: "test-provider",
  supportsLanguage: async () => true,
  unlikelyThreshold: async () => void 0,
  predictEndOfTurn: async () => 1
};
(0, import_vitest.describe)("AudioRecognition user_turn span", () => {
  (0, import_log.initializeLogger)({ pretty: false, level: "silent" });
  (0, import_vitest.it)("creates user_turn and parents eou_detection under it (stt mode)", async () => {
    const { exporter } = setupInMemoryTracing();
    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(),
      onPreemptiveGeneration: import_vitest.vi.fn(),
      onEotPrediction: import_vitest.vi.fn(),
      onAgentBackchannelOpportunity: import_vitest.vi.fn(),
      retrieveChatCtx: () => import_chat_context.ChatContext.empty(),
      onEndOfTurn: import_vitest.vi.fn(async () => true)
    };
    const sttEvents = [
      { type: import_stt.SpeechEventType.START_OF_SPEECH },
      {
        type: import_stt.SpeechEventType.FINAL_TRANSCRIPT,
        alternatives: [
          {
            language: "en",
            text: "hello",
            startTime: 0,
            endTime: 0,
            confidence: 0.9
          }
        ]
      },
      { type: import_stt.SpeechEventType.END_OF_SPEECH }
    ];
    const sttNode = async () => new import_web.ReadableStream({
      start(controller) {
        for (const ev of sttEvents) controller.enqueue(ev);
        controller.close();
      }
    });
    const ar = new import_audio_recognition.AudioRecognition({
      recognitionHooks: hooks,
      stt: sttNode,
      vad: void 0,
      turnDetector: alwaysTrueTurnDetector,
      turnDetectionMode: "stt",
      minEndpointingDelay: 0,
      maxEndpointingDelay: 0,
      sttModel: "deepgram-nova2",
      sttProvider: "deepgram",
      getLinkedParticipant: () => ({ sid: "p1", identity: "bob", kind: import_rtc_node.ParticipantKind.AGENT })
    });
    await ar.start();
    await new Promise((r) => setTimeout(r, 20));
    await ar.close();
    const spans = exporter.getFinishedSpans();
    const userTurn = spanByName(spans, "user_turn");
    const eou = spanByName(spans, "eou_detection");
    (0, import_vitest.expect)(userTurn, "user_turn span missing").toBeTruthy();
    (0, import_vitest.expect)(eou, "eou_detection span missing").toBeTruthy();
    if (!userTurn || !eou) {
      throw new Error("expected user_turn and eou_detection spans");
    }
    (0, import_vitest.expect)(eou.parentSpanId).toBe(userTurn.spanContext().spanId);
    (0, import_vitest.expect)(userTurn.attributes["lk.participant_id"]).toBe("p1");
    (0, import_vitest.expect)(userTurn.attributes["lk.participant_identity"]).toBe("bob");
    (0, import_vitest.expect)(userTurn.attributes["lk.participant_kind"]).toBe("AGENT");
    (0, import_vitest.expect)(userTurn.attributes["gen_ai.request.model"]).toBe("deepgram-nova2");
    (0, import_vitest.expect)(userTurn.attributes["gen_ai.provider.name"]).toBe("deepgram");
    (0, import_vitest.expect)(userTurn.attributes["lk.user_transcript"]).toContain("hello");
    (0, import_vitest.expect)(userTurn.attributes["lk.transcript_confidence"]).toBeGreaterThan(0);
  });
  (0, import_vitest.it)("creates user_turn from VAD startTime (vad mode) and keeps same parenting", async () => {
    const { exporter } = setupInMemoryTracing();
    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(),
      onPreemptiveGeneration: import_vitest.vi.fn(),
      onEotPrediction: import_vitest.vi.fn(),
      onAgentBackchannelOpportunity: import_vitest.vi.fn(),
      retrieveChatCtx: () => import_chat_context.ChatContext.empty(),
      onEndOfTurn: import_vitest.vi.fn(async () => true)
    };
    const now = Date.now();
    const vadEvents = [
      {
        type: import_vad.VADEventType.START_OF_SPEECH,
        samplesIndex: 0,
        timestamp: now,
        speechDuration: 100,
        silenceDuration: 0,
        frames: [],
        probability: 0,
        inferenceDuration: 0,
        speaking: true,
        rawAccumulatedSilence: 0,
        rawAccumulatedSpeech: 0
      },
      {
        type: import_vad.VADEventType.END_OF_SPEECH,
        samplesIndex: 0,
        timestamp: now + 200,
        speechDuration: 100,
        silenceDuration: 100,
        frames: [],
        probability: 0,
        inferenceDuration: 0,
        speaking: false,
        rawAccumulatedSilence: 0,
        rawAccumulatedSpeech: 0
      }
    ];
    const sttEvents = [
      {
        type: import_stt.SpeechEventType.FINAL_TRANSCRIPT,
        alternatives: [
          {
            language: "en",
            text: "test",
            startTime: 0,
            endTime: 0,
            confidence: 0.8
          }
        ]
      }
    ];
    const sttNode = async () => new import_web.ReadableStream({
      start(controller) {
        for (const ev of sttEvents) controller.enqueue(ev);
        controller.close();
      }
    });
    const ar = new import_audio_recognition.AudioRecognition({
      recognitionHooks: hooks,
      stt: sttNode,
      vad: new FakeVAD(vadEvents),
      turnDetector: alwaysTrueTurnDetector,
      turnDetectionMode: "vad",
      minEndpointingDelay: 0,
      maxEndpointingDelay: 0,
      sttModel: "stt-model",
      sttProvider: "stt-provider",
      getLinkedParticipant: () => ({ sid: "p2", identity: "alice", kind: import_rtc_node.ParticipantKind.AGENT })
    });
    await ar.start();
    await new Promise((r) => setTimeout(r, 20));
    await ar.close();
    const spans = exporter.getFinishedSpans();
    const userTurn = spanByName(spans, "user_turn");
    const eou = spanByName(spans, "eou_detection");
    (0, import_vitest.expect)(userTurn).toBeTruthy();
    (0, import_vitest.expect)(eou).toBeTruthy();
    if (!userTurn || !eou) {
      throw new Error("expected user_turn and eou_detection spans");
    }
    (0, import_vitest.expect)(eou.parentSpanId).toBe(userTurn.spanContext().spanId);
    (0, import_vitest.expect)(hooks.onStartOfSpeech).toHaveBeenCalled();
    (0, import_vitest.expect)(hooks.onEndOfSpeech).toHaveBeenCalled();
  });
  (0, import_vitest.it)("parents user_speaking under user_turn when an explicit speech context is provided", () => {
    const { exporter } = setupInMemoryTracing();
    const sessionSpan = import_telemetry.tracer.startSpan({ name: "agent_session", context: import_api.ROOT_CONTEXT });
    const sessionContext = import_api.trace.setSpan(import_api.ROOT_CONTEXT, sessionSpan);
    const fakeSession = createFakeSession(sessionContext);
    const userTurn = import_telemetry.tracer.startSpan({ name: "user_turn", context: sessionContext });
    const userTurnContext = import_api.trace.setSpan(sessionContext, userTurn);
    const speakingStartedAt = Date.now() - 100;
    const speakingEndedAt = Date.now();
    import_api.context.with(userTurnContext, () => {
      import_agent_session.AgentSession.prototype._updateUserState.call(fakeSession, "speaking", {
        lastSpeakingTime: speakingStartedAt,
        otelContext: import_api.context.active()
      });
      import_agent_session.AgentSession.prototype._updateUserState.call(fakeSession, "listening", {
        lastSpeakingTime: speakingEndedAt,
        otelContext: import_api.context.active()
      });
    });
    userTurn.end();
    sessionSpan.end();
    const spans = exporter.getFinishedSpans();
    const userSpeaking = spanByName(spans, "user_speaking");
    const exportedUserTurn = spanByName(spans, "user_turn");
    (0, import_vitest.expect)(userSpeaking).toBeTruthy();
    (0, import_vitest.expect)(exportedUserTurn).toBeTruthy();
    if (!userSpeaking || !exportedUserTurn) {
      throw new Error("expected user_speaking and user_turn spans");
    }
    (0, import_vitest.expect)(userSpeaking.parentSpanId).toBe(exportedUserTurn.spanContext().spanId);
    (0, import_vitest.expect)(userSpeaking.attributes["lk.participant_id"]).toBe("p3");
  });
  (0, import_vitest.it)("keeps user_speaking attached to the session root without an explicit speech context", () => {
    const { exporter } = setupInMemoryTracing();
    const sessionSpan = import_telemetry.tracer.startSpan({ name: "agent_session", context: import_api.ROOT_CONTEXT });
    const sessionContext = import_api.trace.setSpan(import_api.ROOT_CONTEXT, sessionSpan);
    const fakeSession = createFakeSession(sessionContext);
    import_agent_session.AgentSession.prototype._updateUserState.call(fakeSession, "speaking", {
      lastSpeakingTime: Date.now() - 100
    });
    import_agent_session.AgentSession.prototype._updateUserState.call(fakeSession, "listening", {
      lastSpeakingTime: Date.now()
    });
    sessionSpan.end();
    const spans = exporter.getFinishedSpans();
    const userSpeaking = spanByName(spans, "user_speaking");
    (0, import_vitest.expect)(userSpeaking).toBeTruthy();
    if (!userSpeaking) {
      throw new Error("expected user_speaking span");
    }
    (0, import_vitest.expect)(userSpeaking.parentSpanId).toBe(sessionSpan.spanContext().spanId);
  });
});
//# sourceMappingURL=audio_recognition_span.test.cjs.map