import { describe, expect, it } from "vitest";
import { initializeLogger } from "../log.js";
import { INSTRUCTIONS_MESSAGE_ID, applyInstructionsModality } from "../voice/generation.js";
import { FakeLLM } from "../voice/testing/fake_llm.js";
import {
  ChatContext,
  ChatMessage,
  FunctionCall,
  FunctionCallOutput,
  Instructions,
  ReadonlyChatContext,
  concatInstructions,
  isInstructions,
  renderInstructions
} from "./chat_context.js";
import { ProviderTool, ToolContext, tool } from "./tool_context.js";
initializeLogger({ pretty: false, level: "error" });
const summaryXml = (summary) => ["<chat_history_summary>", summary, "</chat_history_summary>"].join("\n");
class TrackingFakeLLM extends FakeLLM {
  chatCalls = 0;
  chat(...args) {
    this.chatCalls += 1;
    return super.chat(...args);
  }
}
describe("ChatContext.toJSON", () => {
  it("should match snapshot for empty context", () => {
    const context = new ChatContext();
    expect(context.toJSON()).toMatchSnapshot();
  });
  it("should match snapshot for simple conversation", () => {
    const context = new ChatContext();
    context.addMessage({
      id: "msg_system_1",
      role: "system",
      content: "You are a helpful assistant.",
      createdAt: 1e9
    });
    context.addMessage({
      id: "msg_user_1",
      role: "user",
      content: "Hello, how are you?",
      createdAt: 1000000001
    });
    context.addMessage({
      id: "msg_assistant_1",
      role: "assistant",
      content: "I'm doing well, thank you! How can I help you today?",
      createdAt: 1000000002
    });
    expect(context.toJSON()).toMatchSnapshot("simple-conversation-no-timestamps");
    expect(context.toJSON({ excludeTimestamp: false })).toMatchSnapshot(
      "simple-conversation-with-timestamps"
    );
  });
  it("should match snapshot for multimodal content", () => {
    const context = new ChatContext();
    const imageContent = {
      id: "img_test_1",
      type: "image_content",
      image: "https://example.com/test-image.jpg",
      inferenceDetail: "high",
      inferenceWidth: 1024,
      inferenceHeight: 768,
      mimeType: "image/jpeg",
      _cache: {}
    };
    const audioContent = {
      type: "audio_content",
      frame: [],
      // This won't be included in JSON
      transcript: "This is a test audio transcript"
    };
    context.addMessage({
      id: "msg_user_2",
      role: "user",
      content: [
        "Check out this image and audio:",
        imageContent,
        audioContent,
        "What do you think?"
      ],
      createdAt: 2e9
    });
    expect(context.toJSON()).toMatchSnapshot("multimodal-default-exclusions");
    expect(
      context.toJSON({
        excludeImage: false,
        excludeAudio: true
      })
    ).toMatchSnapshot("multimodal-with-images-only");
    expect(
      context.toJSON({
        excludeImage: true,
        excludeAudio: false
      })
    ).toMatchSnapshot("multimodal-with-audio-only");
    expect(
      context.toJSON({
        excludeImage: false,
        excludeAudio: false,
        excludeTimestamp: false
      })
    ).toMatchSnapshot("multimodal-full-content");
  });
  it("should match snapshot for function calls", () => {
    const context = new ChatContext();
    context.addMessage({
      id: "msg_user_3",
      role: "user",
      content: "What's the weather in Paris?",
      createdAt: 3e9
    });
    const functionCall = new FunctionCall({
      id: "func_call_1",
      callId: "call_weather_123",
      name: "get_weather",
      args: '{"location": "Paris, France", "unit": "celsius"}',
      createdAt: 3000000001
    });
    context.insert(functionCall);
    const functionOutput = new FunctionCallOutput({
      id: "func_output_1",
      callId: "call_weather_123",
      name: "get_weather",
      output: '{"temperature": 22, "condition": "partly cloudy", "humidity": 65}',
      isError: false,
      createdAt: 3000000002
    });
    context.insert(functionOutput);
    context.addMessage({
      id: "msg_assistant_2",
      role: "assistant",
      content: "The weather in Paris is currently 22\xB0C and partly cloudy with 65% humidity.",
      createdAt: 3000000003
    });
    expect(context.toJSON()).toMatchSnapshot("conversation-with-function-calls");
    expect(
      context.toJSON({
        excludeFunctionCall: true
      })
    ).toMatchSnapshot("conversation-without-function-calls");
    expect(
      context.toJSON({
        excludeTimestamp: false
      })
    ).toMatchSnapshot("conversation-with-function-calls-and-timestamps");
  });
  it("should match snapshot for edge cases", () => {
    const context = new ChatContext();
    context.addMessage({
      id: "msg_empty_1",
      role: "user",
      content: [],
      createdAt: 5e9
    });
    const silentAudio = {
      type: "audio_content",
      frame: [],
      transcript: void 0
    };
    context.addMessage({
      id: "msg_silent_audio",
      role: "user",
      content: [silentAudio],
      createdAt: 5000000001
    });
    context.addMessage({
      id: "msg_multi_text",
      role: "assistant",
      content: ["Part 1. ", "Part 2. ", "Part 3."],
      createdAt: 5000000002
    });
    const minimalCall = new FunctionCall({
      id: "func_minimal",
      callId: "minimal",
      name: "test",
      args: "{}",
      createdAt: 5000000003
    });
    context.insert(minimalCall);
    const namelessOutput = new FunctionCallOutput({
      id: "func_output_nameless",
      callId: "minimal",
      output: "OK",
      isError: false,
      createdAt: 5000000004
    });
    context.insert(namelessOutput);
    context.addMessage({
      id: "msg_special_chars",
      role: "user",
      content: `Test with special chars: 
	\r "quotes" 'apostrophes' \\backslashes\\ {braces} [brackets]`,
      createdAt: 5000000005
    });
    expect(context.toJSON()).toMatchSnapshot("edge-cases-default");
    expect(
      context.toJSON({
        excludeTimestamp: false,
        excludeAudio: false
      })
    ).toMatchSnapshot("edge-cases-with-details");
  });
  it("should match snapshot for message property variations", () => {
    const context = new ChatContext();
    context.addMessage({
      id: "custom-message-id-123",
      role: "user",
      content: "Message with custom ID",
      createdAt: 6e9
    });
    context.addMessage({
      id: "msg_interrupted",
      role: "assistant",
      content: "This response was interrupted...",
      interrupted: true,
      createdAt: 6000000001
    });
    context.addMessage({
      id: "msg_dev_2",
      role: "developer",
      content: "Developer message",
      createdAt: 6000000002
    });
    context.addMessage({
      id: "msg_system_3",
      role: "system",
      content: "System message",
      createdAt: 6000000003
    });
    const detailedImage = {
      id: "img_detailed",
      type: "image_content",
      image: "https://example.com/image.jpg",
      inferenceDetail: "low",
      inferenceWidth: 512,
      inferenceHeight: 512,
      mimeType: "image/png",
      _cache: { cached: true }
    };
    context.addMessage({
      id: "msg_with_image",
      role: "user",
      content: ["Image with all properties:", detailedImage],
      createdAt: 6000000004
    });
    expect(context.toJSON()).toMatchSnapshot("message-properties-default");
    expect(
      context.toJSON({
        excludeImage: false,
        excludeTimestamp: false
      })
    ).toMatchSnapshot("message-properties-full");
  });
});
describe("ChatContext._summarize", () => {
  it("includes function calls in the summarization source and keeps chronological order", async () => {
    const ctx = new ChatContext();
    ctx.addMessage({ role: "system", content: "System prompt", createdAt: 0 });
    ctx.addMessage({ role: "user", content: "hello", createdAt: 1e3 });
    ctx.addMessage({ role: "assistant", content: "hi there", createdAt: 2e3 });
    ctx.insert(
      FunctionCall.create({
        callId: "call_1",
        name: "lookup",
        args: '{"order":"123"}',
        createdAt: 2500
      })
    );
    ctx.insert(
      new FunctionCallOutput({
        callId: "call_1",
        name: "lookup",
        output: '{"status":"delivered"}',
        isError: false,
        createdAt: 2600
      })
    );
    ctx.addMessage({ role: "user", content: "my color is blue", createdAt: 3e3 });
    ctx.addMessage({ role: "assistant", content: "noted", createdAt: 4e3 });
    const fake = new FakeLLM([
      {
        input: [
          "Conversation to summarize:",
          "",
          "<user>",
          "hello",
          "</user>",
          "<assistant>",
          "hi there",
          "</assistant>",
          '<function_call name="lookup" call_id="call_1">',
          '{"order":"123"}',
          "</function_call>",
          '<function_call_output name="lookup" call_id="call_1">',
          '{"status":"delivered"}',
          "</function_call_output>"
        ].join("\n"),
        content: "condensed head"
      }
    ]);
    await ctx._summarize(fake, { keepLastTurns: 1 });
    const summary = ctx.items.find(
      (item) => {
        var _a;
        return item.type === "message" && item.role === "assistant" && ((_a = item.extra) == null ? void 0 : _a.is_summary) === true;
      }
    );
    expect(summary).toBeDefined();
    if (!summary || summary.type !== "message") {
      throw new Error("summary message is missing");
    }
    expect(summary.textContent).toBe(summaryXml("condensed head"));
    expect(summary.createdAt).toBeCloseTo(2999.999999, 6);
    expect(ctx.items.filter((item) => item.type === "function_call")).toHaveLength(0);
    expect(ctx.items.filter((item) => item.type === "function_call_output")).toHaveLength(0);
    const createdAts = ctx.items.map((item) => item.createdAt);
    const sorted = [...createdAts].sort((a, b) => a - b);
    expect(createdAts).toEqual(sorted);
  });
  it("preserves interleaved tool items that belong to the recent tail", async () => {
    const ctx = new ChatContext();
    ctx.addMessage({ role: "system", content: "System prompt", createdAt: 0 });
    ctx.addMessage({ role: "user", content: "my earbuds are broken", createdAt: 1e3 });
    ctx.addMessage({
      role: "assistant",
      content: "Can you share your order number?",
      createdAt: 2e3
    });
    ctx.addMessage({ role: "user", content: "Order #123", createdAt: 3e3 });
    ctx.insert(
      FunctionCall.create({
        callId: "call_2",
        name: "lookup_order",
        args: '{"order":"123"}',
        createdAt: 3500
      })
    );
    ctx.insert(
      new FunctionCallOutput({
        callId: "call_2",
        name: "lookup_order",
        output: '{"status":"delivered"}',
        isError: false,
        createdAt: 3600
      })
    );
    ctx.addMessage({
      role: "assistant",
      content: "Found your order. Let me check the warranty.",
      createdAt: 4e3
    });
    ctx.addMessage({ role: "user", content: "Thanks.", createdAt: 5e3 });
    ctx.addMessage({ role: "assistant", content: "You are under warranty.", createdAt: 6e3 });
    const fake = new FakeLLM([
      {
        input: [
          "Conversation to summarize:",
          "",
          "<user>",
          "my earbuds are broken",
          "</user>",
          "<assistant>",
          "Can you share your order number?",
          "</assistant>"
        ].join("\n"),
        content: "older summary"
      }
    ]);
    await ctx._summarize(fake, { keepLastTurns: 2 });
    const functionItems = ctx.items.filter(
      (item) => item.type === "function_call" || item.type === "function_call_output"
    );
    expect(functionItems).toHaveLength(2);
    expect(functionItems.map((item) => item.createdAt)).toEqual([3500, 3600]);
    const rawTailMessages = ctx.items.filter(
      (item) => {
        var _a;
        return item.type === "message" && (item.role === "user" || item.role === "assistant") && ((_a = item.extra) == null ? void 0 : _a.is_summary) !== true;
      }
    );
    expect(rawTailMessages).toHaveLength(4);
    expect(rawTailMessages.map((item) => item.textContent)).toEqual([
      "Order #123",
      "Found your order. Let me check the warranty.",
      "Thanks.",
      "You are under warranty."
    ]);
    const createdAts = ctx.items.map((item) => item.createdAt);
    const sorted = [...createdAts].sort((a, b) => a - b);
    expect(createdAts).toEqual(sorted);
  });
  it("skips summarization when the recent-turn budget already covers the history", async () => {
    const ctx = new ChatContext();
    ctx.addMessage({ role: "system", content: "System prompt", createdAt: 0 });
    ctx.addMessage({ role: "user", content: "hello", createdAt: 1e3 });
    ctx.addMessage({ role: "assistant", content: "hi there", createdAt: 2e3 });
    const llm = new TrackingFakeLLM();
    const originalIds = ctx.items.map((item) => item.id);
    const result = await ctx._summarize(llm, { keepLastTurns: 1 });
    expect(result).toBe(ctx);
    expect(llm.chatCalls).toBe(0);
    expect(ctx.items.map((item) => item.id)).toEqual(originalIds);
  });
});
describe("ReadonlyChatContext with immutable array", () => {
  it("should have readonly property set to true", () => {
    const items = [
      new ChatMessage({
        id: "msg_1",
        role: "user",
        content: ["Test"],
        interrupted: false,
        createdAt: Date.now()
      })
    ];
    const readonlyContext = new ReadonlyChatContext(items);
    expect(readonlyContext.readonly).toBe(true);
  });
  it("should prevent setting items property", () => {
    const items = [
      new ChatMessage({
        id: "msg_1",
        role: "user",
        content: ["Test"],
        interrupted: false,
        createdAt: Date.now()
      })
    ];
    const readonlyContext = new ReadonlyChatContext(items);
    expect(() => {
      readonlyContext.items = [];
    }).toThrow(
      `Cannot set items on a read-only chat context. Please use .copy() and agent.update_chat_ctx() to modify the chat context.`
    );
  });
  it("should prevent modifications through array methods", () => {
    const items = [
      new ChatMessage({
        id: "msg_1",
        role: "user",
        content: ["Test"],
        interrupted: false,
        createdAt: Date.now()
      })
    ];
    const readonlyContext = new ReadonlyChatContext(items);
    const newItem = new ChatMessage({
      id: "msg_2",
      role: "assistant",
      content: ["Response"],
      interrupted: false,
      createdAt: Date.now()
    });
    const mutableItems = readonlyContext.items;
    expect(() => mutableItems.push(newItem)).toThrow(
      "Cannot call push() on a read-only array. Please use .copy() and agent.update_chat_ctx() to modify the chat context."
    );
    expect(() => mutableItems.pop()).toThrow(
      "Cannot call pop() on a read-only array. Please use .copy() and agent.update_chat_ctx() to modify the chat context."
    );
    expect(() => mutableItems.shift()).toThrow(
      "Cannot call shift() on a read-only array. Please use .copy() and agent.update_chat_ctx() to modify the chat context."
    );
    expect(() => mutableItems.unshift(newItem)).toThrow(
      "Cannot call unshift() on a read-only array. Please use .copy() and agent.update_chat_ctx() to modify the chat context."
    );
    expect(() => mutableItems.splice(0, 1)).toThrow(
      "Cannot call splice() on a read-only array. Please use .copy() and agent.update_chat_ctx() to modify the chat context."
    );
    expect(() => mutableItems.sort()).toThrow(
      "Cannot call sort() on a read-only array. Please use .copy() and agent.update_chat_ctx() to modify the chat context."
    );
    expect(() => mutableItems.reverse()).toThrow(
      "Cannot call reverse() on a read-only array. Please use .copy() and agent.update_chat_ctx() to modify the chat context."
    );
    expect(() => mutableItems.fill(newItem)).toThrow(
      "Cannot call fill() on a read-only array. Please use .copy() and agent.update_chat_ctx() to modify the chat context."
    );
    expect(() => mutableItems.copyWithin(0, 1)).toThrow(
      "Cannot call copyWithin() on a read-only array. Please use .copy() and agent.update_chat_ctx() to modify the chat context."
    );
  });
  it("should prevent bracket notation assignment and deletion", () => {
    const items = [
      new ChatMessage({
        id: "msg_1",
        role: "user",
        content: ["Test"],
        interrupted: false,
        createdAt: Date.now()
      })
    ];
    const readonlyContext = new ReadonlyChatContext(items);
    const newItem = new ChatMessage({
      id: "msg_2",
      role: "assistant",
      content: ["Response"],
      interrupted: false,
      createdAt: Date.now()
    });
    expect(() => {
      readonlyContext.items[0] = newItem;
    }).toThrow(
      'Cannot assign to read-only array index "0". Please use .copy() and agent.update_chat_ctx() to modify the chat context.'
    );
    expect(() => {
      delete readonlyContext.items[0];
    }).toThrow(
      'Cannot delete read-only array index "0". Please use .copy() and agent.update_chat_ctx() to modify the chat context.'
    );
    expect(() => {
      readonlyContext.items[99] = newItem;
    }).toThrow(
      'Cannot assign to read-only array index "99". Please use .copy() and agent.update_chat_ctx() to modify the chat context.'
    );
  });
  it("should allow read operations on the immutable array", () => {
    const items = [
      new ChatMessage({
        id: "msg_1",
        role: "user",
        content: ["Test 1"],
        interrupted: false,
        createdAt: 1e3
      }),
      new ChatMessage({
        id: "msg_2",
        role: "assistant",
        content: ["Test 2"],
        interrupted: false,
        createdAt: 2e3
      })
    ];
    const readonlyContext = new ReadonlyChatContext(items);
    expect(readonlyContext.items.length).toBe(2);
    expect(readonlyContext.items[0]).toEqual(items[0]);
    expect(readonlyContext.items[1]).toEqual(items[1]);
    expect(readonlyContext.items.find((item) => item.id === "msg_2")).toEqual(items[1]);
    expect(readonlyContext.items.map((item) => item.id)).toEqual(["msg_1", "msg_2"]);
    expect(
      readonlyContext.items.filter(
        (item) => item.type === "message" && item.role === "user"
      )
    ).toHaveLength(1);
    const ids = [];
    readonlyContext.items.forEach((item) => ids.push(item.id));
    expect(ids).toEqual(["msg_1", "msg_2"]);
  });
});
describe("ChatContext.isEquivalent", () => {
  it("should return true for same reference", () => {
    const ctx = new ChatContext();
    ctx.addMessage({
      id: "msg_1",
      role: "user",
      content: "Hello"
    });
    expect(ctx.isEquivalent(ctx)).toBe(true);
  });
  it("should return true for identical empty contexts", () => {
    const ctx1 = new ChatContext();
    const ctx2 = new ChatContext();
    expect(ctx1.isEquivalent(ctx2)).toBe(true);
  });
  it("should return false for contexts with different lengths", () => {
    const ctx1 = new ChatContext();
    ctx1.addMessage({
      id: "msg_1",
      role: "user",
      content: "Hello"
    });
    const ctx2 = new ChatContext();
    ctx2.addMessage({
      id: "msg_1",
      role: "user",
      content: "Hello"
    });
    ctx2.addMessage({
      id: "msg_2",
      role: "assistant",
      content: "Hi"
    });
    expect(ctx1.isEquivalent(ctx2)).toBe(false);
  });
  it("should return false for contexts with different item IDs", () => {
    const ctx1 = new ChatContext();
    ctx1.addMessage({
      id: "msg_1",
      role: "user",
      content: "Hello"
    });
    const ctx2 = new ChatContext();
    ctx2.addMessage({
      id: "msg_2",
      role: "user",
      content: "Hello"
    });
    expect(ctx1.isEquivalent(ctx2)).toBe(false);
  });
  it("should return false for contexts with different item types", () => {
    const ctx1 = new ChatContext();
    ctx1.addMessage({
      id: "msg_1",
      role: "user",
      content: "Hello"
    });
    const ctx2 = new ChatContext();
    ctx2.insert(
      new FunctionCall({
        id: "msg_1",
        callId: "call_1",
        name: "test",
        args: "{}"
      })
    );
    expect(ctx1.isEquivalent(ctx2)).toBe(false);
  });
  describe("message comparison", () => {
    it("should return true for identical messages", () => {
      const ctx1 = new ChatContext();
      ctx1.addMessage({
        id: "msg_1",
        role: "user",
        content: "Hello",
        interrupted: false
      });
      const ctx2 = new ChatContext();
      ctx2.addMessage({
        id: "msg_1",
        role: "user",
        content: "Hello",
        interrupted: false
      });
      expect(ctx1.isEquivalent(ctx2)).toBe(true);
    });
    it("should return false for messages with different roles", () => {
      const ctx1 = new ChatContext();
      ctx1.addMessage({
        id: "msg_1",
        role: "user",
        content: "Hello"
      });
      const ctx2 = new ChatContext();
      ctx2.addMessage({
        id: "msg_1",
        role: "assistant",
        content: "Hello"
      });
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should return false for messages with different interrupted flags", () => {
      const ctx1 = new ChatContext();
      ctx1.addMessage({
        id: "msg_1",
        role: "user",
        content: "Hello",
        interrupted: false
      });
      const ctx2 = new ChatContext();
      ctx2.addMessage({
        id: "msg_1",
        role: "user",
        content: "Hello",
        interrupted: true
      });
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should return false for messages with different content", () => {
      const ctx1 = new ChatContext();
      ctx1.addMessage({
        id: "msg_1",
        role: "user",
        content: "Hello"
      });
      const ctx2 = new ChatContext();
      ctx2.addMessage({
        id: "msg_1",
        role: "user",
        content: "World"
      });
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should return true for messages with identical array content", () => {
      const ctx1 = new ChatContext();
      ctx1.addMessage({
        id: "msg_1",
        role: "user",
        content: ["Hello", "World"]
      });
      const ctx2 = new ChatContext();
      ctx2.addMessage({
        id: "msg_1",
        role: "user",
        content: ["Hello", "World"]
      });
      expect(ctx1.isEquivalent(ctx2)).toBe(true);
    });
    it("should return false for messages with different array content", () => {
      const ctx1 = new ChatContext();
      ctx1.addMessage({
        id: "msg_1",
        role: "user",
        content: ["Hello", "World"]
      });
      const ctx2 = new ChatContext();
      ctx2.addMessage({
        id: "msg_1",
        role: "user",
        content: ["Hello"]
      });
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should return true for messages with identical image content", () => {
      const imageContent = {
        id: "img_1",
        type: "image_content",
        image: "https://example.com/image.jpg",
        inferenceDetail: "high",
        inferenceWidth: 1024,
        inferenceHeight: 768,
        mimeType: "image/jpeg",
        _cache: {}
      };
      const ctx1 = new ChatContext();
      ctx1.addMessage({
        id: "msg_1",
        role: "user",
        content: ["Check this:", imageContent]
      });
      const ctx2 = new ChatContext();
      ctx2.addMessage({
        id: "msg_1",
        role: "user",
        content: ["Check this:", { ...imageContent }]
      });
      expect(ctx1.isEquivalent(ctx2)).toBe(true);
    });
    it("should return false for messages with different image content", () => {
      const imageContent1 = {
        id: "img_1",
        type: "image_content",
        image: "https://example.com/image1.jpg",
        inferenceDetail: "high",
        _cache: {}
      };
      const imageContent2 = {
        id: "img_2",
        type: "image_content",
        image: "https://example.com/image2.jpg",
        inferenceDetail: "high",
        _cache: {}
      };
      const ctx1 = new ChatContext();
      ctx1.addMessage({
        id: "msg_1",
        role: "user",
        content: ["Check this:", imageContent1]
      });
      const ctx2 = new ChatContext();
      ctx2.addMessage({
        id: "msg_1",
        role: "user",
        content: ["Check this:", imageContent2]
      });
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
  });
  describe("function call comparison", () => {
    it("should return true for identical function calls", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: '{"location": "Paris"}'
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: '{"location": "Paris"}'
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(true);
    });
    it("should return false for function calls with different names", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: "{}"
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_time",
          args: "{}"
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should return false for function calls with different call IDs", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: "{}"
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_2",
          name: "get_weather",
          args: "{}"
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should return false for function calls with different arguments", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: '{"location": "Paris"}'
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: '{"location": "London"}'
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should ignore timestamps", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: "{}",
          createdAt: 1e3
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: "{}",
          createdAt: 2e3
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(true);
    });
  });
  describe("function call output comparison", () => {
    it("should return true for identical function call outputs", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: '{"temperature": 22}',
          isError: false
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: '{"temperature": 22}',
          isError: false
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(true);
    });
    it("should return false for function call outputs with different names", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: "{}",
          isError: false
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_time",
          output: "{}",
          isError: false
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should return false for function call outputs with different call IDs", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: "{}",
          isError: false
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_2",
          name: "get_weather",
          output: "{}",
          isError: false
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should return false for function call outputs with different output values", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: '{"temperature": 22}',
          isError: false
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: '{"temperature": 25}',
          isError: false
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should return false for function call outputs with different error flags", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: "Error occurred",
          isError: false
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: "Error occurred",
          isError: true
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(false);
    });
    it("should ignore timestamps", () => {
      const ctx1 = new ChatContext();
      ctx1.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: "{}",
          isError: false,
          createdAt: 1e3
        })
      );
      const ctx2 = new ChatContext();
      ctx2.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: "{}",
          isError: false,
          createdAt: 2e3
        })
      );
      expect(ctx1.isEquivalent(ctx2)).toBe(true);
    });
  });
  describe("complex context comparison", () => {
    it("should return true for identical complex contexts", () => {
      const ctx1 = new ChatContext();
      ctx1.addMessage({
        id: "msg_1",
        role: "user",
        content: "What is the weather?"
      });
      ctx1.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: '{"location": "Paris"}'
        })
      );
      ctx1.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: '{"temperature": 22}',
          isError: false
        })
      );
      ctx1.addMessage({
        id: "msg_2",
        role: "assistant",
        content: "The weather is 22\xB0C"
      });
      const ctx2 = new ChatContext();
      ctx2.addMessage({
        id: "msg_1",
        role: "user",
        content: "What is the weather?"
      });
      ctx2.insert(
        new FunctionCall({
          id: "func_1",
          callId: "call_1",
          name: "get_weather",
          args: '{"location": "Paris"}'
        })
      );
      ctx2.insert(
        new FunctionCallOutput({
          id: "output_1",
          callId: "call_1",
          name: "get_weather",
          output: '{"temperature": 22}',
          isError: false
        })
      );
      ctx2.addMessage({
        id: "msg_2",
        role: "assistant",
        content: "The weather is 22\xB0C"
      });
      expect(ctx1.isEquivalent(ctx2)).toBe(true);
    });
  });
});
describe("Instructions", () => {
  it("constructs from an object with audio and text variants", () => {
    const instr = new Instructions({ audio: "audio variant", text: "text variant" });
    expect(instr.audio).toBe("audio variant");
    expect(instr.text).toBe("text variant");
    expect(instr.value).toBe("audio variant");
  });
  it("identifies Instructions with a type guard", () => {
    const instr = new Instructions({ audio: "audio variant", text: "text variant" });
    expect(isInstructions(instr)).toBe(true);
    expect(isInstructions("audio variant")).toBe(false);
    expect(isInstructions({ type: "instructions", audio: "audio variant" })).toBe(false);
  });
  it("tpl propagates Instructions interpolations into audio and text variants", () => {
    const instr = Instructions.tpl`persona
${new Instructions({ audio: "audio rules", text: "text rules" })}
extra`;
    expect(instr).toBeInstanceOf(Instructions);
    expect(instr.audio).toBe("persona\naudio rules\nextra");
    expect(instr.text).toBe("persona\ntext rules\nextra");
    expect(instr.value).toBe("persona\naudio rules\nextra");
    expect(instr.asModality("text").value).toBe("persona\ntext rules\nextra");
  });
  it("tpl preserves audio-only interpolation as audio-only output", () => {
    const instr = Instructions.tpl`prefix ${new Instructions({ audio: "same" })} suffix`;
    expect(instr.toJSON()).toEqual({ type: "instructions", audio: "prefix same suffix" });
    expect(instr.audio).toBe("prefix same suffix");
    expect(instr.text).toBe("prefix same suffix");
  });
  it("tpl interpolates primitive values into both variants", () => {
    const instr = Instructions.tpl`date=${"2026-05-13"} enabled=${true} count=${3}`;
    expect(instr.toJSON()).toEqual({
      type: "instructions",
      audio: "date=2026-05-13 enabled=true count=3"
    });
    expect(instr.audio).toBe("date=2026-05-13 enabled=true count=3");
    expect(instr.text).toBe("date=2026-05-13 enabled=true count=3");
    expect(instr.value).toBe("date=2026-05-13 enabled=true count=3");
  });
  it("tpl combines multiple modality-aware interpolations", () => {
    const instr = Instructions.tpl`${new Instructions({ audio: "audio A", text: "text A" })} / ${new Instructions({ audio: "audio B", text: "text B" })}`;
    expect(instr.audio).toBe("audio A / audio B");
    expect(instr.text).toBe("text A / text B");
    expect(instr.value).toBe("audio A / audio B");
  });
  it("tpl preserves the current rendered value of resolved interpolations", () => {
    const resolved = new Instructions({ audio: "audio rules", text: "text rules" }).asModality(
      "text"
    );
    const instr = Instructions.tpl`prefix ${resolved} suffix`;
    expect(instr.audio).toBe("prefix audio rules suffix");
    expect(instr.text).toBe("prefix text rules suffix");
    expect(instr.value).toBe("prefix text rules suffix");
  });
  it("tpl stringifies null and undefined values like template literals", () => {
    const instr = Instructions.tpl`null=${null} undefined=${void 0}`;
    expect(instr.toJSON()).toEqual({
      type: "instructions",
      audio: "null=null undefined=undefined"
    });
    expect(instr.audio).toBe("null=null undefined=undefined");
    expect(instr.text).toBe("null=null undefined=undefined");
  });
  it("serializes to a dict with both variants and round-trips through toJSON", () => {
    const instr = new Instructions({ audio: "audio variant", text: "text variant" });
    const ctx = new ChatContext([ChatMessage.create({ role: "system", content: [instr] })]);
    const data = ctx.toJSON();
    const items = data.items;
    const content = items[0].content[0];
    expect(content).toEqual({
      type: "instructions",
      audio: "audio variant",
      text: "text variant"
    });
  });
  it("omits the text key in toJSON when only audio variant is provided", () => {
    const instr = new Instructions({ audio: "audio only" });
    expect(instr.toJSON()).toEqual({ type: "instructions", audio: "audio only" });
  });
  it("falls back text -> audio when no text variant is provided", () => {
    const instr = new Instructions({ audio: "audio only" });
    expect(instr.audio).toBe("audio only");
    expect(instr.text).toBe("audio only");
    expect(instr.value).toBe("audio only");
  });
  it("renderInstructions returns strings and resolved Instructions values explicitly", () => {
    const instr = new Instructions({ audio: "audio instructions", text: "text instructions" });
    expect(renderInstructions("plain instructions")).toBe("plain instructions");
    expect(renderInstructions(instr)).toBe("audio instructions");
    expect(renderInstructions(instr, "audio")).toBe("audio instructions");
    expect(renderInstructions(instr, "text")).toBe("text instructions");
  });
  it("concatenates two Instructions, propagating both variants", () => {
    const a = new Instructions({ audio: "audio A", text: "text A" });
    const b = new Instructions({ audio: "audio B", text: "text B" });
    const result = a.concat(b);
    expect(result).toBeInstanceOf(Instructions);
    expect(result.audio).toBe("audio Aaudio B");
    expect(result.text).toBe("text Atext B");
  });
  it("concatenates Instructions + string, propagating both variants", () => {
    const instr = new Instructions({ audio: "audio", text: "text" });
    const result = instr.concat(" suffix");
    expect(result.audio).toBe("audio suffix");
    expect(result.text).toBe("text suffix");
  });
  it("concatInstructions handles string + Instructions (radd-style)", () => {
    const instr = new Instructions({ audio: "audio", text: "text" });
    const result = concatInstructions("prefix ", instr);
    expect(isInstructions(result)).toBe(true);
    if (!isInstructions(result)) return;
    expect(result.audio).toBe("prefix audio");
    expect(result.text).toBe("prefix text");
  });
  it("preserves text=undefined when concatenating an audio-only instructions", () => {
    const audioOnly = new Instructions({ audio: "audio only" });
    const result = audioOnly.concat(" more");
    expect(result.toJSON()).toEqual({ type: "instructions", audio: "audio only more" });
    expect(result.audio).toBe("audio only more");
    expect(result.text).toBe("audio only more");
  });
  it("when only one side has a text variant, the other contributes its audio", () => {
    const a = new Instructions({ audio: "audio A", text: "text A" });
    const b = new Instructions({ audio: "audio B" });
    const result = concatInstructions(a, " ", b);
    expect(isInstructions(result)).toBe(true);
    if (!isInstructions(result)) return;
    expect(result.audio).toBe("audio A audio B");
    expect(result.text).toBe("text A audio B");
  });
  it("asModality returns a copy with both variants preserved", () => {
    const instr = new Instructions({ audio: "audio instructions", text: "text instructions" });
    let resolved = instr.asModality("audio");
    expect(resolved.value).toBe("audio instructions");
    expect(resolved.audio).toBe("audio instructions");
    expect(resolved.text).toBe("text instructions");
    resolved = instr.asModality("text");
    expect(resolved.value).toBe("text instructions");
    expect(resolved.audio).toBe("audio instructions");
    expect(resolved.text).toBe("text instructions");
  });
  it("can switch modality after a previous resolution", () => {
    const instr = new Instructions({ audio: "audio instructions", text: "text instructions" });
    const resolvedText = instr.asModality("text");
    const resolvedAudio = resolvedText.asModality("audio");
    expect(resolvedAudio.value).toBe("audio instructions");
  });
  it("asModality on audio-only Instructions returns audio for both modalities", () => {
    const audioOnly = new Instructions({ audio: "audio only" });
    expect(audioOnly.asModality("audio").value).toBe("audio only");
    expect(audioOnly.asModality("text").value).toBe("audio only");
  });
  it("applyInstructionsModality rewrites the system message content", () => {
    const instr = new Instructions({ audio: "audio instructions", text: "text instructions" });
    const ctx = new ChatContext([
      ChatMessage.create({
        id: INSTRUCTIONS_MESSAGE_ID,
        role: "system",
        content: [instr]
      })
    ]);
    applyInstructionsModality(ctx, { modality: "audio" });
    let content = ctx.items[0].content[0];
    expect(isInstructions(content) ? content.value : "").toBe("audio instructions");
    applyInstructionsModality(ctx, { modality: "text" });
    content = ctx.items[0].content[0];
    expect(isInstructions(content) ? content.value : "").toBe("text instructions");
  });
  it("applyInstructionsModality is a no-op when content has no Instructions", () => {
    const ctx = new ChatContext([
      ChatMessage.create({
        id: INSTRUCTIONS_MESSAGE_ID,
        role: "system",
        content: ["plain string instructions"]
      })
    ]);
    const before = ctx.items[0].content[0];
    applyInstructionsModality(ctx, { modality: "text" });
    expect(ctx.items[0].content[0]).toBe(before);
  });
  it("survives copy and lets a different modality be applied to the copy", () => {
    const instr = new Instructions({ audio: "audio instructions", text: "text instructions" });
    const baseCtx = new ChatContext([
      ChatMessage.create({
        id: INSTRUCTIONS_MESSAGE_ID,
        role: "system",
        content: [instr]
      })
    ]);
    const turn1 = baseCtx.copy();
    applyInstructionsModality(turn1, { modality: "text" });
    const turn2 = turn1.copy();
    applyInstructionsModality(turn2, { modality: "audio" });
    const turn2Content = turn2.items[0].content[0];
    expect(isInstructions(turn2Content) ? turn2Content.value : "").toBe("audio instructions");
    expect(baseCtx.items[0].content[0]).toBe(instr);
  });
});
describe("ChatContext.copy with toolCtx filter", () => {
  it("drops function calls / outputs whose tool is not in the supplied ToolContext", () => {
    const known = tool({ name: "known", description: "k", execute: async () => "ok" });
    const ctx = new ChatContext([
      ChatMessage.create({ role: "user", content: ["hello"] }),
      FunctionCall.create({ callId: "c1", name: "known", args: "{}" }),
      FunctionCallOutput.create({ callId: "c1", name: "known", output: "done", isError: false }),
      FunctionCall.create({ callId: "c2", name: "removed", args: "{}" }),
      FunctionCallOutput.create({ callId: "c2", name: "removed", output: "x", isError: false })
    ]);
    const filtered = ctx.copy({ toolCtx: new ToolContext([known]) });
    const types = filtered.items.map((i) => `${i.type}:${"name" in i ? i.name : ""}`);
    expect(types).toEqual(["message:", "function_call:known", "function_call_output:known"]);
  });
  it("keeps provider-tool calls when the ToolContext holds a matching provider tool id", () => {
    class CodeRunner extends ProviderTool {
    }
    const provider = new CodeRunner({ id: "code_runner" });
    const ctx = new ChatContext([
      FunctionCall.create({ callId: "p1", name: "code_runner", args: "{}" }),
      FunctionCall.create({ callId: "p2", name: "other", args: "{}" })
    ]);
    const filtered = ctx.copy({ toolCtx: new ToolContext([provider]) });
    expect(filtered.items.map((i) => "name" in i ? i.name : "")).toEqual(["code_runner"]);
  });
});
//# sourceMappingURL=chat_context.test.js.map