"use strict";
var import_heap_js = require("heap-js");
var import_vitest = require("vitest");
var import_chat_context = require("../llm/chat_context.cjs");
var import_llm = require("../llm/llm.cjs");
var import_utils = require("../utils.cjs");
var import_agent = require("./agent.cjs");
var import_agent_activity = require("./agent_activity.cjs");
var import_speech_handle = require("./speech_handle.cjs");
const agentMocks = import_vitest.vi.hoisted(() => ({
  getActivityTaskInfo: import_vitest.vi.fn(() => null)
}));
import_vitest.vi.mock("./agent.js", () => {
  class Agent {
  }
  class AgentTask extends Agent {
  }
  class StopResponse {
  }
  return {
    Agent,
    AgentTask,
    StopResponse,
    _getActivityTaskInfo: agentMocks.getActivityTaskInfo,
    _setActivityTaskInfo: () => {
    },
    functionCallStorage: {
      getStore: () => void 0,
      enterWith: () => {
      },
      run: (_, fn) => fn()
    },
    speechHandleStorage: {
      getStore: () => void 0,
      enterWith: () => {
      }
    }
  };
});
import_vitest.vi.mock("../version.js", () => ({ version: "0.0.0-test" }));
async function raceTimeout(promise, ms) {
  let timer;
  const timeout = new Promise((resolve) => {
    timer = setTimeout(() => resolve("timeout"), ms);
  });
  return Promise.race([promise.then(() => "resolved"), timeout]).finally(
    () => clearTimeout(timer)
  );
}
function buildMainTaskRunner() {
  const q_updated = new import_utils.Future();
  const speechQueue = new import_heap_js.Heap((a, b) => b[0] - a[0] || a[1] - b[1]);
  const speechTasks = /* @__PURE__ */ new Set();
  const fakeActivity = {
    q_updated,
    speechQueue,
    speechTasks,
    _currentSpeech: void 0,
    _schedulingPaused: false,
    _authorizationPaused: false,
    _drainBlockedTasks: [],
    _mainTask: void 0,
    logger: {
      info: () => {
      },
      debug: () => {
      },
      warn: () => {
      },
      error: () => {
      }
    }
  };
  const proto = import_agent_activity.AgentActivity.prototype;
  fakeActivity.getDrainPendingSpeechTasks = proto.getDrainPendingSpeechTasks.bind(fakeActivity);
  fakeActivity.wakeupMainTask = proto.wakeupMainTask.bind(fakeActivity);
  const mainTask = proto.mainTask;
  const pauseSchedulingTask = proto._pauseSchedulingTask;
  return {
    fakeActivity,
    mainTask: mainTask.bind(fakeActivity),
    pauseSchedulingTask: pauseSchedulingTask.bind(fakeActivity),
    speechQueue,
    speechTasks,
    q_updated
  };
}
(0, import_vitest.describe)("AgentActivity - mainTask", () => {
  (0, import_vitest.it)("should recover when speech handle is interrupted after authorization", async () => {
    const { fakeActivity, mainTask, speechQueue, q_updated } = buildMainTaskRunner();
    const handle = import_speech_handle.SpeechHandle.create({ allowInterruptions: true });
    speechQueue.push([import_speech_handle.SpeechHandle.SPEECH_PRIORITY_NORMAL, 1, handle]);
    handle._markScheduled();
    q_updated.resolve();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    await new Promise((r) => setTimeout(r, 50));
    handle.interrupt();
    await new Promise((r) => setTimeout(r, 50));
    fakeActivity._schedulingPaused = true;
    fakeActivity.q_updated = new import_utils.Future();
    fakeActivity.q_updated.resolve();
    ac.abort();
    const result = await raceTimeout(mainTaskPromise, 2e3);
    (0, import_vitest.expect)(result).toBe("resolved");
  });
  (0, import_vitest.it)("should process next queued handle after an interrupted one", async () => {
    const { fakeActivity, mainTask, speechQueue, q_updated } = buildMainTaskRunner();
    const handleA = import_speech_handle.SpeechHandle.create({ allowInterruptions: true });
    const handleB = import_speech_handle.SpeechHandle.create({ allowInterruptions: true });
    speechQueue.push([import_speech_handle.SpeechHandle.SPEECH_PRIORITY_NORMAL, 1, handleA]);
    handleA._markScheduled();
    speechQueue.push([import_speech_handle.SpeechHandle.SPEECH_PRIORITY_NORMAL, 2, handleB]);
    handleB._markScheduled();
    q_updated.resolve();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    await new Promise((r) => setTimeout(r, 50));
    handleA.interrupt();
    await new Promise((r) => setTimeout(r, 50));
    try {
      handleB._markGenerationDone();
    } catch {
    }
    await new Promise((r) => setTimeout(r, 50));
    fakeActivity._schedulingPaused = true;
    fakeActivity.q_updated = new import_utils.Future();
    fakeActivity.q_updated.resolve();
    ac.abort();
    const result = await raceTimeout(mainTaskPromise, 2e3);
    (0, import_vitest.expect)(result).toBe("resolved");
  });
  (0, import_vitest.it)("should skip handles that were interrupted before being popped", async () => {
    const { fakeActivity, mainTask, speechQueue, q_updated } = buildMainTaskRunner();
    const handle = import_speech_handle.SpeechHandle.create({ allowInterruptions: true });
    handle.interrupt();
    speechQueue.push([import_speech_handle.SpeechHandle.SPEECH_PRIORITY_NORMAL, 1, handle]);
    handle._markScheduled();
    q_updated.resolve();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    await new Promise((r) => setTimeout(r, 50));
    fakeActivity._schedulingPaused = true;
    fakeActivity.q_updated = new import_utils.Future();
    fakeActivity.q_updated.resolve();
    ac.abort();
    const result = await raceTimeout(mainTaskPromise, 2e3);
    (0, import_vitest.expect)(result).toBe("resolved");
  });
  (0, import_vitest.it)("should hold queued speech while reply authorization is paused", async () => {
    const { fakeActivity, mainTask, speechQueue, q_updated } = buildMainTaskRunner();
    const handle = import_speech_handle.SpeechHandle.create({ allowInterruptions: true });
    fakeActivity._authorizationPaused = true;
    speechQueue.push([import_speech_handle.SpeechHandle.SPEECH_PRIORITY_NORMAL, 1, handle]);
    handle._markScheduled();
    q_updated.resolve();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    await new Promise((r) => setTimeout(r, 50));
    (0, import_vitest.expect)(() => handle._markGenerationDone()).toThrow(
      "cannot use mark_generation_done: no active generation is running."
    );
    fakeActivity._authorizationPaused = false;
    fakeActivity.q_updated.resolve();
    await new Promise((r) => setTimeout(r, 50));
    (0, import_vitest.expect)(() => handle._markGenerationDone()).not.toThrow();
    fakeActivity._schedulingPaused = true;
    fakeActivity.q_updated = new import_utils.Future();
    fakeActivity.q_updated.resolve();
    ac.abort();
    const result = await raceTimeout(mainTaskPromise, 2e3);
    (0, import_vitest.expect)(result).toBe("resolved");
  });
  (0, import_vitest.it)("does not deadlock when a drain runs from inside its own speech task", async () => {
    const { fakeActivity, mainTask, pauseSchedulingTask, speechTasks } = buildMainTaskRunner();
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    fakeActivity._mainTask = {
      result: mainTaskPromise
    };
    await new Promise((resolve) => setTimeout(resolve, 50));
    const drain = import_utils.Task.from(
      async () => {
        const current = import_utils.Task.current();
        speechTasks.add(current);
        await pauseSchedulingTask([]);
      },
      void 0,
      "reentrant-drain"
    );
    try {
      const result = await raceTimeout(drain.result, 2e3);
      (0, import_vitest.expect)(result).toBe("resolved");
    } finally {
      speechTasks.clear();
      fakeActivity._schedulingPaused = true;
      fakeActivity.q_updated = new import_utils.Future();
      fakeActivity.q_updated.resolve();
      ac.abort();
      await raceTimeout(mainTaskPromise, 2e3);
    }
  });
  (0, import_vitest.it)("does not deadlock when drain is held only by interrupted speech tasks", async () => {
    const { fakeActivity, mainTask, pauseSchedulingTask, speechTasks } = buildMainTaskRunner();
    const interruptedHandle = { interrupted: true };
    const zombieTask = import_utils.Task.from(
      () => new Promise(() => {
      }),
      void 0,
      "zombie-speech-task"
    );
    speechTasks.add(zombieTask);
    import_vitest.vi.mocked(import_agent._getActivityTaskInfo).mockImplementation(
      (task) => task === zombieTask ? { speechHandle: interruptedHandle } : void 0
    );
    const ac = new AbortController();
    const mainTaskPromise = mainTask(ac.signal);
    fakeActivity._mainTask = {
      result: mainTaskPromise
    };
    await new Promise((resolve) => setTimeout(resolve, 50));
    try {
      const result = await raceTimeout(pauseSchedulingTask([]), 2e3);
      (0, import_vitest.expect)(result).toBe("resolved");
    } finally {
      import_vitest.vi.mocked(import_agent._getActivityTaskInfo).mockReturnValue(void 0);
      speechTasks.clear();
      fakeActivity._schedulingPaused = true;
      fakeActivity.q_updated = new import_utils.Future();
      fakeActivity.q_updated.resolve();
      ac.abort();
      await raceTimeout(mainTaskPromise, 2e3);
    }
  });
  (0, import_vitest.it)("does not deadlock cancelling a paused speech whose generation never finishes", async () => {
    const handle = import_speech_handle.SpeechHandle.create({ allowInterruptions: true });
    handle._authorizeGeneration();
    const fakeActivity = {
      cancelSpeechPauseTask: void 0,
      falseInterruptionTimer: void 0,
      pausedSpeech: {
        handle,
        agentState: "speaking",
        timeout: 1e3
      },
      logger: {
        debug: import_vitest.vi.fn()
      },
      agentSession: {
        sessionOptions: {
          turnHandling: {
            interruption: {
              resumeFalseInterruption: false
            }
          }
        },
        output: {
          audio: void 0
        }
      }
    };
    const cancelSpeechPause = import_agent_activity.AgentActivity.prototype.cancelSpeechPause.bind(fakeActivity);
    const result = await raceTimeout(cancelSpeechPause(), 2e3);
    (0, import_vitest.expect)(result).toBe("resolved");
    (0, import_vitest.expect)(handle.interrupted).toBe(true);
    (0, import_vitest.expect)(fakeActivity.pausedSpeech).toBeUndefined();
  });
});
(0, import_vitest.describe)("AgentActivity - speech completion", () => {
  (0, import_vitest.it)("ends audio recognition speech when pipeline completion moves session out of speaking", () => {
    const audioRecognition = {
      onEndOfAgentSpeech: import_vitest.vi.fn()
    };
    const fakeActivity = {
      speechQueue: {
        peek: () => void 0
      },
      _currentSpeech: {
        done: () => true
      },
      audioRecognition,
      agentSession: {
        agentState: "speaking",
        _updateAgentState: import_vitest.vi.fn((state) => {
          fakeActivity.agentSession.agentState = state;
        })
      }
    };
    const onPipelineReplyDone = import_agent_activity.AgentActivity.prototype.onPipelineReplyDone;
    onPipelineReplyDone.call(fakeActivity);
    (0, import_vitest.expect)(fakeActivity.agentSession._updateAgentState).toHaveBeenCalledWith("listening");
    (0, import_vitest.expect)(audioRecognition.onEndOfAgentSpeech).toHaveBeenCalledTimes(1);
  });
});
class FakePreemptiveLLM extends import_llm.LLM {
  label() {
    return "fake.LLM";
  }
  chat() {
    throw new Error("not used in these tests");
  }
}
function buildPreemptiveRunner(opts = {}) {
  const preemptiveOpts = {
    enabled: true,
    preemptiveTts: false,
    maxSpeechDuration: 1e4,
    maxRetries: 3,
    ...opts
  };
  const generateReply = import_vitest.vi.fn(
    () => ({ id: "speech_fake", _cancel: () => {
    } })
  );
  const cancelPreemptiveGeneration = import_vitest.vi.fn();
  const fakeChatCtx = new import_chat_context.ChatContext();
  const fakeActivity = {
    _preemptiveGenerationCount: 0,
    _preemptiveGeneration: void 0,
    _currentSpeech: void 0,
    schedulingPaused: false,
    llm: new FakePreemptiveLLM(),
    tools: {},
    toolChoice: null,
    agent: { chatCtx: fakeChatCtx },
    agentSession: {
      sessionOptions: {
        turnHandling: { preemptiveGeneration: preemptiveOpts }
      }
    },
    logger: {
      info: () => {
      },
      debug: () => {
      },
      warn: () => {
      },
      error: () => {
      }
    },
    generateReply,
    cancelPreemptiveGeneration
  };
  Object.setPrototypeOf(fakeActivity, import_agent_activity.AgentActivity.prototype);
  const onPreemptiveGeneration = import_agent_activity.AgentActivity.prototype.onPreemptiveGeneration;
  return {
    fakeActivity,
    preemptiveOpts,
    generateReply,
    cancelPreemptiveGeneration,
    call: (info = {}) => onPreemptiveGeneration.call(fakeActivity, {
      newTranscript: "hello world",
      transcriptConfidence: 0.95,
      startedSpeakingAt: void 0,
      ...info
    })
  };
}
(0, import_vitest.describe)("AgentActivity - onPreemptiveGeneration guards", () => {
  (0, import_vitest.it)("increments counter up to maxRetries then skips further calls within the same turn", () => {
    const { fakeActivity, generateReply, call } = buildPreemptiveRunner({ maxRetries: 2 });
    call();
    (0, import_vitest.expect)(fakeActivity._preemptiveGenerationCount).toBe(1);
    (0, import_vitest.expect)(generateReply).toHaveBeenCalledTimes(1);
    call();
    (0, import_vitest.expect)(fakeActivity._preemptiveGenerationCount).toBe(2);
    (0, import_vitest.expect)(generateReply).toHaveBeenCalledTimes(2);
    call();
    (0, import_vitest.expect)(fakeActivity._preemptiveGenerationCount).toBe(2);
    (0, import_vitest.expect)(generateReply).toHaveBeenCalledTimes(2);
    call();
    (0, import_vitest.expect)(fakeActivity._preemptiveGenerationCount).toBe(2);
    (0, import_vitest.expect)(generateReply).toHaveBeenCalledTimes(2);
  });
  (0, import_vitest.it)("resumes preemption after the counter is reset (simulates onEndOfTurn)", () => {
    const { fakeActivity, generateReply, call } = buildPreemptiveRunner({ maxRetries: 2 });
    call();
    call();
    call();
    (0, import_vitest.expect)(generateReply).toHaveBeenCalledTimes(2);
    fakeActivity._preemptiveGenerationCount = 0;
    call();
    (0, import_vitest.expect)(fakeActivity._preemptiveGenerationCount).toBe(1);
    (0, import_vitest.expect)(generateReply).toHaveBeenCalledTimes(3);
  });
  (0, import_vitest.it)("skips preemption when startedSpeakingAt exceeds maxSpeechDuration", () => {
    const { fakeActivity, generateReply, call } = buildPreemptiveRunner({
      maxSpeechDuration: 3e3
    });
    call({ startedSpeakingAt: Date.now() - 1e3 });
    (0, import_vitest.expect)(fakeActivity._preemptiveGenerationCount).toBe(1);
    (0, import_vitest.expect)(generateReply).toHaveBeenCalledTimes(1);
    call({ startedSpeakingAt: Date.now() - 5e3 });
    (0, import_vitest.expect)(fakeActivity._preemptiveGenerationCount).toBe(1);
    (0, import_vitest.expect)(generateReply).toHaveBeenCalledTimes(1);
  });
  (0, import_vitest.it)("skips preemption entirely when enabled is false", () => {
    const { fakeActivity, generateReply, cancelPreemptiveGeneration, call } = buildPreemptiveRunner(
      { enabled: false }
    );
    call();
    call();
    (0, import_vitest.expect)(fakeActivity._preemptiveGenerationCount).toBe(0);
    (0, import_vitest.expect)(generateReply).not.toHaveBeenCalled();
    (0, import_vitest.expect)(cancelPreemptiveGeneration).not.toHaveBeenCalled();
  });
});
//# sourceMappingURL=agent_activity.test.cjs.map