/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ do_get_profile(); const { ChatConversation, MESSAGE_ROLE, ChatMessage } = ChromeUtils.importESModule( "moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs" ); const { MEMORIES_FLAG_SOURCE, SYSTEM_PROMPT_TYPE } = ChromeUtils.importESModule( "moz-src:///browser/components/aiwindow/ui/modules/ChatEnums.sys.mjs" ); const { UserRoleOpts, AssistantRoleOpts, ToolRoleOpts } = ChromeUtils.importESModule( "moz-src:///browser/components/aiwindow/ui/modules/ChatMessage.sys.mjs" ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { sinon: "resource://testing-common/Sinon.sys.mjs", }); add_task(function test_ChatConversation_constructor_defaults() { const conversation = new ChatConversation({}); Assert.withSoftAssertions(function (soft) { soft.equal(conversation.id.length, 12); soft.ok(Array.isArray(conversation.messages)); soft.ok(!isNaN(conversation.createdDate)); soft.ok(!isNaN(conversation.updatedDate)); soft.strictEqual(conversation.title, undefined); soft.strictEqual(conversation.description, undefined); soft.strictEqual(conversation.pageUrl, undefined); soft.strictEqual(conversation.pageMeta, undefined); }); }); add_task(function test_ChatConversation_addMessage() { const conversation = new ChatConversation({}); const content = { type: "text", content: "hello world", }; conversation.addMessage( MESSAGE_ROLE.USER, content, new URL("https://www.mozilla.com"), 0 ); const message = conversation.messages[0]; Assert.withSoftAssertions(function (soft) { soft.strictEqual(message.role, MESSAGE_ROLE.USER); soft.strictEqual(message.content, content); soft.strictEqual(message.pageUrl.href, "https://www.mozilla.com/"); soft.strictEqual(message.turnIndex, 0); }); }); add_task(function test_invalidRole_ChatConversation_addMessage() { const conversation = new ChatConversation({}); const content = { type: "text", content: "hello world", }; conversation.addMessage(313, content, new URL("https://www.mozilla.com"), 0); Assert.equal(conversation.messages.length, 0); }); add_task(function test_negativeTurnIndex_ChatConversation_addMessage() { const conversation = new ChatConversation({}); const content = { type: "text", content: "hello world", }; conversation.addMessage( MESSAGE_ROLE.USER, content, new URL("https://www.mozilla.com"), -1 ); const message = conversation.messages[0]; Assert.equal(message.turnIndex, 0); }); add_task(function test_parentMessageId_ChatConversation_addMessage() { const conversation = new ChatConversation({}); const content = { type: "text", content: "hello world", }; conversation.addMessage( MESSAGE_ROLE.USER, content, new URL("https://www.mozilla.com"), 0 ); conversation.addMessage( MESSAGE_ROLE.ASSISTANT, content, new URL("https://www.mozilla.com"), 0 ); const userMsg = conversation.messages[0]; const assistantMsg = conversation.messages[1]; Assert.equal(assistantMsg.parentMessageId, userMsg.id); }); add_task(function test_ordinal_ChatConversation_addMessage() { const conversation = new ChatConversation({}); const content = { type: "text", content: "hello world", }; conversation.addMessage( MESSAGE_ROLE.USER, content, new URL("https://www.mozilla.com"), 0 ); conversation.addMessage( MESSAGE_ROLE.ASSISTANT, content, new URL("https://www.mozilla.com"), 0 ); const userMsg = conversation.messages[0]; const assistantMsg = conversation.messages[1]; Assert.withSoftAssertions(function (soft) { soft.equal(userMsg.ordinal, 1); soft.equal(assistantMsg.ordinal, 2); }); }); add_task(function test_ChatConversation_addUserMessage() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; conversation.addUserMessage(content, new URL("https://www.mozilla.com")); const message = conversation.messages[0]; Assert.withSoftAssertions(function (soft) { soft.equal(message.role, MESSAGE_ROLE.USER); soft.equal(message.turnIndex, 1); soft.deepEqual(message.pageUrl, new URL("https://www.mozilla.com")); soft.deepEqual(message.content, { type: "text", body: "user to assistant msg", }); }); }); add_task(function test_revisionRootMessageId_ChatConversation_addUserMessage() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; conversation.addUserMessage(content, "https://www.firefox.com"); const message = conversation.messages[0]; Assert.equal(message.revisionRootMessageId, message.id); }); add_task(function test_opts_ChatConversation_addUserMessage() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; conversation.addUserMessage( content, "https://www.firefox.com", new UserRoleOpts({ revisionRootMessageId: "321" }) ); const message = conversation.messages[0]; Assert.equal(message.revisionRootMessageId, "321"); }); add_task(function test_ChatConversation_addAssistantMessage() { const conversation = new ChatConversation({}); const content = "response from assistant"; conversation.addAssistantMessage("text", content); const message = conversation.messages[0]; Assert.withSoftAssertions(function (soft) { soft.equal(message.role, MESSAGE_ROLE.ASSISTANT); soft.equal(message.turnIndex, 0); soft.deepEqual(message.pageUrl, null); soft.deepEqual(message.content, { type: "text", body: "response from assistant", }); soft.strictEqual(message.modelId, null, "modelId should default to false"); soft.strictEqual(message.params, null, "params should default to null"); soft.strictEqual(message.usage, null, "usage should default to null"); soft.strictEqual( message.memoriesEnabled, false, "memoriesEnabled should default to false" ); soft.strictEqual( message.memoriesFlagSource, null, "memoriesFlagSource should default to null" ); soft.deepEqual( message.memoriesApplied, [], "memoriesApplied should default to emtpy array" ); soft.deepEqual( message.webSearchQueries, [], "webSearchQueries should default to emtpy array" ); }); }); add_task(function test_opts_ChatConversation_addAssistantMessage() { const conversation = new ChatConversation({}); const content = "response from assistant"; const assistantOpts = new AssistantRoleOpts( "the-model-id", { some: "params for model" }, { usage: "data" }, true, 1, ["memory"], ["search"] ); conversation.addAssistantMessage("text", content, assistantOpts); const message = conversation.messages[0]; Assert.withSoftAssertions(function (soft) { soft.equal(message.role, MESSAGE_ROLE.ASSISTANT); soft.equal(message.turnIndex, 0); soft.deepEqual(message.pageUrl, null); soft.deepEqual(message.content, { type: "text", body: "response from assistant", }); soft.strictEqual( message.modelId, "the-model-id", "modelId should be 'the-model-id'" ); soft.deepEqual( message.params, { some: "params for model" }, 'params should equal { some: "params for model"}' ); soft.deepEqual( message.usage, { usage: "data" }, 'usage should equal {"usage": "data"}' ); soft.strictEqual( message.memoriesEnabled, true, "memoriesEnabled should equal true" ); soft.strictEqual( message.memoriesFlagSource, 1, "memoriesFlagSource equal 1" ); soft.deepEqual( message.memoriesApplied, ["memory"], "memoriesApplied should equal ['memory']" ); soft.deepEqual( message.webSearchQueries, ["search"], "memoriesApplied should equal ['search']" ); }); }); add_task(function test_ChatConversation_addToolCallMessage() { const conversation = new ChatConversation({}); const content = { random: "tool call specific keys", }; conversation.addToolCallMessage(content); const message = conversation.messages[0]; Assert.withSoftAssertions(function (soft) { soft.equal(message.role, MESSAGE_ROLE.TOOL); soft.equal(message.turnIndex, 0); soft.deepEqual(message.pageUrl, null); soft.deepEqual(message.content, { random: "tool call specific keys", }); soft.equal(message.modelId, null, "modelId should default to null"); }); }); add_task(function test_opts_ChatConversation_addToolCallMessage() { const conversation = new ChatConversation({}); const content = { random: "tool call specific keys", }; conversation.addToolCallMessage(content, new ToolRoleOpts("the-model-id")); const message = conversation.messages[0]; Assert.withSoftAssertions(function (soft) { soft.equal(message.role, MESSAGE_ROLE.TOOL); soft.equal(message.turnIndex, 0); soft.deepEqual(message.pageUrl, null); soft.deepEqual(message.content, { random: "tool call specific keys", }); soft.equal( message.modelId, "the-model-id", "modelId should equal the-model-id" ); }); }); add_task(function test_ChatConversation_addSystemMessage() { const conversation = new ChatConversation({}); const content = { random: "system call specific keys", }; conversation.addSystemMessage("text", content); const message = conversation.messages[0]; Assert.withSoftAssertions(function (soft) { soft.equal(message.role, MESSAGE_ROLE.SYSTEM); soft.equal(message.turnIndex, 0); soft.deepEqual(message.pageUrl, null); soft.deepEqual(message.content, { type: "text", body: { random: "system call specific keys" }, }); }); }); add_task(function test_ChatConversation_getSitesList() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; conversation.addUserMessage(content, new URL("https://www.mozilla.com")); conversation.addUserMessage(content, new URL("https://www.mozilla.com")); conversation.addUserMessage(content, new URL("https://www.firefox.com")); conversation.addUserMessage(content, new URL("https://www.cnn.com")); conversation.addUserMessage(content, new URL("https://www.espn.com")); conversation.addUserMessage(content, new URL("https://www.espn.com")); const sites = conversation.getSitesList(); Assert.deepEqual(sites, [ URL.parse("https://www.mozilla.com/"), URL.parse("https://www.firefox.com/"), URL.parse("https://www.cnn.com/"), URL.parse("https://www.espn.com/"), ]); }); add_task(function test_ChatConversation_getMostRecentPageVisited() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; conversation.addUserMessage(content, new URL("https://www.mozilla.com")); conversation.addUserMessage(content, new URL("https://www.mozilla.com")); conversation.addUserMessage(content, new URL("https://www.firefox.com")); conversation.addUserMessage(content, new URL("https://www.cnn.com")); conversation.addUserMessage(content, new URL("https://www.espn.com")); conversation.addUserMessage(content, new URL("https://www.espn.com")); const mostRecentPageVisited = conversation.getMostRecentPageVisited(); Assert.equal(mostRecentPageVisited, "https://www.espn.com/"); }); add_task(function test_noBrowsing_ChatConversation_getMostRecentPageVisited() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; conversation.addUserMessage(content, new URL("about:aiwindow")); conversation.addUserMessage(content, null); conversation.addUserMessage(content, null); const mostRecentPageVisited = conversation.getMostRecentPageVisited(); Assert.equal(mostRecentPageVisited, null); }); add_task(function test_ChatConversation_renderState() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; conversation.addUserMessage(content, "about:aiwindow"); conversation.addToolCallMessage("some content"); conversation.addAssistantMessage("text", "a response"); conversation.addUserMessage(content, "about:aiwindow"); conversation.addSystemMessage("text", "some system message"); conversation.addAssistantMessage("text", "a response"); const renderState = conversation.renderState(); Assert.deepEqual(renderState, [ conversation.messages[0], conversation.messages[2], conversation.messages[3], conversation.messages[5], ]); }); add_task(function test_ChatConversation_currentTurnIndex() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; conversation.addSystemMessage("text", "the system prompt"); conversation.addUserMessage(content, "about:aiwindow"); conversation.addAssistantMessage("text", "a response"); conversation.addUserMessage(content, "about:aiwindow"); conversation.addAssistantMessage("text", "a response"); conversation.addUserMessage(content, "about:aiwindow"); conversation.addAssistantMessage("text", "a response"); conversation.addUserMessage(content, "about:aiwindow"); conversation.addAssistantMessage("text", "a response"); conversation.addUserMessage(content, "about:aiwindow"); conversation.addAssistantMessage("text", "a response"); Assert.deepEqual(conversation.currentTurnIndex(), 4); }); add_task(function test_ChatConversation_helpersTurnIndexing() { const conversation = new ChatConversation({}); conversation.addSystemMessage("text", "the system prompt"); conversation.addUserMessage("a user's prompt", "https://www.somesite.com"); conversation.addToolCallMessage({ some: "tool call details" }); conversation.addAssistantMessage("text", "the llm response"); conversation.addUserMessage( "a user's second prompt", "https://www.somesite.com" ); conversation.addToolCallMessage({ some: "more tool call details" }); conversation.addAssistantMessage("text", "the second llm response"); Assert.withSoftAssertions(function (soft) { soft.equal(conversation.messages.length, 7); soft.equal(conversation.messages[0].turnIndex, 0); soft.equal(conversation.messages[1].turnIndex, 0); soft.equal(conversation.messages[2].turnIndex, 0); soft.equal(conversation.messages[3].turnIndex, 0); soft.equal(conversation.messages[4].turnIndex, 1); soft.equal(conversation.messages[5].turnIndex, 1); soft.equal(conversation.messages[6].turnIndex, 1); }); }); add_task(function test_ChatConversation_getMessagesInOpenAiFormat() { const conversation = new ChatConversation({}); conversation.addSystemMessage("text", "the system prompt"); conversation.addUserMessage("a user's prompt", "https://www.somesite.com"); conversation.addToolCallMessage({ tool_call_id: "123", name: "tool_1", body: [1, 2, 3], }); conversation.addAssistantMessage("text", "the llm response"); conversation.addUserMessage("a user's second prompt", "some question"); conversation.addToolCallMessage({ tool_call_id: "456", name: "tool_1", body: [4, 5, 6], }); conversation.addAssistantMessage("text", "the second llm response"); const openAiFormat = conversation.getMessagesInOpenAiFormat(); Assert.deepEqual(openAiFormat, [ { role: "system", content: "the system prompt" }, { role: "user", content: "a user's prompt" }, { role: "tool", content: "[1,2,3]", name: "tool_1", tool_call_id: "123" }, { role: "assistant", content: "the llm response" }, { role: "user", content: "a user's second prompt" }, { role: "tool", content: "[4,5,6]", name: "tool_1", tool_call_id: "456" }, { role: "assistant", content: "the second llm response" }, ]); }); add_task(async function test_unrelatedMessage_ChatConversation_retryMessage() { const conversation = new ChatConversation({}); conversation.addSystemMessage("text", "the system prompt"); conversation.addUserMessage("a user's prompt", "https://www.somesite.com"); const unrelatedMessage = new ChatMessage({ ordinal: 0, role: MESSAGE_ROLE.USER, content: "some content", turnIndex: 0, }); await Assert.rejects( conversation.retryMessage(unrelatedMessage), /Unrelated message/ ); }); add_task(async function test_nonUserMessage_ChatConversation_retryMessage() { const conversation = new ChatConversation({}); conversation.addSystemMessage("text", "the system prompt"); conversation.addUserMessage("a user's prompt", "https://www.somesite.com"); await Assert.rejects( conversation.retryMessage(conversation.messages[0]), /Not a user message/ ); }); add_task(async function test_withMemories_ChatConversation_retryMessage() { let sandbox = lazy.sinon.createSandbox(); const conversation = new ChatConversation({}); sandbox.stub(conversation, "getRealTimeInfo").callsFake(() => { conversation.addSystemMessage( SYSTEM_PROMPT_TYPE.REAL_TIME, "real time data" ); }); sandbox.stub(conversation, "getMemoriesContext").callsFake(() => { conversation.addSystemMessage(SYSTEM_PROMPT_TYPE.MEMORIES, "memories data"); }); conversation.addSystemMessage("text", "the system prompt"); conversation.addUserMessage("a user's prompt", "https://www.somesite.com"); conversation.addToolCallMessage({ some: "tool call details" }); conversation.addAssistantMessage("text", "the llm response"); conversation.addUserMessage("a user's second prompt", "some question"); conversation.addToolCallMessage({ some: "more tool call details" }); conversation.addAssistantMessage("text", "the second llm response"); const retryTarget = conversation.messages[1]; const retryIndexBefore = conversation.messages.findIndex( m => m.id === retryTarget.id ); if (retryIndexBefore === -1) { throw new Error("Retry target must exist in messages"); } const deleted = await conversation.retryMessage(retryTarget); Assert.withSoftAssertions(function (soft) { soft.equal( conversation.messages.length, retryIndexBefore, "Conversation should be truncated to messages before the retried user message" ); soft.ok( Array.isArray(deleted), "retryMessage should return an array of deleted messages" ); soft.equal( deleted[0].content.body, "a user's prompt", "First deleted message should be the retried user message" ); }); sandbox.restore(); }); add_task(async function test_withoutMemories_ChatConversation_retryMessage() { let sandbox = lazy.sinon.createSandbox(); const conversation = new ChatConversation({}); sandbox.stub(conversation, "getRealTimeInfo").callsFake(() => { conversation.addSystemMessage( SYSTEM_PROMPT_TYPE.REAL_TIME, "real time data" ); }); const memoriesStub = sandbox.stub(conversation, "getMemoriesContext"); conversation.addSystemMessage("text", "the system prompt"); conversation.addUserMessage("a user's prompt", "https://www.somesite.com"); conversation.addToolCallMessage({ some: "tool call details" }); conversation.addAssistantMessage("text", "the llm response"); conversation.addUserMessage("a user's second prompt", "some question"); conversation.addToolCallMessage({ some: "more tool call details" }); conversation.addAssistantMessage("text", "the second llm response"); const retryTarget = conversation.messages[1]; const retryIndexBefore = conversation.messages.findIndex( m => m.id === retryTarget.id ); if (retryIndexBefore === -1) { throw new Error("Retry target must exist in messages"); } const deleted = await conversation.retryMessage(retryTarget); Assert.withSoftAssertions(function (soft) { soft.equal( conversation.messages.length, retryIndexBefore, "Conversation should be truncated to messages before the retried user message" ); soft.ok( Array.isArray(deleted), "retryMessage should return an array of deleted messages" ); soft.equal( deleted[0].content.body, "a user's prompt", "First deleted message should be the retried user message" ); soft.equal( memoriesStub.called, false, "getMemoriesContext should not have been called by retryMessage" ); }); sandbox.restore(); }); add_task( async function test_ChatConversation_retryMessage_returnsRemovedMessages() { let sandbox = lazy.sinon.createSandbox(); const conversation = new ChatConversation({}); sandbox.stub(conversation, "getRealTimeInfo").callsFake(() => { conversation.addSystemMessage( SYSTEM_PROMPT_TYPE.REAL_TIME, "real time data" ); }); sandbox.stub(conversation, "getMemoriesContext").callsFake(() => { conversation.addSystemMessage( SYSTEM_PROMPT_TYPE.MEMORIES, "memories data" ); }); conversation.addSystemMessage("text", "the system prompt"); conversation.addUserMessage("a user's prompt", "https://www.somesite.com"); conversation.addToolCallMessage({ some: "tool call details" }); conversation.addAssistantMessage("text", "the llm response"); conversation.addUserMessage("a user's second prompt", "some question"); conversation.addToolCallMessage({ some: "more tool call details" }); conversation.addAssistantMessage("text", "the second llm response"); const toDeleteMessages = await conversation.retryMessage( conversation.messages[1] ); Assert.withSoftAssertions(function (soft) { soft.equal(toDeleteMessages.length, 6, "Incorrect number of messages"); soft.equal(toDeleteMessages[0].content.body, "a user's prompt"); soft.equal(toDeleteMessages[1].content.some, "tool call details"); soft.equal(toDeleteMessages[2].content.body, "the llm response"); soft.equal(toDeleteMessages[3].content.body, "a user's second prompt"); soft.equal(toDeleteMessages[4].content.some, "more tool call details"); soft.equal(toDeleteMessages[5].content.body, "the second llm response"); }); sandbox.restore(); } ); add_task(async function test_returnsContent_ChatConversation_getRealTimeInfo() { console.log(Object.keys(lazy.sinon)); const constructRealTime = lazy.sinon .stub() .resolves({ content: "real time data" }); const conversation = new ChatConversation({}); await conversation.getRealTimeInfo(constructRealTime); Assert.withSoftAssertions(function (soft) { soft.equal(conversation.messages[0].role, 2); soft.deepEqual(conversation.messages[0].content, { type: "injected_real_time_info", body: "real time data", }); }); }); add_task( async function test_returnsNoContent_ChatConversation_getRealTimeInfo() { console.log(Object.keys(lazy.sinon)); const constructRealTime = lazy.sinon.stub().resolves({}); const conversation = new ChatConversation({}); await conversation.getRealTimeInfo(constructRealTime); Assert.equal(conversation.messages.length, 0); } ); add_task( async function test_returnsContent_ChatConversation_getMemoriesContext() { console.log(Object.keys(lazy.sinon)); const constructMemories = lazy.sinon .stub() .resolves({ content: "memories data" }); const conversation = new ChatConversation({}); await conversation.getMemoriesContext("hello", constructMemories); Assert.withSoftAssertions(function (soft) { soft.equal(conversation.messages[0].role, 2); soft.deepEqual(conversation.messages[0].content, { type: "injected_memories", body: "memories data", }); }); } ); add_task( async function test_returnsNoContent_ChatConversation_getMemoriesContext() { console.log(Object.keys(lazy.sinon)); const constructMemories = lazy.sinon.stub().resolves({}); const conversation = new ChatConversation({}); await conversation.getMemoriesContext("hello", constructMemories); Assert.equal(conversation.messages.length, 0); } ); add_task(function test_ChatConversation_renderState_filters_phantom_messages() { const conversation = new ChatConversation({}); conversation.addUserMessage("What's the weather?", "about:aiwindow"); conversation.addAssistantMessage("text", ""); conversation.addAssistantMessage("function", { tool_calls: [ { id: "call_1", function: { name: "run_search", arguments: '{"query":"weather"}', }, }, ], }); conversation.addAssistantMessage("text", "Here is the weather forecast."); const renderState = conversation.renderState(); Assert.equal( renderState.length, 2, "Should only contain user message and real assistant message" ); Assert.equal(renderState[0].role, MESSAGE_ROLE.USER); Assert.equal(renderState[1].role, MESSAGE_ROLE.ASSISTANT); Assert.equal(renderState[1].content.body, "Here is the weather forecast."); }); add_task(async function test_addUserMessage_sets_memories_fields() { const conversation = new ChatConversation({}); const userOpts = new UserRoleOpts({ memoriesEnabled: false, memoriesFlagSource: MEMORIES_FLAG_SOURCE.CONVERSATION, }); await conversation.addUserMessage("hello", null, userOpts); const lastUserMessage = conversation.messages .filter(m => m.role === MESSAGE_ROLE.USER) .at(-1); Assert.ok(lastUserMessage, "Last user message exists"); Assert.equal( lastUserMessage.memoriesEnabled, false, "memoriesEnabled is persisted on the user message" ); Assert.equal( lastUserMessage.memoriesFlagSource, MEMORIES_FLAG_SOURCE.CONVERSATION, "memoriesFlagSource is persisted on the user message" ); });