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 );