tor-browser

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

test_MemoriesManager.js (29278B)


      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 ("use strict");
      9 
     10 const { sinon } = ChromeUtils.importESModule(
     11  "resource://testing-common/Sinon.sys.mjs"
     12 );
     13 const { MemoriesManager } = ChromeUtils.importESModule(
     14  "moz-src:///browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs"
     15 );
     16 const {
     17  CATEGORIES,
     18  INTENTS,
     19  HISTORY: SOURCE_HISTORY,
     20  CONVERSATION: SOURCE_CONVERSATION,
     21 } = ChromeUtils.importESModule(
     22  "moz-src:///browser/components/aiwindow/models/memories/MemoriesConstants.sys.mjs"
     23 );
     24 const { getFormattedMemoryAttributeList } = ChromeUtils.importESModule(
     25  "moz-src:///browser/components/aiwindow/models/memories/Memories.sys.mjs"
     26 );
     27 const { MemoryStore } = ChromeUtils.importESModule(
     28  "moz-src:///browser/components/aiwindow/services/MemoryStore.sys.mjs"
     29 );
     30 
     31 /**
     32 * Constants for test memories
     33 */
     34 const TEST_MESSAGE = "Remember I like coffee.";
     35 const TEST_MEMORIES = [
     36  {
     37    memory_summary: "Loves drinking coffee",
     38    category: "Food & Drink",
     39    intent: "Plan / Organize",
     40    score: 3,
     41  },
     42  {
     43    memory_summary: "Buys dog food online",
     44    category: "Pets & Animals",
     45    intent: "Buy / Acquire",
     46    score: 4,
     47  },
     48 ];
     49 
     50 /**
     51 * Constants for preference keys and test values
     52 */
     53 const PREF_API_KEY = "browser.aiwindow.apiKey";
     54 const PREF_ENDPOINT = "browser.aiwindow.endpoint";
     55 const PREF_MODEL = "browser.aiwindow.model";
     56 
     57 const API_KEY = "fake-key";
     58 const ENDPOINT = "https://api.fake-endpoint.com/v1";
     59 const MODEL = "fake-model";
     60 
     61 /**
     62 * Helper function to delete all memories before and after a test
     63 */
     64 async function deleteAllMemories() {
     65  const memories = await MemoryStore.getMemories({ includeSoftDeleted: true });
     66  for (const memory of memories) {
     67    await MemoryStore.hardDeleteMemory(memory.id);
     68  }
     69 }
     70 
     71 /**
     72 * Helper function to bulk-add memories
     73 */
     74 async function addMemories() {
     75  await deleteAllMemories();
     76  for (const memory of TEST_MEMORIES) {
     77    await MemoryStore.addMemory(memory);
     78  }
     79 }
     80 
     81 add_setup(async function () {
     82  // Setup prefs used across multiple tests
     83  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
     84  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
     85  Services.prefs.setStringPref(PREF_MODEL, MODEL);
     86 
     87  // Clear prefs after testing
     88  registerCleanupFunction(() => {
     89    for (let pref of [PREF_API_KEY, PREF_ENDPOINT, PREF_MODEL]) {
     90      if (Services.prefs.prefHasUserValue(pref)) {
     91        Services.prefs.clearUserPref(pref);
     92      }
     93    }
     94  });
     95 });
     96 
     97 /**
     98 * Tests getting aggregated browser history from MemoriesHistorySource
     99 */
    100 add_task(async function test_getAggregatedBrowserHistory() {
    101  // Setup fake history data
    102  const now = Date.now();
    103  const seeded = [
    104    {
    105      url: "https://www.google.com/search?q=firefox+history",
    106      title: "Google Search: firefox history",
    107      visits: [{ date: new Date(now - 5 * 60 * 1000) }],
    108    },
    109    {
    110      url: "https://news.ycombinator.com/",
    111      title: "Hacker News",
    112      visits: [{ date: new Date(now - 15 * 60 * 1000) }],
    113    },
    114    {
    115      url: "https://mozilla.org/en-US/",
    116      title: "Internet for people, not profit — Mozilla",
    117      visits: [{ date: new Date(now - 25 * 60 * 1000) }],
    118    },
    119  ];
    120  await PlacesUtils.history.clear();
    121  await PlacesUtils.history.insertMany(seeded);
    122 
    123  // Check that all 3 outputs are arrays
    124  const [domainItems, titleItems, searchItems] =
    125    await MemoriesManager.getAggregatedBrowserHistory();
    126  Assert.ok(Array.isArray(domainItems), "Domain items should be an array");
    127  Assert.ok(Array.isArray(titleItems), "Title items should be an array");
    128  Assert.ok(Array.isArray(searchItems), "Search items should be an array");
    129 
    130  // Check the length of each
    131  Assert.equal(domainItems.length, 3, "Should have 3 domain items");
    132  Assert.equal(titleItems.length, 3, "Should have 3 title items");
    133  Assert.equal(searchItems.length, 1, "Should have 1 search item");
    134 
    135  // Check the top entry in each aggregate
    136  Assert.deepEqual(
    137    domainItems[0],
    138    ["mozilla.org", 100],
    139    "Top domain should be `mozilla.org' with score 100"
    140  );
    141  Assert.deepEqual(
    142    titleItems[0],
    143    ["Internet for people, not profit — Mozilla", 100],
    144    "Top title should be 'Internet for people, not profit — Mozilla' with score 100"
    145  );
    146  Assert.equal(
    147    searchItems[0].q[0],
    148    "Google Search: firefox history",
    149    "Top search item query should be 'Google Search: firefox history'"
    150  );
    151  Assert.equal(searchItems[0].r, 1, "Top search item rank should be 1");
    152 });
    153 
    154 /**
    155 * Tests retrieving all stored memories
    156 */
    157 add_task(async function test_getAllMemories() {
    158  await addMemories();
    159 
    160  const memories = await MemoriesManager.getAllMemories();
    161 
    162  // Check that the right number of memories were retrieved
    163  Assert.equal(
    164    memories.length,
    165    TEST_MEMORIES.length,
    166    "Should retrieve all stored memories."
    167  );
    168 
    169  // Check that the memories summaries are correct
    170  const testMemoriesSummaries = TEST_MEMORIES.map(
    171    memory => memory.memory_summary
    172  );
    173  const retrievedMemoriesSummaries = memories.map(
    174    memory => memory.memory_summary
    175  );
    176  retrievedMemoriesSummaries.forEach(memorySummary => {
    177    Assert.ok(
    178      testMemoriesSummaries.includes(memorySummary),
    179      `Memory summary "${memorySummary}" should be in the test memories.`
    180    );
    181  });
    182 
    183  await deleteAllMemories();
    184 });
    185 
    186 /**
    187 * Tests soft deleting a memory by ID
    188 */
    189 add_task(async function test_softDeleteMemoryById() {
    190  await addMemories();
    191 
    192  // Pull memories that aren't already soft deleted
    193  const memoriesBeforeSoftDelete = await MemoriesManager.getAllMemories();
    194 
    195  // Pick a memory off the top to soft delete
    196  const memoryBeforeSoftDelete = memoriesBeforeSoftDelete[0];
    197 
    198  // Double check that the memory isn't already soft deleted
    199  Assert.equal(
    200    memoryBeforeSoftDelete.is_deleted,
    201    false,
    202    "Memory should not be soft deleted initially."
    203  );
    204 
    205  // Soft delete the memory
    206  const memoryAfterSoftDelete = await MemoriesManager.softDeleteMemoryById(
    207    memoryBeforeSoftDelete.id
    208  );
    209 
    210  // Check that the memory is soft deleted
    211  Assert.equal(
    212    memoryAfterSoftDelete.is_deleted,
    213    true,
    214    "Memory should be soft deleted after calling softDeleteMemoryById."
    215  );
    216 
    217  // Retrieve all memories again, including soft deleted ones this time to make sure the deletion saved correctly
    218  const memoriesAfterSoftDelete = await MemoriesManager.getAllMemories({
    219    includeSoftDeleted: true,
    220  });
    221  const softDeletedMemories = memoriesAfterSoftDelete.filter(
    222    memory => memory.is_deleted
    223  );
    224  Assert.equal(
    225    softDeletedMemories.length,
    226    1,
    227    "There should be one soft deleted memory."
    228  );
    229 
    230  await deleteAllMemories();
    231 });
    232 
    233 /**
    234 * Tests attempting to soft delete a memory that doesn't exist by ID
    235 */
    236 add_task(async function test_softDeleteMemoryById_not_found() {
    237  await addMemories();
    238 
    239  // Retrieve all memories, including soft deleted ones
    240  const memoriesBeforeSoftDelete = await MemoriesManager.getAllMemories({
    241    includeSoftDeleted: true,
    242  });
    243 
    244  // Check that no memories are soft deleted initially
    245  const softDeletedMemoriesBefore = memoriesBeforeSoftDelete.filter(
    246    memory => memory.is_deleted
    247  );
    248  Assert.equal(
    249    softDeletedMemoriesBefore.length,
    250    0,
    251    "There should be no soft deleted memories initially."
    252  );
    253 
    254  // Attempt to soft delete a non-existent memory
    255  const memoryAfterSoftDelete =
    256    await MemoriesManager.softDeleteMemoryById("non-existent-id");
    257 
    258  // Check that the result is null (no memories were soft deleted)
    259  Assert.equal(
    260    memoryAfterSoftDelete,
    261    null,
    262    "softDeleteMemoryById should return null for non-existent memory ID."
    263  );
    264 
    265  // Retrieve all memories again to confirm no memories were soft deleted
    266  const memoriesAfterSoftDelete = await MemoriesManager.getAllMemories({
    267    includeSoftDeleted: true,
    268  });
    269  const softDeletedMemoriesAfter = memoriesAfterSoftDelete.filter(
    270    memory => memory.is_deleted
    271  );
    272  Assert.equal(
    273    softDeletedMemoriesAfter.length,
    274    0,
    275    "There should be no soft deleted memories after attempting to delete a non-existent memory."
    276  );
    277 
    278  await deleteAllMemories();
    279 });
    280 
    281 /**
    282 * Tests hard deleting a memory by ID
    283 */
    284 add_task(async function test_hardDeleteMemoryById() {
    285  await addMemories();
    286 
    287  // Retrieve all memories, including soft deleted ones
    288  const memoriesBeforeHardDelete = await MemoriesManager.getAllMemories({
    289    includeSoftDeleted: true,
    290  });
    291 
    292  // Pick a memory off the top to test hard deletion
    293  const memoryBeforeHardDelete = memoriesBeforeHardDelete[0];
    294 
    295  // Hard delete the memory
    296  const deletionResult = await MemoriesManager.hardDeleteMemoryById(
    297    memoryBeforeHardDelete.id
    298  );
    299 
    300  // Check that the deletion was successful
    301  Assert.ok(
    302    deletionResult,
    303    "hardDeleteMemoryById should return true on successful deletion."
    304  );
    305 
    306  // Retrieve all memories again to confirm the hard deletion was saved correctly
    307  const memoriesAfterHardDelete = await MemoriesManager.getAllMemories({
    308    includeSoftDeleted: true,
    309  });
    310  Assert.equal(
    311    memoriesAfterHardDelete.length,
    312    memoriesBeforeHardDelete.length - 1,
    313    "There should be one fewer memory after hard deletion."
    314  );
    315 
    316  await deleteAllMemories();
    317 });
    318 
    319 /**
    320 * Tests attempting to hard delete a memory that doesn't exist by ID
    321 */
    322 add_task(async function test_hardDeleteMemoryById_not_found() {
    323  await addMemories();
    324 
    325  // Retrieve all memories, including soft deleted ones
    326  const memoriesBeforeHardDelete = await MemoriesManager.getAllMemories({
    327    includeSoftDeleted: true,
    328  });
    329 
    330  // Hard delete the memory
    331  const deletionResult =
    332    await MemoriesManager.hardDeleteMemoryById("non-existent-id");
    333 
    334  // Check that the result is false (no memories were hard deleted)
    335  Assert.ok(
    336    !deletionResult,
    337    "hardDeleteMemoryById should return false for non-existent memory ID."
    338  );
    339 
    340  // Retrieve all memories again to make sure no memories were hard deleted
    341  const memoriesAfterHardDelete = await MemoriesManager.getAllMemories({
    342    includeSoftDeleted: true,
    343  });
    344  Assert.equal(
    345    memoriesAfterHardDelete.length,
    346    memoriesBeforeHardDelete.length,
    347    "Memory count before and after failed hard deletion should be the same."
    348  );
    349 
    350  await deleteAllMemories();
    351 });
    352 
    353 /**
    354 * Tests building the message memory classification prompt
    355 */
    356 add_task(async function test_buildMessageMemoryClassificationPrompt() {
    357  const prompt =
    358    await MemoriesManager.buildMessageMemoryClassificationPrompt(TEST_MESSAGE);
    359 
    360  Assert.ok(
    361    prompt.includes(TEST_MESSAGE),
    362    "Prompt should include the original message."
    363  );
    364  Assert.ok(
    365    prompt.includes(getFormattedMemoryAttributeList(CATEGORIES)),
    366    "Prompt should include formatted categories."
    367  );
    368  Assert.ok(
    369    prompt.includes(getFormattedMemoryAttributeList(INTENTS)),
    370    "Prompt should include formatted intents."
    371  );
    372 });
    373 
    374 /**
    375 * Tests classifying a user message into memory categories and intents
    376 */
    377 add_task(async function test_memoryClassifyMessage_happy_path() {
    378  const sb = sinon.createSandbox();
    379  try {
    380    const fakeEngine = {
    381      run() {
    382        return {
    383          finalOutput: `{
    384            "categories": ["Food & Drink"],
    385            "intents": ["Plan / Organize"]
    386          }`,
    387        };
    388      },
    389    };
    390 
    391    const stub = sb
    392      .stub(MemoriesManager, "ensureOpenAIEngine")
    393      .returns(fakeEngine);
    394    const messageClassification =
    395      await MemoriesManager.memoryClassifyMessage(TEST_MESSAGE);
    396    // Check that the stub was called
    397    Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
    398 
    399    // Check classification result was returned correctly
    400    Assert.equal(
    401      typeof messageClassification,
    402      "object",
    403      "Result should be an object."
    404    );
    405    Assert.equal(
    406      Object.keys(messageClassification).length,
    407      2,
    408      "Result should have two keys."
    409    );
    410    Assert.deepEqual(
    411      messageClassification.categories,
    412      ["Food & Drink"],
    413      "Categories should match the fake response."
    414    );
    415    Assert.deepEqual(
    416      messageClassification.intents,
    417      ["Plan / Organize"],
    418      "Intents should match the fake response."
    419    );
    420  } finally {
    421    sb.restore();
    422  }
    423 });
    424 
    425 /**
    426 * Tests failed message classification - LLM returns empty output
    427 */
    428 add_task(async function test_memoryClassifyMessage_sad_path_empty_output() {
    429  const sb = sinon.createSandbox();
    430  try {
    431    const fakeEngine = {
    432      run() {
    433        return {
    434          finalOutput: ``,
    435        };
    436      },
    437    };
    438 
    439    const stub = sb
    440      .stub(MemoriesManager, "ensureOpenAIEngine")
    441      .returns(fakeEngine);
    442    const messageClassification =
    443      await MemoriesManager.memoryClassifyMessage(TEST_MESSAGE);
    444    // Check that the stub was called
    445    Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
    446 
    447    // Check classification result was returned correctly despite empty output
    448    Assert.equal(
    449      typeof messageClassification,
    450      "object",
    451      "Result should be an object."
    452    );
    453    Assert.equal(
    454      Object.keys(messageClassification).length,
    455      2,
    456      "Result should have two keys."
    457    );
    458    Assert.equal(
    459      messageClassification.category,
    460      null,
    461      "Category should be null for empty output."
    462    );
    463    Assert.equal(
    464      messageClassification.intent,
    465      null,
    466      "Intent should be null for empty output."
    467    );
    468  } finally {
    469    sb.restore();
    470  }
    471 });
    472 
    473 /**
    474 * Tests failed message classification - LLM returns incorrect schema
    475 */
    476 add_task(async function test_memoryClassifyMessage_sad_path_bad_schema() {
    477  const sb = sinon.createSandbox();
    478  try {
    479    const fakeEngine = {
    480      run() {
    481        return {
    482          finalOutput: `{
    483            "wrong_key": "some value"
    484          }`,
    485        };
    486      },
    487    };
    488 
    489    const stub = sb
    490      .stub(MemoriesManager, "ensureOpenAIEngine")
    491      .returns(fakeEngine);
    492    const messageClassification =
    493      await MemoriesManager.memoryClassifyMessage(TEST_MESSAGE);
    494    // Check that the stub was called
    495    Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
    496 
    497    // Check classification result was returned correctly despite bad schema
    498    Assert.equal(
    499      typeof messageClassification,
    500      "object",
    501      "Result should be an object."
    502    );
    503    Assert.equal(
    504      Object.keys(messageClassification).length,
    505      2,
    506      "Result should have two keys."
    507    );
    508    Assert.equal(
    509      messageClassification.category,
    510      null,
    511      "Category should be null for bad schema output."
    512    );
    513    Assert.equal(
    514      messageClassification.intent,
    515      null,
    516      "Intent should be null for bad schema output."
    517    );
    518  } finally {
    519    sb.restore();
    520  }
    521 });
    522 
    523 /**
    524 * Tests retrieving relevant memories for a user message
    525 */
    526 add_task(async function test_getRelevantMemories_happy_path() {
    527  // Add memories so that we pass the existing memories check in the `getRelevantMemories` method
    528  await addMemories();
    529 
    530  const sb = sinon.createSandbox();
    531  try {
    532    const fakeEngine = {
    533      run() {
    534        return {
    535          finalOutput: `{
    536            "categories": ["Food & Drink"],
    537            "intents": ["Plan / Organize"]
    538          }`,
    539        };
    540      },
    541    };
    542 
    543    const stub = sb
    544      .stub(MemoriesManager, "ensureOpenAIEngine")
    545      .returns(fakeEngine);
    546    const relevantMemories =
    547      await MemoriesManager.getRelevantMemories(TEST_MESSAGE);
    548    // Check that the stub was called
    549    Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
    550 
    551    // Check that the correct relevant memory was returned
    552    Assert.ok(Array.isArray(relevantMemories), "Result should be an array.");
    553    Assert.equal(
    554      relevantMemories.length,
    555      1,
    556      "Result should contain one relevant memory."
    557    );
    558    Assert.equal(
    559      relevantMemories[0].memory_summary,
    560      "Loves drinking coffee",
    561      "Relevant memory summary should match."
    562    );
    563 
    564    // Delete memories after test
    565    await deleteAllMemories();
    566  } finally {
    567    sb.restore();
    568  }
    569 });
    570 
    571 /**
    572 * Tests failed memories retrieval - no existing memories stored
    573 *
    574 * We don't mock an engine for this test case because getRelevantMemories should immediately return an empty array
    575 * because there aren't any existing memories -> No need to call the LLM.
    576 */
    577 add_task(
    578  async function test_getRelevantMemories_sad_path_no_existing_memories() {
    579    const relevantMemories =
    580      await MemoriesManager.getRelevantMemories(TEST_MESSAGE);
    581 
    582    // Check that result is an empty array
    583    Assert.ok(Array.isArray(relevantMemories), "Result should be an array.");
    584    Assert.equal(
    585      relevantMemories.length,
    586      0,
    587      "Result should be an empty array when there are no existing memories."
    588    );
    589  }
    590 );
    591 
    592 /**
    593 * Tests failed memories retrieval - null classification
    594 */
    595 add_task(
    596  async function test_getRelevantMemories_sad_path_null_classification() {
    597    // Add memories so that we pass the existing memories check
    598    await addMemories();
    599 
    600    const sb = sinon.createSandbox();
    601    try {
    602      const fakeEngine = {
    603        run() {
    604          return {
    605            finalOutput: `{
    606            "categories": [],
    607            "intents": []
    608          }`,
    609          };
    610        },
    611      };
    612 
    613      const stub = sb
    614        .stub(MemoriesManager, "ensureOpenAIEngine")
    615        .returns(fakeEngine);
    616      const relevantMemories =
    617        await MemoriesManager.getRelevantMemories(TEST_MESSAGE);
    618      // Check that the stub was called
    619      Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
    620 
    621      // Check that result is an empty array
    622      Assert.ok(Array.isArray(relevantMemories), "Result should be an array.");
    623      Assert.equal(
    624        relevantMemories.length,
    625        0,
    626        "Result should be an empty array when category is null."
    627      );
    628 
    629      // Delete memories after test
    630      await deleteAllMemories();
    631    } finally {
    632      sb.restore();
    633    }
    634  }
    635 );
    636 
    637 /**
    638 * Tests failed memories retrieval - no memory in message's category
    639 */
    640 add_task(
    641  async function test_getRelevantMemories_sad_path_no_memories_in_message_category() {
    642    // Add memories so that we pass the existing memories check
    643    await addMemories();
    644 
    645    const sb = sinon.createSandbox();
    646    try {
    647      const fakeEngine = {
    648        run() {
    649          return {
    650            finalOutput: `{
    651            "categories": ["Health & Fitness"],
    652            "intents": ["Plan / Organize"]
    653          }`,
    654          };
    655        },
    656      };
    657 
    658      const stub = sb
    659        .stub(MemoriesManager, "ensureOpenAIEngine")
    660        .returns(fakeEngine);
    661      const relevantMemories =
    662        await MemoriesManager.getRelevantMemories(TEST_MESSAGE);
    663      // Check that the stub was called
    664      Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
    665 
    666      // Check that result is an empty array
    667      Assert.ok(Array.isArray(relevantMemories), "Result should be an array.");
    668      Assert.equal(
    669        relevantMemories.length,
    670        0,
    671        "Result should be an empty array when no memories match the message category."
    672      );
    673 
    674      // Delete memories after test
    675      await deleteAllMemories();
    676    } finally {
    677      sb.restore();
    678    }
    679  }
    680 );
    681 
    682 /**
    683 * Tests saveMemories correctly persists history memories and updates last_history_memory_ts.
    684 */
    685 add_task(async function test_saveMemories_history_updates_meta() {
    686  const sb = sinon.createSandbox();
    687  try {
    688    const now = Date.now();
    689 
    690    const generatedMemories = [
    691      {
    692        memory_summary: "foo",
    693        category: "A",
    694        intent: "X",
    695        score: 1,
    696        updated_at: now - 1000,
    697      },
    698      {
    699        memory_summary: "bar",
    700        category: "B",
    701        intent: "Y",
    702        score: 2,
    703        updated_at: now + 500,
    704      },
    705    ];
    706 
    707    const storedMemories = generatedMemories.map((generatedMemory, idx) => ({
    708      id: `id-${idx}`,
    709      ...generatedMemory,
    710    }));
    711 
    712    const addMemoryStub = sb
    713      .stub(MemoryStore, "addMemory")
    714      .callsFake(async partial => {
    715        // simple mapping: return first / second stored memory based on summary
    716        return storedMemories.find(
    717          s => s.memory_summary === partial.memory_summary
    718        );
    719      });
    720 
    721    const updateMetaStub = sb.stub(MemoryStore, "updateMeta").resolves();
    722 
    723    const { persistedMemories, newTimestampMs } =
    724      await MemoriesManager.saveMemories(
    725        generatedMemories,
    726        SOURCE_HISTORY,
    727        now
    728      );
    729 
    730    Assert.equal(
    731      addMemoryStub.callCount,
    732      generatedMemories.length,
    733      "addMemory should be called once per generated memory"
    734    );
    735    Assert.deepEqual(
    736      persistedMemories.map(i => i.id),
    737      storedMemories.map(i => i.id),
    738      "Persisted memories should match stored memories"
    739    );
    740 
    741    Assert.ok(
    742      updateMetaStub.calledOnce,
    743      "updateMeta should be called once for history source"
    744    );
    745    const metaArg = updateMetaStub.firstCall.args[0];
    746    Assert.ok(
    747      "last_history_memory_ts" in metaArg,
    748      "updateMeta should update last_history_memory_ts for history source"
    749    );
    750    Assert.equal(
    751      metaArg.last_history_memory_ts,
    752      storedMemories[1].updated_at,
    753      "last_history_memory_ts should be set to max(updated_at) among persisted memories"
    754    );
    755    Assert.equal(
    756      newTimestampMs,
    757      storedMemories[1].updated_at,
    758      "Returned newTimestampMs should match the updated meta timestamp"
    759    );
    760  } finally {
    761    sb.restore();
    762  }
    763 });
    764 
    765 /**
    766 * Tests saveMemories correctly persists conversation memories and updates last_chat_memory_ts.
    767 */
    768 add_task(async function test_saveMemories_conversation_updates_meta() {
    769  const sb = sinon.createSandbox();
    770  try {
    771    const now = Date.now();
    772 
    773    const generatedMemories = [
    774      {
    775        memory_summary: "chat-memory",
    776        category: "Chat",
    777        intent: "Talk",
    778        score: 1,
    779        updated_at: now,
    780      },
    781    ];
    782    const storedMemory = { id: "chat-1", ...generatedMemories[0] };
    783 
    784    const addMemoryStub = sb
    785      .stub(MemoryStore, "addMemory")
    786      .resolves(storedMemory);
    787    const updateMetaStub = sb.stub(MemoryStore, "updateMeta").resolves();
    788 
    789    const { persistedMemories, newTimestampMs } =
    790      await MemoriesManager.saveMemories(
    791        generatedMemories,
    792        SOURCE_CONVERSATION,
    793        now
    794      );
    795 
    796    Assert.equal(
    797      addMemoryStub.callCount,
    798      1,
    799      "addMemory should be called once for conversation memory"
    800    );
    801    Assert.equal(
    802      persistedMemories[0].id,
    803      storedMemory.id,
    804      "Persisted memory should match stored memory"
    805    );
    806 
    807    Assert.ok(
    808      updateMetaStub.calledOnce,
    809      "updateMeta should be called once for conversation source"
    810    );
    811    const metaArg = updateMetaStub.firstCall.args[0];
    812    Assert.ok(
    813      "last_chat_memory_ts" in metaArg,
    814      "updateMeta should update last_chat_memory_ts for conversation source"
    815    );
    816    Assert.equal(
    817      metaArg.last_chat_memory_ts,
    818      storedMemory.updated_at,
    819      "last_chat_memory_ts should be set to memory.updated_at"
    820    );
    821    Assert.equal(
    822      newTimestampMs,
    823      storedMemory.updated_at,
    824      "Returned newTimestampMs should match the updated meta timestamp"
    825    );
    826  } finally {
    827    sb.restore();
    828  }
    829 });
    830 
    831 /**
    832 * Tests that getLastHistoryMemoryTimestamp reads the same value written via MemoryStore.updateMeta.
    833 */
    834 add_task(async function test_getLastHistoryMemoryTimestamp_reads_meta() {
    835  const ts = Date.now() - 12345;
    836 
    837  // Write meta directly
    838  await MemoryStore.updateMeta({
    839    last_history_memory_ts: ts,
    840  });
    841 
    842  // Read via MemoriesManager helper
    843  const readTs = await MemoriesManager.getLastHistoryMemoryTimestamp();
    844 
    845  Assert.equal(
    846    readTs,
    847    ts,
    848    "getLastHistoryMemoryTimestamp should return last_history_memory_ts from MemoryStore meta"
    849  );
    850 });
    851 
    852 /**
    853 * Tests that getLastConversationMemoryTimestamp reads the same value written via MemoryStore.updateMeta.
    854 */
    855 add_task(async function test_getLastConversationMemoryTimestamp_reads_meta() {
    856  const ts = Date.now() - 54321;
    857 
    858  // Write meta directly
    859  await MemoryStore.updateMeta({
    860    last_chat_memory_ts: ts,
    861  });
    862 
    863  // Read via MemoriesManager helper
    864  const readTs = await MemoriesManager.getLastConversationMemoryTimestamp();
    865 
    866  Assert.equal(
    867    readTs,
    868    ts,
    869    "getLastConversationMemoryTimestamp should return last_chat_memory_ts from MemoryStore meta"
    870  );
    871 });
    872 
    873 /**
    874 * Tests that history memory generation updates last_history_memory_ts and not last_conversation_memory_ts.
    875 */
    876 add_task(
    877  async function test_historyTimestampUpdatedAfterHistoryMemoriesGenerationPass() {
    878    const sb = sinon.createSandbox();
    879 
    880    const lastHistoryMemoriesUpdateTs =
    881      await MemoriesManager.getLastHistoryMemoryTimestamp();
    882    const lastConversationMemoriesUpdateTs =
    883      await MemoriesManager.getLastConversationMemoryTimestamp();
    884 
    885    try {
    886      const aggregateBrowserHistoryStub = sb
    887        .stub(MemoriesManager, "getAggregatedBrowserHistory")
    888        .resolves([[], [], []]);
    889      const fakeEngine = sb
    890        .stub(MemoriesManager, "ensureOpenAIEngine")
    891        .resolves({
    892          run() {
    893            return {
    894              finalOutput: `[
    895  {
    896    "why": "User has recently searched for Firefox history and visited mozilla.org.",
    897    "category": "Internet & Telecom",
    898    "intent": "Research / Learn",
    899    "memory_summary": "Searches for Firefox information",
    900    "score": 7,
    901    "evidence": [
    902      {
    903        "type": "search",
    904        "value": "Google Search: firefox history"
    905      },
    906      {
    907        "type": "domain",
    908        "value": "mozilla.org"
    909      }
    910    ]
    911  },
    912  {
    913    "why": "User buys dog food online regularly from multiple sources.",
    914    "category": "Pets & Animals",
    915    "intent": "Buy / Acquire",
    916    "memory_summary": "Purchases dog food online",
    917    "score": -1,
    918    "evidence": [
    919      {
    920        "type": "domain",
    921        "value": "example.com"
    922      }
    923    ]
    924  }
    925 ]`,
    926            };
    927          },
    928        });
    929 
    930      await MemoriesManager.generateMemoriesFromBrowsingHistory();
    931 
    932      Assert.ok(
    933        aggregateBrowserHistoryStub.calledOnce,
    934        "getAggregatedBrowserHistory should be called once during memory generation"
    935      );
    936      Assert.ok(
    937        fakeEngine.calledOnce,
    938        "ensureOpenAIEngine should be called once during memory generation"
    939      );
    940 
    941      Assert.greater(
    942        await MemoriesManager.getLastHistoryMemoryTimestamp(),
    943        lastHistoryMemoriesUpdateTs,
    944        "Last history memory timestamp should be updated after history generation pass"
    945      );
    946      Assert.equal(
    947        await MemoriesManager.getLastConversationMemoryTimestamp(),
    948        lastConversationMemoriesUpdateTs,
    949        "Last conversation memory timestamp should remain unchanged after history generation pass"
    950      );
    951    } finally {
    952      sb.restore();
    953    }
    954  }
    955 );
    956 
    957 /**
    958 * Tests that conversation memory generation updates last_conversation_memory_ts and not last_history_memory_ts.
    959 */
    960 add_task(
    961  async function test_conversationTimestampUpdatedAfterConversationMemoriesGenerationPass() {
    962    const sb = sinon.createSandbox();
    963 
    964    const lastConversationMemoriesUpdateTs =
    965      await MemoriesManager.getLastConversationMemoryTimestamp();
    966    const lastHistoryMemoriesUpdateTs =
    967      await MemoriesManager.getLastHistoryMemoryTimestamp();
    968 
    969    try {
    970      const getRecentChatsStub = sb
    971        .stub(MemoriesManager, "_getRecentChats")
    972        .resolves([]);
    973 
    974      const fakeEngine = sb
    975        .stub(MemoriesManager, "ensureOpenAIEngine")
    976        .resolves({
    977          run() {
    978            return {
    979              finalOutput: `[
    980  {
    981    "why": "User has recently searched for Firefox history and visited mozilla.org.",
    982    "category": "Internet & Telecom",
    983    "intent": "Research / Learn",
    984    "memory_summary": "Searches for Firefox information",
    985    "score": 7,
    986    "evidence": [
    987      {
    988        "type": "search",
    989        "value": "Google Search: firefox history"
    990      },
    991      {
    992        "type": "domain",
    993        "value": "mozilla.org"
    994      }
    995    ]
    996  },
    997  {
    998    "why": "User buys dog food online regularly from multiple sources.",
    999    "category": "Pets & Animals",
   1000    "intent": "Buy / Acquire",
   1001    "memory_summary": "Purchases dog food online",
   1002    "score": -1,
   1003    "evidence": [
   1004      {
   1005        "type": "domain",
   1006        "value": "example.com"
   1007      }
   1008    ]
   1009  }
   1010 ]`,
   1011            };
   1012          },
   1013        });
   1014 
   1015      await MemoriesManager.generateMemoriesFromConversationHistory();
   1016 
   1017      Assert.ok(
   1018        getRecentChatsStub.calledOnce,
   1019        "getRecentChats should be called once during memory generation"
   1020      );
   1021      Assert.ok(
   1022        fakeEngine.calledOnce,
   1023        "ensureOpenAIEngine should be called once during memory generation"
   1024      );
   1025 
   1026      Assert.greater(
   1027        await MemoriesManager.getLastConversationMemoryTimestamp(),
   1028        lastConversationMemoriesUpdateTs,
   1029        "Last conversation memory timestamp should be updated after conversation generation pass"
   1030      );
   1031      Assert.equal(
   1032        await MemoriesManager.getLastHistoryMemoryTimestamp(),
   1033        lastHistoryMemoriesUpdateTs,
   1034        "Last history memory timestamp should remain unchanged after conversation generation pass"
   1035      );
   1036    } finally {
   1037      sb.restore();
   1038    }
   1039  }
   1040 );