tor-browser

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

test_ChatUtils.js (15838B)


      1 /**
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
      5 */
      6 
      7 do_get_profile();
      8 
      9 const {
     10  constructRealTimeInfoInjectionMessage,
     11  getLocalIsoTime,
     12  getCurrentTabMetadata,
     13  constructRelevantMemoriesContextMessage,
     14  parseContentWithTokens,
     15  detectTokens,
     16 } = ChromeUtils.importESModule(
     17  "moz-src:///browser/components/aiwindow/models/ChatUtils.sys.mjs"
     18 );
     19 const { MemoriesManager } = ChromeUtils.importESModule(
     20  "moz-src:///browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs"
     21 );
     22 const { MemoryStore } = ChromeUtils.importESModule(
     23  "moz-src:///browser/components/aiwindow/services/MemoryStore.sys.mjs"
     24 );
     25 const { sinon } = ChromeUtils.importESModule(
     26  "resource://testing-common/Sinon.sys.mjs"
     27 );
     28 
     29 /**
     30 * Constants for test memories
     31 */
     32 const TEST_MEMORIES = [
     33  {
     34    memory_summary: "Loves drinking coffee",
     35    category: "Food & Drink",
     36    intent: "Plan / Organize",
     37    score: 3,
     38  },
     39  {
     40    memory_summary: "Buys dog food online",
     41    category: "Pets & Animals",
     42    intent: "Buy / Acquire",
     43    score: 4,
     44  },
     45 ];
     46 
     47 /**
     48 * Helper function bulk-add memories
     49 */
     50 async function clearAndAddMemories() {
     51  const memories = await MemoryStore.getMemories();
     52  for (const memory of memories) {
     53    await MemoryStore.hardDeleteMemory(memory.id);
     54  }
     55  for (const memory of TEST_MEMORIES) {
     56    await MemoryStore.addMemory(memory);
     57  }
     58 }
     59 
     60 /**
     61 * Constants for preference keys and test values
     62 */
     63 const PREF_API_KEY = "browser.aiwindow.apiKey";
     64 const PREF_ENDPOINT = "browser.aiwindow.endpoint";
     65 const PREF_MODEL = "browser.aiwindow.model";
     66 
     67 const API_KEY = "fake-key";
     68 const ENDPOINT = "https://api.fake-endpoint.com/v1";
     69 const MODEL = "fake-model";
     70 
     71 add_setup(async function () {
     72  // Setup prefs used across multiple tests
     73  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
     74  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
     75  Services.prefs.setStringPref(PREF_MODEL, MODEL);
     76 
     77  // Clear prefs after testing
     78  registerCleanupFunction(() => {
     79    for (let pref of [PREF_API_KEY, PREF_ENDPOINT, PREF_MODEL]) {
     80      if (Services.prefs.prefHasUserValue(pref)) {
     81        Services.prefs.clearUserPref(pref);
     82      }
     83    }
     84  });
     85 });
     86 
     87 add_task(function test_getLocalIsoTime_returns_offset_timestamp() {
     88  const sb = sinon.createSandbox();
     89  const clock = sb.useFakeTimers({ now: Date.UTC(2025, 11, 27, 14, 0, 0) });
     90  try {
     91    const iso = getLocalIsoTime();
     92    Assert.ok(
     93      typeof iso === "string" && !!iso.length,
     94      "Should return a non-empty string"
     95    );
     96    Assert.ok(
     97      /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(iso),
     98      "Should include date, time (up to seconds), without timezone offset"
     99    );
    100  } finally {
    101    clock.restore();
    102    sb.restore();
    103  }
    104 });
    105 
    106 add_task(async function test_getCurrentTabMetadata_fetch_fallback() {
    107  const sb = sinon.createSandbox();
    108  const tracker = { getTopWindow: sb.stub() };
    109  const pageData = {
    110    getCached: sb.stub(),
    111  };
    112  const fakeActor = {
    113    collectPageData: sb.stub().resolves({
    114      description: "Collected description",
    115    }),
    116  };
    117  const fakeBrowser = {
    118    currentURI: { spec: "https://example.com/article" },
    119    contentTitle: "",
    120    documentTitle: "Example Article",
    121    browsingContext: {
    122      currentWindowGlobal: {
    123        getActor: sb.stub().returns(fakeActor),
    124      },
    125    },
    126  };
    127 
    128  tracker.getTopWindow.returns({
    129    gBrowser: { selectedBrowser: fakeBrowser },
    130  });
    131  pageData.getCached.returns(null);
    132 
    133  try {
    134    const result = await getCurrentTabMetadata({
    135      BrowserWindowTracker: tracker,
    136      PageDataService: pageData,
    137    });
    138    Assert.deepEqual(result, {
    139      url: "https://example.com/article",
    140      title: "Example Article",
    141      description: "Collected description",
    142    });
    143    Assert.ok(
    144      fakeActor.collectPageData.calledOnce,
    145      "Should collect page data from actor when not cached"
    146    );
    147    Assert.ok(
    148      fakeBrowser.browsingContext.currentWindowGlobal.getActor.calledWith(
    149        "PageData"
    150      ),
    151      "Should get PageData actor"
    152    );
    153  } finally {
    154    sb.restore();
    155  }
    156 });
    157 
    158 add_task(
    159  async function test_constructRealTimeInfoInjectionMessage_with_tab_info() {
    160    const sb = sinon.createSandbox();
    161    const tracker = { getTopWindow: sb.stub() };
    162    const pageData = {
    163      getCached: sb.stub(),
    164    };
    165    const locale = Services.locale.appLocaleAsBCP47;
    166    const fakeActor = {
    167      collectPageData: sb.stub(),
    168    };
    169    const fakeBrowser = {
    170      currentURI: { spec: "https://mozilla.org" },
    171      contentTitle: "Mozilla",
    172      documentTitle: "Mozilla",
    173      browsingContext: {
    174        currentWindowGlobal: {
    175          getActor: sb.stub().returns(fakeActor),
    176        },
    177      },
    178    };
    179 
    180    tracker.getTopWindow.returns({
    181      gBrowser: { selectedBrowser: fakeBrowser },
    182    });
    183    pageData.getCached.returns({
    184      description: "Internet for people",
    185    });
    186    const clock = sb.useFakeTimers({ now: Date.UTC(2025, 11, 27, 14, 0, 0) });
    187 
    188    try {
    189      const message = await constructRealTimeInfoInjectionMessage({
    190        BrowserWindowTracker: tracker,
    191        PageDataService: pageData,
    192      });
    193      Assert.equal(message.role, "system", "Should return system role");
    194      Assert.ok(
    195        message.content.includes(`Locale: ${locale}`),
    196        "Should include locale"
    197      );
    198      Assert.ok(
    199        message.content.includes("Current active browser tab details:"),
    200        "Should include tab details heading"
    201      );
    202      Assert.ok(
    203        message.content.includes("- URL: https://mozilla.org"),
    204        "Should include tab URL"
    205      );
    206      Assert.ok(
    207        message.content.includes("- Title: Mozilla"),
    208        "Should include tab title"
    209      );
    210      Assert.ok(
    211        message.content.includes("- Description: Internet for people"),
    212        "Should include tab description"
    213      );
    214      Assert.ok(
    215        fakeActor.collectPageData.notCalled,
    216        "Should not collect page data when cached data exists"
    217      );
    218    } finally {
    219      clock.restore();
    220      sb.restore();
    221    }
    222  }
    223 );
    224 
    225 add_task(
    226  async function test_constructRealTimeInfoInjectionMessage_without_tab_info() {
    227    const sb = sinon.createSandbox();
    228    const tracker = { getTopWindow: sb.stub() };
    229    const pageData = {
    230      getCached: sb.stub(),
    231      fetchPageData: sb.stub(),
    232    };
    233    const locale = Services.locale.appLocaleAsBCP47;
    234 
    235    tracker.getTopWindow.returns(null);
    236    const clock = sb.useFakeTimers({ now: Date.UTC(2025, 11, 27, 14, 0, 0) });
    237 
    238    try {
    239      const message = await constructRealTimeInfoInjectionMessage({
    240        BrowserWindowTracker: tracker,
    241        PageDataService: pageData,
    242      });
    243      Assert.ok(
    244        message.content.includes("No active browser tab."),
    245        "Should mention missing tab info"
    246      );
    247      Assert.ok(
    248        !message.content.includes("- URL:"),
    249        "Should not include empty tab fields"
    250      );
    251      Assert.ok(
    252        message.content.includes(`Locale: ${locale}`),
    253        "Should include system locale"
    254      );
    255    } finally {
    256      clock.restore();
    257      sb.restore();
    258    }
    259  }
    260 );
    261 
    262 add_task(async function test_constructRelevantMemoriesContextMessage() {
    263  await clearAndAddMemories();
    264 
    265  const sb = sinon.createSandbox();
    266  try {
    267    const fakeEngine = {
    268      run() {
    269        return {
    270          finalOutput: `{
    271            "categories": ["Food & Drink"],
    272            "intents": ["Plan / Organize"]
    273          }`,
    274        };
    275      },
    276    };
    277 
    278    // Stub the `ensureOpenAIEngine` method in MemoriesManager
    279    const stub = sb
    280      .stub(MemoriesManager, "ensureOpenAIEngine")
    281      .returns(fakeEngine);
    282 
    283    const relevantMemoriesContextMessage =
    284      await constructRelevantMemoriesContextMessage("I love drinking coffee");
    285    Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
    286 
    287    // Check relevantMemoriesContextMessage's top level structure
    288    Assert.strictEqual(
    289      typeof relevantMemoriesContextMessage,
    290      "object",
    291      "Should return an object"
    292    );
    293    Assert.equal(
    294      Object.keys(relevantMemoriesContextMessage).length,
    295      2,
    296      "Should have 2 keys"
    297    );
    298 
    299    // Check specific fields
    300    Assert.equal(
    301      relevantMemoriesContextMessage.role,
    302      "system",
    303      "Should have role 'system'"
    304    );
    305    Assert.ok(
    306      typeof relevantMemoriesContextMessage.content === "string" &&
    307        relevantMemoriesContextMessage.content.length,
    308      "Content should be a non-empty string"
    309    );
    310 
    311    const content = relevantMemoriesContextMessage.content;
    312    Assert.ok(
    313      content.includes(
    314        "Use them to personalized your response using the following guidelines:"
    315      ),
    316      "Relevant memories context prompt should pull from the correct base"
    317    );
    318    Assert.ok(
    319      content.includes("- Loves drinking coffee"),
    320      "Content should include relevant memory"
    321    );
    322    Assert.ok(
    323      !content.includes("- Buys dog food online"),
    324      "Content should not include non-relevant memory"
    325    );
    326  } finally {
    327    sb.restore();
    328  }
    329 });
    330 
    331 add_task(
    332  async function test_constructRelevantMemoriesContextMessage_no_relevant_memories() {
    333    await clearAndAddMemories();
    334 
    335    const sb = sinon.createSandbox();
    336    try {
    337      const fakeEngine = {
    338        run() {
    339          return {
    340            finalOutput: `{
    341            "categories": ["Health & Fitness"],
    342            "intents": ["Plan / Organize"]
    343          }`,
    344          };
    345        },
    346      };
    347 
    348      // Stub the `ensureOpenAIEngine` method in MemoriesManager
    349      const stub = sb
    350        .stub(MemoriesManager, "ensureOpenAIEngine")
    351        .returns(fakeEngine);
    352 
    353      const relevantMemoriesContextMessage =
    354        await constructRelevantMemoriesContextMessage("I love drinking coffee");
    355      Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
    356 
    357      // No relevant memories, so returned value should be null
    358      Assert.equal(
    359        relevantMemoriesContextMessage,
    360        null,
    361        "Should return null when there are no relevant memories"
    362      );
    363    } finally {
    364      sb.restore();
    365    }
    366  }
    367 );
    368 
    369 add_task(async function test_parseContentWithTokens_no_tokens() {
    370  const content = "This is a regular message with no special tokens.";
    371  const result = await parseContentWithTokens(content);
    372 
    373  Assert.equal(
    374    result.cleanContent,
    375    content,
    376    "Clean content should match original when no tokens present"
    377  );
    378  Assert.equal(result.searchQueries.length, 0, "Should have no search queries");
    379  Assert.equal(result.usedMemories.length, 0, "Should have no used memories");
    380 });
    381 
    382 add_task(async function test_parseContentWithTokens_single_search_token() {
    383  const content =
    384    "You can find great coffee in the downtown area.§search: best coffee shops near me§";
    385  const result = await parseContentWithTokens(content);
    386 
    387  Assert.equal(
    388    result.cleanContent,
    389    "You can find great coffee in the downtown area.",
    390    "Should remove search token from content"
    391  );
    392  Assert.equal(result.searchQueries.length, 1, "Should have one search query");
    393  Assert.equal(
    394    result.searchQueries[0],
    395    "best coffee shops near me",
    396    "Should extract correct search query"
    397  );
    398  Assert.equal(result.usedMemories.length, 0, "Should have no used memories");
    399 });
    400 
    401 add_task(async function test_parseContentWithTokens_single_memory_token() {
    402  const content =
    403    "I recommend trying herbal tea blends.§existing_memory: likes tea§";
    404  const result = await parseContentWithTokens(content);
    405 
    406  Assert.equal(
    407    result.cleanContent,
    408    "I recommend trying herbal tea blends.",
    409    "Should remove memory token from content"
    410  );
    411  Assert.equal(result.searchQueries.length, 0, "Should have no search queries");
    412  Assert.equal(result.usedMemories.length, 1, "Should have one used memory");
    413  Assert.equal(
    414    result.usedMemories[0],
    415    "likes tea",
    416    "Should extract correct memory"
    417  );
    418 });
    419 
    420 add_task(async function test_parseContentWithTokens_multiple_mixed_tokens() {
    421  const content =
    422    "I recommend checking out organic coffee options.§existing_memory: prefers organic§ They have great flavor profiles.§search: organic coffee beans reviews§§search: best organic cafes nearby§";
    423  const result = await parseContentWithTokens(content);
    424 
    425  Assert.equal(
    426    result.cleanContent,
    427    "I recommend checking out organic coffee options. They have great flavor profiles.",
    428    "Should remove all tokens from content"
    429  );
    430  Assert.equal(
    431    result.searchQueries.length,
    432    2,
    433    "Should have two search queries"
    434  );
    435  Assert.deepEqual(
    436    result.searchQueries,
    437    ["organic coffee beans reviews", "best organic cafes nearby"],
    438    "Should extract search queries in correct order"
    439  );
    440  Assert.equal(result.usedMemories.length, 1, "Should have one used memory");
    441  Assert.equal(
    442    result.usedMemories[0],
    443    "prefers organic",
    444    "Should extract correct memory"
    445  );
    446 });
    447 
    448 add_task(async function test_parseContentWithTokens_tokens_with_whitespace() {
    449  const content =
    450    "You can find more details online.§search:   coffee brewing methods   §";
    451  const result = await parseContentWithTokens(content);
    452 
    453  Assert.equal(
    454    result.cleanContent,
    455    "You can find more details online.",
    456    "Should remove token with whitespace"
    457  );
    458  Assert.equal(result.searchQueries.length, 1, "Should have one search query");
    459  Assert.equal(
    460    result.searchQueries[0],
    461    "coffee brewing methods",
    462    "Should trim whitespace from extracted query"
    463  );
    464 });
    465 
    466 add_task(async function test_parseContentWithTokens_adjacent_tokens() {
    467  const content =
    468    "Here are some great Italian dining options.§existing_memory: prefers italian food§§search: local italian restaurants§";
    469  const result = await parseContentWithTokens(content);
    470 
    471  Assert.equal(
    472    result.cleanContent,
    473    "Here are some great Italian dining options.",
    474    "Should remove adjacent tokens"
    475  );
    476  Assert.equal(result.searchQueries.length, 1, "Should have one search query");
    477  Assert.equal(
    478    result.searchQueries[0],
    479    "local italian restaurants",
    480    "Should extract search query"
    481  );
    482  Assert.equal(result.usedMemories.length, 1, "Should have one memory");
    483  Assert.equal(
    484    result.usedMemories[0],
    485    "prefers italian food",
    486    "Should extract memory"
    487  );
    488 });
    489 
    490 add_task(function test_detectTokens_basic_pattern() {
    491  const content =
    492    "There are many great options available.§search: coffee shops near downtown§§search: best rated restaurants§";
    493  const searchRegex = /§search:\s*([^§]+)§/gi;
    494  const result = detectTokens(content, searchRegex, "query");
    495 
    496  Assert.equal(result.length, 2, "Should find two matches");
    497  Assert.equal(
    498    result[0].query,
    499    "coffee shops near downtown",
    500    "First match should extract correct query"
    501  );
    502  Assert.equal(
    503    result[0].fullMatch,
    504    "§search: coffee shops near downtown§",
    505    "First match should include full match"
    506  );
    507  Assert.equal(
    508    result[0].startIndex,
    509    39,
    510    "First match should have correct start index"
    511  );
    512  Assert.equal(
    513    result[1].query,
    514    "best rated restaurants",
    515    "Second match should extract correct query"
    516  );
    517 });
    518 
    519 add_task(function test_detectTokens_custom_key() {
    520  const content =
    521    "I recommend trying the Thai curry.§memory: prefers spicy food§";
    522  const memoryRegex = /§memory:\s*([^§]+)§/gi;
    523  const result = detectTokens(content, memoryRegex, "customKey");
    524 
    525  Assert.equal(result.length, 1, "Should find one match");
    526  Assert.equal(
    527    result[0].customKey,
    528    "prefers spicy food",
    529    "Should use custom key for extracted value"
    530  );
    531  Assert.ok(
    532    result[0].hasOwnProperty("customKey"),
    533    "Result should have the custom key property"
    534  );
    535  Assert.ok(
    536    !result[0].hasOwnProperty("query"),
    537    "Result should not have default 'query' property"
    538  );
    539 });