"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_utils = require("../utils.cjs");
var import_events = require("./events.cjs");
var import_run_context = require("./run_context.cjs");
var import_speech_handle = require("./speech_handle.cjs");
(0, import_vitest.describe)("RunContext async updates", () => {
  function buildContext() {
    const functionCall = import_chat_context.FunctionCall.create({
      callId: "call_123",
      name: "slow_lookup",
      args: '{"query":"flights"}'
    });
    const speechHandle = import_speech_handle.SpeechHandle.create();
    const session = { userData: { userId: "user_1" } };
    const ctx = new import_run_context.RunContext(session, speechHandle, functionCall);
    return { ctx, functionCall };
  }
  (0, import_vitest.it)("first update resolves dispatch and later updates enqueue deferred replies", async () => {
    const { ctx, functionCall } = buildContext();
    const firstUpdate = new import_utils.Future();
    const enqueued = [];
    ctx._attachExecutor(
      {
        toolOptions: {
          updateTemplate: "Tool {functionName} update for {callId}: {message}"
        },
        enqueueReply: async (_ctx, items) => {
          enqueued.push(items);
        }
      },
      firstUpdate
    );
    await ctx.update("started");
    (0, import_vitest.expect)(await firstUpdate.await).toBe("Tool slow_lookup update for call_123: started");
    (0, import_vitest.expect)(functionCall.extra.__livekit_agents_tool_non_blocking).toBe(true);
    (0, import_vitest.expect)(enqueued).toHaveLength(0);
    await ctx.update("halfway");
    (0, import_vitest.expect)(enqueued).toHaveLength(1);
    (0, import_vitest.expect)(enqueued[0][0].callId).toBe("call_123_update_1");
    (0, import_vitest.expect)(enqueued[0][0].name).toBe("slow_lookup");
    (0, import_vitest.expect)(enqueued[0][1].callId).toBe("call_123_update_1");
    (0, import_vitest.expect)(enqueued[0][1].output).toContain("halfway");
  });
  (0, import_vitest.it)("detached contexts record updates without enqueueing replies", async () => {
    const { ctx } = buildContext();
    await ctx.update("standalone");
    (0, import_vitest.expect)(ctx.updates).toHaveLength(1);
    (0, import_vitest.expect)(ctx.updates[0][0].callId).toBe("call_123");
    (0, import_vitest.expect)(ctx.updates[0][1].output).toContain("standalone");
  });
});
(0, import_vitest.describe)("RunContext filler", () => {
  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
  function buildContext() {
    const functionCall = import_chat_context.FunctionCall.create({
      callId: "call_123",
      name: "slow_lookup",
      args: '{"query":"flights"}'
    });
    const speechHandle = import_speech_handle.SpeechHandle.create();
    const session = new FakeFillerSession();
    const ctx = new import_run_context.RunContext(
      session,
      speechHandle,
      functionCall
    );
    return { ctx, session };
  }
  (0, import_vitest.it)("speaks filler after the session stays idle for the delay", async () => {
    const { ctx, session } = buildContext();
    await ctx.filler("Still searching.", { delay: 10 }, async () => {
      await sleep(30);
    });
    (0, import_vitest.expect)(session.sayTexts).toEqual(["Still searching."]);
  });
  (0, import_vitest.it)("cancels pending filler when the scope exits before the delay", async () => {
    const { ctx, session } = buildContext();
    await ctx.filler("Too late.", { delay: 50 }, async () => {
      await sleep(5);
    });
    (0, import_vitest.expect)(session.sayTexts).toEqual([]);
  });
  (0, import_vitest.it)("restarts the idle dwell when update takes the floor", async () => {
    const { ctx, session } = buildContext();
    await ctx.filler("Still working.", { delay: 30 }, async () => {
      await sleep(20);
      await ctx.update("halfway");
      await sleep(20);
      (0, import_vitest.expect)(session.sayTexts).toEqual([]);
      await sleep(20);
    });
    (0, import_vitest.expect)(session.sayTexts).toEqual(["Still working."]);
  });
  (0, import_vitest.it)("does not let scheduler shutdown errors replace the callback result", async () => {
    const functionCall = import_chat_context.FunctionCall.create({
      callId: "call_123",
      name: "slow_lookup",
      args: '{"query":"flights"}'
    });
    const speechHandle = import_speech_handle.SpeechHandle.create();
    const session = new FakeFillerSession({
      waitForIdleError: new Error("AgentSession is closing")
    });
    const ctx = new import_run_context.RunContext(
      session,
      speechHandle,
      functionCall
    );
    await (0, import_vitest.expect)(ctx.filler("Still searching.", async () => "lookup result")).resolves.toBe(
      "lookup result"
    );
  });
  (0, import_vitest.it)("removes the abort listener when waitForIdle wins the race", async () => {
    const { ctx, session } = buildContext();
    const activeAbortListeners = /* @__PURE__ */ new Map();
    const originalAddEventListener = AbortSignal.prototype.addEventListener;
    const originalRemoveEventListener = AbortSignal.prototype.removeEventListener;
    const addSpy = import_vitest.vi.spyOn(AbortSignal.prototype, "addEventListener").mockImplementation(function(type, callback, options) {
      if (type === "abort" && callback) {
        const listeners = activeAbortListeners.get(this) ?? /* @__PURE__ */ new Set();
        listeners.add(callback);
        activeAbortListeners.set(this, listeners);
      }
      return originalAddEventListener.call(this, type, callback, options);
    });
    const removeSpy = import_vitest.vi.spyOn(AbortSignal.prototype, "removeEventListener").mockImplementation(function(type, callback, options) {
      if (type === "abort" && callback) {
        const listeners = activeAbortListeners.get(this);
        listeners == null ? void 0 : listeners.delete(callback);
        if ((listeners == null ? void 0 : listeners.size) === 0) {
          activeAbortListeners.delete(this);
        }
      }
      return originalRemoveEventListener.call(this, type, callback, options);
    });
    try {
      await ctx.filler("Listener cleanup.", { delay: 30 }, async () => {
        await sleep(5);
        (0, import_vitest.expect)([...activeAbortListeners.values()].every((listeners) => listeners.size <= 1)).toBe(
          true
        );
      });
      (0, import_vitest.expect)(session.listenerCount(import_events.AgentSessionEventTypes.AgentStateChanged)).toBe(0);
      (0, import_vitest.expect)(session.listenerCount(import_events.AgentSessionEventTypes.UserStateChanged)).toBe(0);
    } finally {
      addSpy.mockRestore();
      removeSpy.mockRestore();
    }
  });
  (0, import_vitest.it)("does not create filler when maxSteps is zero", async () => {
    const { ctx, session } = buildContext();
    await ctx.filler("Disabled.", { delay: 0, interval: 1, maxSteps: 0 }, async () => {
      await sleep(20);
    });
    (0, import_vitest.expect)(session.sayTexts).toEqual([]);
  });
  (0, import_vitest.it)("repeats filler on an interval until maxSteps is reached", async () => {
    const { ctx, session } = buildContext();
    await ctx.filler("Still working.", { delay: 0, interval: 5, maxSteps: 3 }, async () => {
      await sleep(40);
    });
    (0, import_vitest.expect)(session.sayTexts).toEqual(["Still working.", "Still working.", "Still working."]);
  });
  (0, import_vitest.it)("invokes callable sources lazily and only advances step for created speeches", async () => {
    const { ctx, session } = buildContext();
    const steps = [];
    await ctx.filler(
      (step) => {
        steps.push(step);
        return steps.length === 1 ? null : `step ${step}`;
      },
      { delay: 0, interval: 5, maxSteps: 1 },
      async () => {
        (0, import_vitest.expect)(steps).toEqual([]);
        await sleep(25);
      }
    );
    (0, import_vitest.expect)(steps).toEqual([0, 0]);
    (0, import_vitest.expect)(session.sayTexts).toEqual(["step 0"]);
  });
  (0, import_vitest.it)("accepts SpeechHandle sources without calling session.say", async () => {
    const { ctx, session } = buildContext();
    const fillerHandle = import_speech_handle.SpeechHandle.create();
    fillerHandle._markDone();
    await ctx.filler(
      () => fillerHandle,
      { delay: 0 },
      async () => {
        await sleep(10);
      }
    );
    (0, import_vitest.expect)(session.sayTexts).toEqual([]);
  });
  (0, import_vitest.it)("honors an external abort signal before the dwell completes", async () => {
    const { ctx, session } = buildContext();
    const abortController = new AbortController();
    await ctx.filler("Cancelled.", { delay: 30, signal: abortController.signal }, async () => {
      await sleep(10);
      abortController.abort();
      await sleep(30);
    });
    (0, import_vitest.expect)(session.sayTexts).toEqual([]);
  });
  (0, import_vitest.it)("honors an already-aborted external signal", async () => {
    const { ctx, session } = buildContext();
    const abortController = new AbortController();
    abortController.abort();
    await ctx.filler(
      "Already cancelled.",
      { delay: 0, signal: abortController.signal },
      async () => {
        await sleep(10);
      }
    );
    (0, import_vitest.expect)(session.sayTexts).toEqual([]);
  });
  (0, import_vitest.it)("exits promptly while still waiting for the session to become idle", async () => {
    const functionCall = import_chat_context.FunctionCall.create({
      callId: "call_123",
      name: "slow_lookup",
      args: '{"query":"flights"}'
    });
    const speechHandle = import_speech_handle.SpeechHandle.create();
    const session = new FakeFillerSession({ waitForIdleNeverResolves: true });
    const ctx = new import_run_context.RunContext(
      session,
      speechHandle,
      functionCall
    );
    await (0, import_vitest.expect)(
      ctx.filler("Still waiting.", { delay: 0 }, async () => {
        await sleep(5);
        return "done";
      })
    ).resolves.toBe("done");
    (0, import_vitest.expect)(session.sayTexts).toEqual([]);
  });
  (0, import_vitest.it)("does not let session.say shutdown errors replace the callback result", async () => {
    const functionCall = import_chat_context.FunctionCall.create({
      callId: "call_123",
      name: "slow_lookup",
      args: '{"query":"flights"}'
    });
    const speechHandle = import_speech_handle.SpeechHandle.create();
    const session = new FakeFillerSession({
      sayError: new Error("AgentSession is closing, cannot use say()")
    });
    const ctx = new import_run_context.RunContext(
      session,
      speechHandle,
      functionCall
    );
    await (0, import_vitest.expect)(
      ctx.filler("Still searching.", { delay: 0 }, async () => {
        await sleep(10);
        return "lookup result";
      })
    ).resolves.toBe("lookup result");
  });
  (0, import_vitest.it)("requires a callback scope", async () => {
    const { ctx } = buildContext();
    const filler = ctx.filler;
    await (0, import_vitest.expect)(filler("x")).rejects.toThrow("RunContext.filler requires a callback scope");
  });
  (0, import_vitest.it)("restarts the idle dwell when agent speech or thinking starts", async () => {
    const { ctx, session } = buildContext();
    await ctx.filler("After agent state.", { delay: 30 }, async () => {
      await sleep(20);
      session.emitAgentState("speaking");
      await sleep(20);
      (0, import_vitest.expect)(session.sayTexts).toEqual([]);
      session.emitAgentState("thinking");
      await sleep(20);
      (0, import_vitest.expect)(session.sayTexts).toEqual([]);
      await sleep(20);
    });
    (0, import_vitest.expect)(session.sayTexts).toEqual(["After agent state."]);
  });
  (0, import_vitest.it)("restarts the idle dwell when the user starts speaking", async () => {
    const { ctx, session } = buildContext();
    await ctx.filler("After user state.", { delay: 30 }, async () => {
      await sleep(20);
      session.emitUserState("speaking");
      await sleep(20);
      (0, import_vitest.expect)(session.sayTexts).toEqual([]);
      await sleep(20);
    });
    (0, import_vitest.expect)(session.sayTexts).toEqual(["After user state."]);
  });
  (0, import_vitest.it)("stops repeating filler after the owning speech handle is interrupted", async () => {
    const functionCall = import_chat_context.FunctionCall.create({
      callId: "call_123",
      name: "slow_lookup",
      args: '{"query":"flights"}'
    });
    const speechHandle = import_speech_handle.SpeechHandle.create();
    const session = new FakeFillerSession();
    const ctx = new import_run_context.RunContext(
      session,
      speechHandle,
      functionCall
    );
    await ctx.filler("Repeat.", { delay: 0, interval: 5 }, async () => {
      await sleep(15);
      speechHandle.interrupt();
      const countAtInterrupt = session.sayTexts.length;
      await sleep(30);
      (0, import_vitest.expect)(session.sayTexts).toHaveLength(countAtInterrupt);
    });
  });
  (0, import_vitest.it)("validates filler timing options", async () => {
    const { ctx } = buildContext();
    await (0, import_vitest.expect)(ctx.filler("x", { delay: -1 }, async () => void 0)).rejects.toThrow(
      "delay must be non-negative"
    );
    await (0, import_vitest.expect)(ctx.filler("x", { interval: -1 }, async () => void 0)).rejects.toThrow(
      "interval must be non-negative when set"
    );
    await (0, import_vitest.expect)(ctx.filler("x", { maxSteps: -1 }, async () => void 0)).rejects.toThrow(
      "maxSteps must be non-negative when set"
    );
  });
});
class FakeFillerSession extends import_node_events.EventEmitter {
  constructor(options = {}) {
    super();
    this.options = options;
  }
  userData = {};
  sayTexts = [];
  async waitForIdle() {
    if (this.options.waitForIdleError) {
      throw this.options.waitForIdleError;
    }
    if (this.options.waitForIdleNeverResolves) {
      await new Promise(() => void 0);
    }
    return;
  }
  say(text) {
    if (this.options.sayError) {
      throw this.options.sayError;
    }
    this.sayTexts.push(text);
    const handle = import_speech_handle.SpeechHandle.create();
    handle._markDone();
    return handle;
  }
  emitAgentState(newState) {
    this.emit(import_events.AgentSessionEventTypes.AgentStateChanged, {
      type: "agent_state_changed",
      oldState: "idle",
      newState,
      createdAt: Date.now()
    });
  }
  emitUserState(newState) {
    this.emit(import_events.AgentSessionEventTypes.UserStateChanged, {
      type: "user_state_changed",
      oldState: "listening",
      newState,
      createdAt: Date.now()
    });
  }
}
//# sourceMappingURL=run_context.test.cjs.map