tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_Chat.js (10607B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 do_get_profile();
      5 
      6 const { ChatConversation } = ChromeUtils.importESModule(
      7  "moz-src:///browser/components/aiwindow/ui/modules/ChatConversation.sys.mjs"
      8 );
      9 const { SYSTEM_PROMPT_TYPE, MESSAGE_ROLE } = ChromeUtils.importESModule(
     10  "moz-src:///browser/components/aiwindow/ui/modules/ChatConstants.sys.mjs"
     11 );
     12 const { Chat } = ChromeUtils.importESModule(
     13  "moz-src:///browser/components/aiwindow/models/Chat.sys.mjs"
     14 );
     15 const { MODEL_FEATURES, openAIEngine } = ChromeUtils.importESModule(
     16  "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs"
     17 );
     18 
     19 const { sinon } = ChromeUtils.importESModule(
     20  "resource://testing-common/Sinon.sys.mjs"
     21 );
     22 
     23 // Prefs for aiwindow
     24 const PREF_API_KEY = "browser.aiwindow.apiKey";
     25 const PREF_ENDPOINT = "browser.aiwindow.endpoint";
     26 const PREF_MODEL = "browser.aiwindow.model";
     27 
     28 // Clean prefs after all tests
     29 registerCleanupFunction(() => {
     30  for (let pref of [PREF_API_KEY, PREF_ENDPOINT, PREF_MODEL]) {
     31    if (Services.prefs.prefHasUserValue(pref)) {
     32      Services.prefs.clearUserPref(pref);
     33    }
     34  }
     35 });
     36 
     37 add_task(async function test_Chat_real_tools_are_registered() {
     38  Assert.strictEqual(
     39    typeof Chat.toolMap.get_open_tabs,
     40    "function",
     41    "get_open_tabs should be registered in toolMap"
     42  );
     43  Assert.strictEqual(
     44    typeof Chat.toolMap.search_browsing_history,
     45    "function",
     46    "search_browsing_history should be registered in toolMap"
     47  );
     48  Assert.strictEqual(
     49    typeof Chat.toolMap.get_page_content,
     50    "function",
     51    "get_page_content should be registered in toolMap"
     52  );
     53 });
     54 
     55 add_task(
     56  async function test_openAIEngine_build_with_chat_feature_and_nonexistent_model() {
     57    Services.prefs.setStringPref(PREF_API_KEY, "test-key-123");
     58    Services.prefs.setStringPref(PREF_ENDPOINT, "https://example.test/v1");
     59    Services.prefs.setStringPref(PREF_MODEL, "nonexistent-model");
     60 
     61    const sb = sinon.createSandbox();
     62    try {
     63      const fakeEngineInstance = {
     64        runWithGenerator() {
     65          throw new Error("not used");
     66        },
     67      };
     68      const stub = sb
     69        .stub(openAIEngine, "_createEngine")
     70        .resolves(fakeEngineInstance);
     71 
     72      const engine = await openAIEngine.build(MODEL_FEATURES.CHAT);
     73 
     74      Assert.ok(
     75        engine instanceof openAIEngine,
     76        "Should return openAIEngine instance"
     77      );
     78      Assert.strictEqual(
     79        engine.engineInstance,
     80        fakeEngineInstance,
     81        "Should store engine instance"
     82      );
     83      Assert.ok(stub.calledOnce, "_createEngine should be called once");
     84 
     85      const opts = stub.firstCall.args[0];
     86      Assert.equal(opts.apiKey, "test-key-123", "apiKey should come from pref");
     87      Assert.equal(
     88        opts.baseURL,
     89        "https://example.test/v1",
     90        "baseURL should come from pref"
     91      );
     92      Assert.equal(
     93        opts.modelId,
     94        "qwen3-235b-a22b-instruct-2507-maas",
     95        "modelId should fallback to default"
     96      );
     97    } finally {
     98      sb.restore();
     99    }
    100  }
    101 );
    102 
    103 add_task(async function test_Chat_fetchWithHistory_streams_and_forwards_args() {
    104  const sb = sinon.createSandbox();
    105  try {
    106    let capturedArgs = null;
    107    let capturedOptions = null;
    108 
    109    // Fake openAIEngine instance that directly has runWithGenerator method
    110    const fakeEngine = {
    111      runWithGenerator(options) {
    112        capturedArgs = options.args;
    113        capturedOptions = options;
    114        async function* gen() {
    115          yield { text: "Hello" };
    116          yield { text: " from" };
    117          yield { text: " fake engine!" };
    118          yield {}; // ignored by Chat
    119          // No toolCalls yielded, so loop will exit after first iteration
    120        }
    121        return gen();
    122      },
    123      getConfig() {
    124        return {};
    125      },
    126    };
    127 
    128    sb.stub(openAIEngine, "build").resolves(fakeEngine);
    129    // sb.stub(Chat, "_getFxAccountToken").resolves("mock_token");
    130 
    131    const conversation = new ChatConversation({
    132      title: "chat title",
    133      description: "chat desc",
    134      pageUrl: new URL("https://www.firefox.com"),
    135      pageMeta: {},
    136    });
    137    conversation.addSystemMessage(
    138      SYSTEM_PROMPT_TYPE.TEXT,
    139      "You are helpful",
    140      0
    141    );
    142    conversation.addUserMessage("Hi there", "https://www.firefox.com", 0);
    143 
    144    // Collect streamed output
    145    let acc = "";
    146    for await (const chunk of Chat.fetchWithHistory(conversation)) {
    147      if (typeof chunk === "string") {
    148        acc += chunk;
    149      }
    150    }
    151 
    152    Assert.equal(
    153      acc,
    154      "Hello from fake engine!",
    155      "Should concatenate streamed chunks"
    156    );
    157    Assert.deepEqual(
    158      [capturedArgs[0].body, capturedArgs[1].body],
    159      [conversation.messages[0].body, conversation.messages[1].body],
    160      "Should forward messages as args to runWithGenerator()"
    161    );
    162    Assert.deepEqual(
    163      capturedOptions.streamOptions.enabled,
    164      true,
    165      "Should enable streaming in runWithGenerator()"
    166    );
    167  } finally {
    168    sb.restore();
    169  }
    170 });
    171 
    172 add_task(async function test_Chat_fetchWithHistory_handles_tool_calls() {
    173  const sb = sinon.createSandbox();
    174  try {
    175    let callCount = 0;
    176    const fakeEngine = {
    177      runWithGenerator(_options) {
    178        callCount++;
    179        async function* gen() {
    180          if (callCount === 1) {
    181            // First call: yield text and tool call
    182            yield { text: "I'll help you with that. " };
    183            yield {
    184              toolCalls: [
    185                {
    186                  id: "call_123",
    187                  function: {
    188                    name: "test_tool",
    189                    arguments: JSON.stringify({ param: "value" }),
    190                  },
    191                },
    192              ],
    193            };
    194          } else {
    195            // Second call: after tool execution
    196            yield { text: "Tool executed successfully!" };
    197          }
    198        }
    199        return gen();
    200      },
    201      getConfig() {
    202        return {};
    203      },
    204    };
    205 
    206    // Mock tool function
    207    Chat.toolMap.test_tool = sb.stub().resolves("tool result");
    208 
    209    sb.stub(openAIEngine, "build").resolves(fakeEngine);
    210    // sb.stub(Chat, "_getFxAccountToken").resolves("mock_token");
    211 
    212    const conversation = new ChatConversation({
    213      title: "chat title",
    214      description: "chat desc",
    215      pageUrl: new URL("https://www.firefox.com"),
    216      pageMeta: {},
    217    });
    218    conversation.addUserMessage(
    219      "Use the test tool",
    220      "https://www.firefox.com",
    221      0
    222    );
    223 
    224    let textOutput = "";
    225    for await (const chunk of Chat.fetchWithHistory(conversation)) {
    226      if (typeof chunk === "string") {
    227        textOutput += chunk;
    228      }
    229    }
    230 
    231    const toolCalls = conversation.messages.filter(
    232      message =>
    233        message.role === MESSAGE_ROLE.ASSISTANT &&
    234        message?.content?.type === "function"
    235    );
    236 
    237    Assert.equal(
    238      textOutput,
    239      "I'll help you with that. Tool executed successfully!",
    240      "Should yield text from both model calls"
    241    );
    242    Assert.equal(toolCalls.length, 1, "Should have one tool call");
    243    Assert.ok(
    244      toolCalls[0].content.body.tool_calls[0].function.name.includes(
    245        "test_tool"
    246      ),
    247      "Tool call log should mention tool name"
    248    );
    249    Assert.ok(Chat.toolMap.test_tool.calledOnce, "Tool should be called once");
    250    Assert.deepEqual(
    251      Chat.toolMap.test_tool.firstCall.args[0],
    252      { param: "value" },
    253      "Tool should receive correct parameters"
    254    );
    255    Assert.equal(
    256      callCount,
    257      2,
    258      "Engine should be called twice (initial + after tool)"
    259    );
    260  } finally {
    261    sb.restore();
    262    delete Chat.toolMap.test_tool;
    263  }
    264 });
    265 
    266 add_task(
    267  async function test_Chat_fetchWithHistory_propagates_engine_build_error() {
    268    const sb = sinon.createSandbox();
    269    try {
    270      const err = new Error("engine build failed");
    271      sb.stub(openAIEngine, "build").rejects(err);
    272      // sb.stub(Chat, "_getFxAccountToken").resolves("mock_token");
    273 
    274      const conversation = new ChatConversation({
    275        title: "chat title",
    276        description: "chat desc",
    277        pageUrl: new URL("https://www.firefox.com"),
    278        pageMeta: {},
    279      });
    280      conversation.addUserMessage("Hi", "https://www.firefox.com", 0);
    281 
    282      const consume = async () => {
    283        for await (const _chunk of Chat.fetchWithHistory(conversation)) {
    284          void _chunk;
    285        }
    286      };
    287 
    288      await Assert.rejects(
    289        consume(),
    290        e => e === err,
    291        "Should propagate the same error thrown by openAIEngine.build"
    292      );
    293    } finally {
    294      sb.restore();
    295    }
    296  }
    297 );
    298 
    299 add_task(
    300  async function test_Chat_fetchWithHistory_handles_invalid_tool_arguments() {
    301    const sb = sinon.createSandbox();
    302    try {
    303      let callCount = 0;
    304      const fakeEngine = {
    305        runWithGenerator(_options) {
    306          callCount++;
    307          async function* gen() {
    308            if (callCount === 1) {
    309              // First call: yield text and invalid tool call
    310              yield { text: "Using tool with bad args: " };
    311              yield {
    312                toolCalls: [
    313                  {
    314                    id: "call_456",
    315                    function: {
    316                      name: "test_tool",
    317                      arguments: "invalid json {",
    318                    },
    319                  },
    320                ],
    321              };
    322            } else {
    323              // Second call: no more tool calls, should exit loop
    324              yield { text: "Done." };
    325            }
    326          }
    327          return gen();
    328        },
    329        getConfig() {
    330          return {};
    331        },
    332      };
    333 
    334      Chat.toolMap.test_tool = sb.stub().resolves("should not be called");
    335 
    336      sb.stub(openAIEngine, "build").resolves(fakeEngine);
    337      // sb.stub(Chat, "_getFxAccountToken").resolves("mock_token");
    338 
    339      const conversation = new ChatConversation({
    340        title: "chat title",
    341        description: "chat desc",
    342        pageUrl: new URL("https://www.firefox.com"),
    343        pageMeta: {},
    344      });
    345      conversation.addUserMessage(
    346        "Test bad JSON",
    347        "https://www.firefox.com",
    348        0
    349      );
    350 
    351      let textOutput = "";
    352      for await (const chunk of Chat.fetchWithHistory(conversation)) {
    353        if (typeof chunk === "string") {
    354          textOutput += chunk;
    355        }
    356      }
    357 
    358      Assert.equal(
    359        textOutput,
    360        "Using tool with bad args: Done.",
    361        "Should yield text from both calls"
    362      );
    363      Assert.ok(
    364        Chat.toolMap.test_tool.notCalled,
    365        "Tool should not be called with invalid JSON"
    366      );
    367    } finally {
    368      sb.restore();
    369      delete Chat.toolMap.test_tool;
    370    }
    371  }
    372 );