test_Memories.js (41273B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 do_get_profile(); 6 7 const { ChatStore, ChatMessage, MESSAGE_ROLE } = ChromeUtils.importESModule( 8 "moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs" 9 ); 10 const { 11 getRecentHistory, 12 generateProfileInputs, 13 aggregateSessions, 14 topkAggregates, 15 } = ChromeUtils.importESModule( 16 "moz-src:///browser/components/aiwindow/models/memories/MemoriesHistorySource.sys.mjs" 17 ); 18 const { getRecentChats } = ChromeUtils.importESModule( 19 "moz-src:///browser/components/aiwindow/models/memories/MemoriesChatSource.sys.mjs" 20 ); 21 const { DEFAULT_ENGINE_ID, MODEL_FEATURES, openAIEngine, SERVICE_TYPES } = 22 ChromeUtils.importESModule( 23 "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs" 24 ); 25 const { sinon } = ChromeUtils.importESModule( 26 "resource://testing-common/Sinon.sys.mjs" 27 ); 28 29 const { CATEGORIES, INTENTS } = ChromeUtils.importESModule( 30 "moz-src:///browser/components/aiwindow/models/memories/MemoriesConstants.sys.mjs" 31 ); 32 33 const { 34 formatListForPrompt, 35 getFormattedMemoryAttributeList, 36 renderRecentHistoryForPrompt, 37 renderRecentConversationForPrompt, 38 mapFilteredMemoriesToInitialList, 39 buildInitialMemoriesGenerationPrompt, 40 buildMemoriesDeduplicationPrompt, 41 buildMemoriesSensitivityFilterPrompt, 42 generateInitialMemoriesList, 43 deduplicateMemories, 44 filterSensitiveMemories, 45 } = ChromeUtils.importESModule( 46 "moz-src:///browser/components/aiwindow/models/memories/Memories.sys.mjs" 47 ); 48 49 /** 50 * Constants for preference keys and test values 51 */ 52 const PREF_API_KEY = "browser.aiwindow.apiKey"; 53 const PREF_ENDPOINT = "browser.aiwindow.endpoint"; 54 const PREF_MODEL = "browser.aiwindow.model"; 55 56 const API_KEY = "fake-key"; 57 const ENDPOINT = "https://api.fake-endpoint.com/v1"; 58 const MODEL = "fake-model"; 59 60 const EXISTING_MEMORIES = [ 61 "Loves outdoor activities", 62 "Enjoys cooking recipes", 63 "Like sci-fi media", 64 ]; 65 const NEW_MEMORIES = [ 66 "Loves hiking and camping", 67 "Reads science fiction novels", 68 "Likes both dogs and cats", 69 "Likes risky stock bets", 70 ]; 71 72 add_setup(async function () { 73 // Setup prefs used across multiple tests 74 Services.prefs.setStringPref(PREF_API_KEY, API_KEY); 75 Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT); 76 Services.prefs.setStringPref(PREF_MODEL, MODEL); 77 78 // Clear prefs after testing 79 registerCleanupFunction(() => { 80 for (let pref of [PREF_API_KEY, PREF_ENDPOINT, PREF_MODEL]) { 81 if (Services.prefs.prefHasUserValue(pref)) { 82 Services.prefs.clearUserPref(pref); 83 } 84 } 85 }); 86 }); 87 88 /** 89 * Builds fake browsing history data for testing 90 */ 91 async function buildFakeBrowserHistory() { 92 const now = Date.now(); 93 94 const seeded = [ 95 { 96 url: "https://www.google.com/search?q=firefox+history", 97 title: "Google Search: firefox history", 98 visits: [{ date: new Date(now - 5 * 60 * 1000) }], 99 }, 100 { 101 url: "https://news.ycombinator.com/", 102 title: "Hacker News", 103 visits: [{ date: new Date(now - 15 * 60 * 1000) }], 104 }, 105 { 106 url: "https://mozilla.org/en-US/", 107 title: "Internet for people, not profit — Mozilla", 108 visits: [{ date: new Date(now - 25 * 60 * 1000) }], 109 }, 110 ]; 111 await PlacesUtils.history.clear(); 112 await PlacesUtils.history.insertMany(seeded); 113 } 114 115 /** 116 * Shortcut for full browser history aggregation pipeline 117 */ 118 async function getBrowserHistoryAggregates() { 119 const profileRecords = await getRecentHistory(); 120 const profilePreparedInputs = await generateProfileInputs(profileRecords); 121 const [domainAgg, titleAgg, searchAgg] = aggregateSessions( 122 profilePreparedInputs 123 ); 124 125 return await topkAggregates(domainAgg, titleAgg, searchAgg); 126 } 127 128 /** 129 * Builds fake chat history data for testing 130 */ 131 async function buildFakeChatHistory() { 132 const fixedNow = 1_700_000_000_000; 133 134 return [ 135 new ChatMessage({ 136 createdDate: fixedNow - 1_000, 137 ordinal: 1, 138 role: MESSAGE_ROLE.USER, 139 content: { type: "text", body: "I like dogs." }, 140 pageUrl: "https://example.com/1", 141 turnIndex: 0, 142 }), 143 new ChatMessage({ 144 createdDate: fixedNow - 10_000, 145 ordinal: 2, 146 role: MESSAGE_ROLE.USER, 147 content: { type: "text", body: "I also like cats." }, 148 pageUrl: "https://example.com/2", 149 turnIndex: 0, 150 }), 151 new ChatMessage({ 152 createdDate: fixedNow - 100_000, 153 ordinal: 3, 154 role: MESSAGE_ROLE.USER, 155 content: { 156 type: "text", 157 body: "Tell me a joke about my favorite animals.", 158 }, 159 pageUrl: "https://example.com/3", 160 turnIndex: 0, 161 }), 162 ]; 163 } 164 165 /** 166 * Tests building the prompt for initial memories generation 167 */ 168 add_task(async function test_buildInitialMemoriesGenerationPrompt() { 169 // Check that history is rendered correctly into CSV tables 170 await buildFakeBrowserHistory(); 171 const [domainItems, titleItems, searchItems] = 172 await getBrowserHistoryAggregates(); 173 const renderedBrowserHistory = await renderRecentHistoryForPrompt( 174 domainItems, 175 titleItems, 176 searchItems 177 ); 178 Assert.equal( 179 renderedBrowserHistory, 180 `# Domains 181 Domain,Importance Score 182 www.google.com,100 183 news.ycombinator.com,100 184 mozilla.org,100 185 186 # Titles 187 Title,Importance Score 188 Google Search: firefox history,100 189 Hacker News,100 190 Internet for people, not profit — Mozilla,100 191 192 # Searches 193 Search,Importance Score 194 Google Search: firefox history,1`.trim() 195 ); 196 197 // Check that the full prompt is built correctly with injected categories, intents, and browsing history 198 const sources = { history: [domainItems, titleItems, searchItems] }; 199 const initialMemoriesPrompt = 200 await buildInitialMemoriesGenerationPrompt(sources); 201 Assert.ok( 202 initialMemoriesPrompt.includes( 203 "You are an expert at extracting memories from user browser data." 204 ), 205 "Initial memories generation prompt should pull from the correct base" 206 ); 207 Assert.ok( 208 initialMemoriesPrompt.includes(getFormattedMemoryAttributeList(CATEGORIES)), 209 "Prompt should include formatted categories list" 210 ); 211 Assert.ok( 212 initialMemoriesPrompt.includes(getFormattedMemoryAttributeList(INTENTS)), 213 "Prompt should include formatted intents list" 214 ); 215 Assert.ok( 216 initialMemoriesPrompt.includes(renderedBrowserHistory), 217 "Prompt should include rendered browsing history" 218 ); 219 }); 220 221 /** 222 * Tests rendering history as CSV when only search data is present 223 */ 224 add_task(async function test_buildRecentHistoryCSV_only_search() { 225 const now = Date.now(); 226 const seeded = [ 227 { 228 url: "https://www.google.com/search?q=firefox+history", 229 title: "Google Search: firefox history", 230 visits: [{ date: new Date(now - 5 * 60 * 1000) }], 231 }, 232 ]; 233 await PlacesUtils.history.clear(); 234 await PlacesUtils.history.insertMany(seeded); 235 236 const [domainItems, titleItems, searchItems] = 237 await getBrowserHistoryAggregates(); 238 const renderedBrowserHistory = await renderRecentHistoryForPrompt( 239 domainItems, 240 titleItems, 241 searchItems 242 ); 243 Assert.equal( 244 renderedBrowserHistory, 245 `# Domains 246 Domain,Importance Score 247 www.google.com,100 248 249 # Titles 250 Title,Importance Score 251 Google Search: firefox history,100 252 253 # Searches 254 Search,Importance Score 255 Google Search: firefox history,1`.trim() 256 ); 257 }); 258 259 /** 260 * Tests rendering history as CSV when only history data is present 261 */ 262 add_task(async function test_buildRecentHistoryCSV_only_browsing_history() { 263 const now = Date.now(); 264 const seeded = [ 265 { 266 url: "https://news.ycombinator.com/", 267 title: "Hacker News", 268 visits: [{ date: new Date(now - 15 * 60 * 1000) }], 269 }, 270 { 271 url: "https://mozilla.org/en-US/", 272 title: "Internet for people, not profit — Mozilla", 273 visits: [{ date: new Date(now - 25 * 60 * 1000) }], 274 }, 275 ]; 276 await PlacesUtils.history.clear(); 277 await PlacesUtils.history.insertMany(seeded); 278 279 const [domainItems, titleItems, searchItems] = 280 await getBrowserHistoryAggregates(); 281 const renderedBrowserHistory = await renderRecentHistoryForPrompt( 282 domainItems, 283 titleItems, 284 searchItems 285 ); 286 Assert.equal( 287 renderedBrowserHistory, 288 `# Domains 289 Domain,Importance Score 290 news.ycombinator.com,100 291 mozilla.org,100 292 293 # Titles 294 Title,Importance Score 295 Hacker News,100 296 Internet for people, not profit — Mozilla,100`.trim() 297 ); 298 }); 299 300 /** 301 * Tests building the prompt for initial memories generation with only chat data 302 */ 303 add_task(async function test_buildInitialMemoriesGenerationPrompt_only_chat() { 304 const messages = await buildFakeChatHistory(); 305 const sb = sinon.createSandbox(); 306 const maxResults = 3; 307 const halfLifeDays = 7; 308 const startTime = 1_700_000_000_000 - 1_000_000; 309 310 try { 311 // Stub the method 312 const stub = sb 313 .stub(ChatStore.prototype, "findMessagesByDate") 314 .callsFake(async () => { 315 return messages; 316 }); 317 318 const recentMessages = await getRecentChats( 319 startTime, 320 maxResults, 321 halfLifeDays 322 ); 323 324 // Assert stub was actually called 325 Assert.equal(stub.callCount, 1, "findMessagesByDate should be called once"); 326 327 // Double check we get only the 3 expected messages back 328 Assert.equal(recentMessages.length, 3, "Should return 3 chat messages"); 329 330 // Render the messages into CSV format and check correctness 331 const renderedConversationHistory = 332 await renderRecentConversationForPrompt(recentMessages); 333 Assert.equal( 334 renderedConversationHistory, 335 `# Chat History 336 Message 337 I like dogs. 338 I also like cats. 339 Tell me a joke about my favorite animals.`.trim(), 340 "Rendered conversation history should match expected CSV format" 341 ); 342 343 // Build the actual prompt and check its contents 344 const sources = { conversation: recentMessages }; 345 const initialMemoriesPrompt = 346 await buildInitialMemoriesGenerationPrompt(sources); 347 Assert.ok( 348 initialMemoriesPrompt.includes( 349 "You are an expert at extracting memories from user browser data." 350 ), 351 "Initial memories generation prompt should pull from the correct base" 352 ); 353 Assert.ok( 354 initialMemoriesPrompt.includes(renderedConversationHistory), 355 "Prompt should include rendered conversation history" 356 ); 357 } finally { 358 sb.restore(); 359 } 360 }); 361 362 /** 363 * Tests building the prompt for memories deduplication 364 */ 365 add_task(async function test_buildMemoriesDeduplicationPrompt() { 366 const memoriesDeduplicationPrompt = await buildMemoriesDeduplicationPrompt( 367 EXISTING_MEMORIES, 368 NEW_MEMORIES 369 ); 370 Assert.ok( 371 memoriesDeduplicationPrompt.includes( 372 "You are an expert at identifying duplicate statements." 373 ), 374 "Memories deduplication prompt should pull from the correct base" 375 ); 376 Assert.ok( 377 memoriesDeduplicationPrompt.includes( 378 formatListForPrompt(EXISTING_MEMORIES) 379 ), 380 "Deduplication prompt should include existing memories list" 381 ); 382 Assert.ok( 383 memoriesDeduplicationPrompt.includes(formatListForPrompt(NEW_MEMORIES)), 384 "Deduplication prompt should include new memories list" 385 ); 386 }); 387 388 /** 389 * Tests building the prompt for memories sensitivity filtering 390 */ 391 add_task(async function test_buildMemoriesSensitivityFilterPrompt() { 392 /** Memories sensitivity filter prompt */ 393 const memoriesSensitivityFilterPrompt = 394 await buildMemoriesSensitivityFilterPrompt(NEW_MEMORIES); 395 Assert.ok( 396 memoriesSensitivityFilterPrompt.includes( 397 "You are an expert at identifying sensitive statements and content." 398 ), 399 "Memories sensitivity filter prompt should pull from the correct base" 400 ); 401 Assert.ok( 402 memoriesSensitivityFilterPrompt.includes(formatListForPrompt(NEW_MEMORIES)), 403 "Sensitivity filter prompt should include memories list" 404 ); 405 }); 406 407 /** 408 * Tests successful initial memories generation 409 */ 410 add_task(async function test_generateInitialMemoriesList_happy_path() { 411 const sb = sinon.createSandbox(); 412 try { 413 /** 414 * The fake engine returns canned LLM response. 415 * The main `generateInitialMemoriesList` function should modify this heavily, cutting it back to only the required fields. 416 */ 417 const fakeEngine = { 418 run() { 419 return { 420 finalOutput: `[ 421 { 422 "why": "User has recently searched for Firefox history and visited mozilla.org.", 423 "category": "Internet & Telecom", 424 "intent": "Research / Learn", 425 "memory_summary": "Searches for Firefox information", 426 "score": 7, 427 "evidence": [ 428 { 429 "type": "search", 430 "value": "Google Search: firefox history" 431 }, 432 { 433 "type": "domain", 434 "value": "mozilla.org" 435 } 436 ] 437 }, 438 { 439 "why": "User buys dog food online regularly from multiple sources.", 440 "category": "Pets & Animals", 441 "intent": "Buy / Acquire", 442 "memory_summary": "Purchases dog food online", 443 "score": -1, 444 "evidence": [ 445 { 446 "type": "domain", 447 "value": "example.com" 448 } 449 ] 450 } 451 ]`, 452 }; 453 }, 454 }; 455 456 // Check that the stub was called 457 const stub = sb.stub(openAIEngine, "_createEngine").returns(fakeEngine); 458 const engine = await openAIEngine.build( 459 MODEL_FEATURES.MEMORIES, 460 DEFAULT_ENGINE_ID, 461 SERVICE_TYPES.MEMORIES 462 ); 463 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 464 465 const [domainItems, titleItems, searchItems] = 466 await getBrowserHistoryAggregates(); 467 const sources = { history: [domainItems, titleItems, searchItems] }; 468 const memoriesList = await generateInitialMemoriesList(engine, sources); 469 470 // Check top level structure 471 Assert.ok( 472 Array.isArray(memoriesList), 473 "Should return an array of memories" 474 ); 475 Assert.equal(memoriesList.length, 2, "Array should contain 2 memories"); 476 477 // Check first memory structure and content 478 const firstMemory = memoriesList[0]; 479 Assert.equal( 480 typeof firstMemory, 481 "object", 482 "First memory should be an object/map" 483 ); 484 Assert.equal( 485 Object.keys(firstMemory).length, 486 4, 487 "First memory should have 4 keys" 488 ); 489 Assert.equal( 490 firstMemory.category, 491 "Internet & Telecom", 492 "First memory should have expected category (Internet & Telecom)" 493 ); 494 Assert.equal( 495 firstMemory.intent, 496 "Research / Learn", 497 "First memory should have expected intent (Research / Learn)" 498 ); 499 Assert.equal( 500 firstMemory.memory_summary, 501 "Searches for Firefox information", 502 "First memory should have expected summary" 503 ); 504 Assert.equal( 505 firstMemory.score, 506 5, 507 "First memory should have expected score, clamping 7 to 5" 508 ); 509 510 // Check that the second memory's score was clamped to the minimum 511 const secondMemory = memoriesList[1]; 512 Assert.equal( 513 secondMemory.score, 514 1, 515 "Second memory should have expected score, clamping -1 to 1" 516 ); 517 } finally { 518 sb.restore(); 519 } 520 }); 521 522 /** 523 * Tests failed initial memories generation - Empty output 524 */ 525 add_task( 526 async function test_generateInitialMemoriesList_sad_path_empty_output() { 527 const sb = sinon.createSandbox(); 528 try { 529 // LLM returns an empty memories list 530 const fakeEngine = { 531 run() { 532 return { 533 finalOutput: `[]`, 534 }; 535 }, 536 }; 537 538 // Check that the stub was called 539 const stub = sb.stub(openAIEngine, "_createEngine").returns(fakeEngine); 540 const engine = await openAIEngine.build( 541 MODEL_FEATURES.MEMORIES, 542 DEFAULT_ENGINE_ID, 543 SERVICE_TYPES.MEMORIES 544 ); 545 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 546 547 const [domainItems, titleItems, searchItems] = 548 await getBrowserHistoryAggregates(); 549 const sources = { history: [domainItems, titleItems, searchItems] }; 550 const memoriesList = await generateInitialMemoriesList(engine, sources); 551 552 Assert.equal(Array.isArray(memoriesList), true, "Should return an array"); 553 Assert.equal(memoriesList.length, 0, "Array should contain 0 memories"); 554 } finally { 555 sb.restore(); 556 } 557 } 558 ); 559 560 /** 561 * Tests failed initial memories generation - Output not array 562 */ 563 add_task( 564 async function test_generateInitialMemoriesList_sad_path_output_not_array() { 565 const sb = sinon.createSandbox(); 566 try { 567 // LLM doesn't return an array 568 const fakeEngine = { 569 run() { 570 return { 571 finalOutput: `testing`, 572 }; 573 }, 574 }; 575 576 // Check that the stub was called 577 const stub = sb.stub(openAIEngine, "_createEngine").returns(fakeEngine); 578 const engine = await openAIEngine.build( 579 MODEL_FEATURES.MEMORIES, 580 DEFAULT_ENGINE_ID, 581 SERVICE_TYPES.MEMORIES 582 ); 583 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 584 585 const [domainItems, titleItems, searchItems] = 586 await getBrowserHistoryAggregates(); 587 const sources = { history: [domainItems, titleItems, searchItems] }; 588 const memoriesList = await generateInitialMemoriesList(engine, sources); 589 590 Assert.equal(Array.isArray(memoriesList), true, "Should return an array"); 591 Assert.equal(memoriesList.length, 0, "Array should contain 0 memories"); 592 } finally { 593 sb.restore(); 594 } 595 } 596 ); 597 598 /** 599 * Tests failed initial memories generation - Output not array of maps 600 */ 601 add_task( 602 async function test_generateInitialMemoriesList_sad_path_output_not_array_of_maps() { 603 const sb = sinon.createSandbox(); 604 try { 605 // LLM doesn't return an array of maps 606 const fakeEngine = { 607 run() { 608 return { 609 finalOutput: `["testing1", "testing2", ["testing3"]]`, 610 }; 611 }, 612 }; 613 614 // Check that the stub was called 615 const stub = sb.stub(openAIEngine, "_createEngine").returns(fakeEngine); 616 const engine = await openAIEngine.build( 617 MODEL_FEATURES.MEMORIES, 618 DEFAULT_ENGINE_ID, 619 SERVICE_TYPES.MEMORIES 620 ); 621 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 622 623 const [domainItems, titleItems, searchItems] = 624 await getBrowserHistoryAggregates(); 625 const sources = { history: [domainItems, titleItems, searchItems] }; 626 const memoriesList = await generateInitialMemoriesList(engine, sources); 627 628 Assert.equal(Array.isArray(memoriesList), true, "Should return an array"); 629 Assert.equal(memoriesList.length, 0, "Array should contain 0 memories"); 630 } finally { 631 sb.restore(); 632 } 633 } 634 ); 635 636 /** 637 * Tests failed initial memories generation - Some correct memories 638 */ 639 add_task( 640 async function test_generateInitialMemoriesList_sad_path_some_correct_memories() { 641 const sb = sinon.createSandbox(); 642 try { 643 // LLM returns an memories list where 1 is fully correct and 1 is missing required keys (category in this case) 644 const fakeEngine = { 645 run() { 646 return { 647 finalOutput: `[ 648 { 649 "why": "User has recently searched for Firefox history and visited mozilla.org.", 650 "intent": "Research / Learn", 651 "memory_summary": "Searches for Firefox information", 652 "score": 7, 653 "evidence": [ 654 { 655 "type": "search", 656 "value": "Google Search: firefox history" 657 }, 658 { 659 "type": "domain", 660 "value": "mozilla.org" 661 } 662 ] 663 }, 664 { 665 "why": "User buys dog food online regularly from multiple sources.", 666 "category": "Pets & Animals", 667 "intent": "Buy / Acquire", 668 "memory_summary": "Purchases dog food online", 669 "score": -1, 670 "evidence": [ 671 { 672 "type": "domain", 673 "value": "example.com" 674 } 675 ] 676 } 677 ]`, 678 }; 679 }, 680 }; 681 682 // Check that the stub was called 683 const stub = sb.stub(openAIEngine, "_createEngine").returns(fakeEngine); 684 const engine = await openAIEngine.build( 685 MODEL_FEATURES.MEMORIES, 686 DEFAULT_ENGINE_ID, 687 SERVICE_TYPES.MEMORIES 688 ); 689 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 690 691 const [domainItems, titleItems, searchItems] = 692 await getBrowserHistoryAggregates(); 693 const sources = { history: [domainItems, titleItems, searchItems] }; 694 const memoriesList = await generateInitialMemoriesList(engine, sources); 695 696 Assert.equal( 697 Array.isArray(memoriesList), 698 true, 699 "Should return an array of memories" 700 ); 701 Assert.equal(memoriesList.length, 1, "Array should contain 1 memory"); 702 Assert.equal( 703 memoriesList[0].memory_summary, 704 "Purchases dog food online", 705 "Memory summary should match the valid memory" 706 ); 707 } finally { 708 sb.restore(); 709 } 710 } 711 ); 712 713 /** 714 * Tests successful memories deduplication 715 */ 716 add_task(async function test_deduplicateMemoriesList_happy_path() { 717 const sb = sinon.createSandbox(); 718 try { 719 /** 720 * The fake engine that returns a canned LLM response for deduplication. 721 * The `deduplicateMemories` function should return an array containing only the `main_memory` values. 722 */ 723 const fakeEngine = { 724 run() { 725 return { 726 finalOutput: `{ 727 "unique_memories": [ 728 { 729 "main_memory": "Loves outdoor activities", 730 "duplicates": ["Loves hiking and camping"] 731 }, 732 { 733 "main_memory": "Enjoys cooking recipes", 734 "duplicates": [] 735 }, 736 { 737 "main_memory": "Like sci-fi media", 738 "duplicates": ["Reads science fiction novels"] 739 }, 740 { 741 "main_memory": "Likes both dogs and cats", 742 "duplicates": [] 743 }, 744 { 745 "main_memory": "Likes risky stock bets", 746 "duplicates": [] 747 } 748 ] 749 }`, 750 }; 751 }, 752 }; 753 754 // Check that the stub was called 755 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 756 const engine = await openAIEngine.build( 757 MODEL_FEATURES.MEMORIES, 758 DEFAULT_ENGINE_ID, 759 SERVICE_TYPES.MEMORIES 760 ); 761 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 762 763 const dedupedMemoriesList = await deduplicateMemories( 764 engine, 765 EXISTING_MEMORIES, 766 NEW_MEMORIES 767 ); 768 769 // Check that the deduplicated list contains only unique memories (`main_memory` values) 770 Assert.equal( 771 dedupedMemoriesList.length, 772 5, 773 "Deduplicated memories list should contain 5 unique memories" 774 ); 775 Assert.ok( 776 dedupedMemoriesList.includes("Loves outdoor activities"), 777 "Deduplicated memories should include 'Loves outdoor activities'" 778 ); 779 Assert.ok( 780 dedupedMemoriesList.includes("Enjoys cooking recipes"), 781 "Deduplicated memories should include 'Enjoys cooking recipes'" 782 ); 783 Assert.ok( 784 dedupedMemoriesList.includes("Like sci-fi media"), 785 "Deduplicated memories should include 'Like sci-fi media'" 786 ); 787 Assert.ok( 788 dedupedMemoriesList.includes("Likes both dogs and cats"), 789 "Deduplicated memories should include 'Likes both dogs and cats'" 790 ); 791 Assert.ok( 792 dedupedMemoriesList.includes("Likes risky stock bets"), 793 "Deduplicated memories should include 'Likes risky stock bets'" 794 ); 795 } finally { 796 sb.restore(); 797 } 798 }); 799 800 /** 801 * Tests failed memories deduplication - Empty output 802 */ 803 add_task(async function test_deduplicateMemoriesList_sad_path_empty_output() { 804 const sb = sinon.createSandbox(); 805 try { 806 // LLM returns the correct schema but with an empty unique_memories array 807 const fakeEngine = { 808 run() { 809 return { 810 finalOutput: `{ 811 "unique_memories": [] 812 }`, 813 }; 814 }, 815 }; 816 817 // Check that the stub was called 818 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 819 const engine = await openAIEngine.build( 820 MODEL_FEATURES.MEMORIES, 821 DEFAULT_ENGINE_ID, 822 SERVICE_TYPES.MEMORIES 823 ); 824 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 825 826 const dedupedMemoriesList = await deduplicateMemories( 827 engine, 828 EXISTING_MEMORIES, 829 NEW_MEMORIES 830 ); 831 832 Assert.ok(Array.isArray(dedupedMemoriesList), "Should return an array"); 833 Assert.equal(dedupedMemoriesList.length, 0, "Should return an empty array"); 834 } finally { 835 sb.restore(); 836 } 837 }); 838 839 /** 840 * Tests failed memories deduplication - Wrong top-level data type 841 */ 842 add_task( 843 async function test_deduplicateMemoriesList_sad_path_wrong_top_level_data_type() { 844 const sb = sinon.createSandbox(); 845 try { 846 // LLM returns an incorrect data type 847 const fakeEngine = { 848 run() { 849 return { 850 finalOutput: `testing`, 851 }; 852 }, 853 }; 854 855 // Check that the stub was called 856 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 857 const engine = await openAIEngine.build( 858 MODEL_FEATURES.MEMORIES, 859 DEFAULT_ENGINE_ID, 860 SERVICE_TYPES.MEMORIES 861 ); 862 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 863 864 const dedupedMemoriesList = await deduplicateMemories( 865 engine, 866 EXISTING_MEMORIES, 867 NEW_MEMORIES 868 ); 869 870 Assert.ok(Array.isArray(dedupedMemoriesList), "Should return an array"); 871 Assert.equal( 872 dedupedMemoriesList.length, 873 0, 874 "Should return an empty array" 875 ); 876 } finally { 877 sb.restore(); 878 } 879 } 880 ); 881 882 /** 883 * Tests failed memories deduplication - Wrong inner data type 884 */ 885 add_task( 886 async function test_deduplicateMemoriesList_sad_path_wrong_inner_data_type() { 887 const sb = sinon.createSandbox(); 888 try { 889 // LLM returns a map with the right top-level key, but the inner structure is wrong 890 const fakeEngine = { 891 run() { 892 return { 893 finalOutput: `{ 894 "unique_memories": "testing" 895 }`, 896 }; 897 }, 898 }; 899 900 // Check that the stub was called 901 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 902 const engine = await openAIEngine.build( 903 MODEL_FEATURES.MEMORIES, 904 DEFAULT_ENGINE_ID, 905 SERVICE_TYPES.MEMORIES 906 ); 907 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 908 909 const dedupedMemoriesList = await deduplicateMemories( 910 engine, 911 EXISTING_MEMORIES, 912 NEW_MEMORIES 913 ); 914 915 Assert.ok(Array.isArray(dedupedMemoriesList), "Should return an array"); 916 Assert.equal( 917 dedupedMemoriesList.length, 918 0, 919 "Should return an empty array" 920 ); 921 } finally { 922 sb.restore(); 923 } 924 } 925 ); 926 927 /** 928 * Tests failed memories deduplication - Wrong inner array structure 929 */ 930 add_task( 931 async function test_deduplicateMemoriesList_sad_path_wrong_inner_array_structure() { 932 const sb = sinon.createSandbox(); 933 try { 934 // LLM returns a map of nested arrays, but the array structure is wrong 935 const fakeEngine = { 936 run() { 937 return { 938 finalOutput: `{ 939 "unique_memories": ["testing1", "testing2"] 940 }`, 941 }; 942 }, 943 }; 944 945 // Check that the stub was called 946 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 947 const engine = await openAIEngine.build( 948 MODEL_FEATURES.MEMORIES, 949 DEFAULT_ENGINE_ID, 950 SERVICE_TYPES.MEMORIES 951 ); 952 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 953 954 const dedupedMemoriesList = await deduplicateMemories( 955 engine, 956 EXISTING_MEMORIES, 957 NEW_MEMORIES 958 ); 959 960 Assert.ok(Array.isArray(dedupedMemoriesList), "Should return an array"); 961 Assert.equal( 962 dedupedMemoriesList.length, 963 0, 964 "Should return an empty array" 965 ); 966 } finally { 967 sb.restore(); 968 } 969 } 970 ); 971 972 /** 973 * Tests failed memories deduplication - Incorrect top-level schema key 974 */ 975 add_task( 976 async function test_deduplicateMemoriesList_sad_path_bad_top_level_key() { 977 const sb = sinon.createSandbox(); 978 try { 979 // LLm returns correct output except that the top-level key is wrong 980 const fakeEngine = { 981 run() { 982 return { 983 finalOutput: `{ 984 "correct_memories": [ 985 { 986 "main_memory": "Loves outdoor activities", 987 "duplicates": ["Loves hiking and camping"] 988 }, 989 { 990 "main_memory": "Enjoys cooking recipes", 991 "duplicates": [] 992 }, 993 { 994 "main_memory": "Like sci-fi media", 995 "duplicates": ["Reads science fiction novels"] 996 }, 997 { 998 "main_memory": "Likes both dogs and cats", 999 "duplicates": [] 1000 }, 1001 { 1002 "main_memory": "Likes risky stock bets", 1003 "duplicates": [] 1004 } 1005 ] 1006 }`, 1007 }; 1008 }, 1009 }; 1010 1011 // Check that the stub was called 1012 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 1013 const engine = await openAIEngine.build( 1014 MODEL_FEATURES.MEMORIES, 1015 DEFAULT_ENGINE_ID, 1016 SERVICE_TYPES.MEMORIES 1017 ); 1018 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 1019 1020 const dedupedMemoriesList = await deduplicateMemories( 1021 engine, 1022 EXISTING_MEMORIES, 1023 NEW_MEMORIES 1024 ); 1025 1026 Assert.ok(Array.isArray(dedupedMemoriesList), "Should return an array"); 1027 Assert.equal( 1028 dedupedMemoriesList.length, 1029 0, 1030 "Should return an empty array" 1031 ); 1032 } finally { 1033 sb.restore(); 1034 } 1035 } 1036 ); 1037 1038 /** 1039 * Tests failed memories deduplication - Some correct inner schema 1040 */ 1041 add_task( 1042 async function test_deduplicateMemoriesList_sad_path_bad_some_correct_inner_schema() { 1043 const sb = sinon.createSandbox(); 1044 try { 1045 // LLm returns correct output except that 1 of the inner maps is wrong and 1 main_memory is the wrong data type 1046 const fakeEngine = { 1047 run() { 1048 return { 1049 finalOutput: `{ 1050 "unique_memories": [ 1051 { 1052 "primary_memory": "Loves outdoor activities", 1053 "duplicates": ["Loves hiking and camping"] 1054 }, 1055 { 1056 "main_memory": "Enjoys cooking recipes", 1057 "duplicates": [] 1058 }, 1059 { 1060 "main_memory": 12345, 1061 "duplicates": [] 1062 } 1063 ] 1064 }`, 1065 }; 1066 }, 1067 }; 1068 1069 // Check that the stub was called 1070 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 1071 const engine = await openAIEngine.build( 1072 MODEL_FEATURES.MEMORIES, 1073 DEFAULT_ENGINE_ID, 1074 SERVICE_TYPES.MEMORIES 1075 ); 1076 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 1077 1078 const dedupedMemoriesList = await deduplicateMemories( 1079 engine, 1080 EXISTING_MEMORIES, 1081 NEW_MEMORIES 1082 ); 1083 1084 Assert.ok(Array.isArray(dedupedMemoriesList), "Should return an array"); 1085 Assert.equal( 1086 dedupedMemoriesList.length, 1087 1, 1088 "Should return an array with one valid memory" 1089 ); 1090 Assert.equal( 1091 dedupedMemoriesList[0], 1092 "Enjoys cooking recipes", 1093 "Should return the single valid memory" 1094 ); 1095 } finally { 1096 sb.restore(); 1097 } 1098 } 1099 ); 1100 1101 /** 1102 * Tests successful memories sensitivity filtering 1103 */ 1104 add_task(async function test_filterSensitiveMemories_happy_path() { 1105 const sb = sinon.createSandbox(); 1106 try { 1107 /** 1108 * The fake engine that returns a canned LLM response for deduplication. 1109 * The `filterSensitiveMemories` function should return the inner array from `non_sensitive_memories`. 1110 */ 1111 const fakeEngine = { 1112 run() { 1113 return { 1114 finalOutput: `{ 1115 "non_sensitive_memories": [ 1116 "Loves hiking and camping", 1117 "Reads science fiction novels", 1118 "Likes both dogs and cats" 1119 ] 1120 }`, 1121 }; 1122 }, 1123 }; 1124 1125 // Check that the stub was called 1126 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 1127 const engine = await openAIEngine.build( 1128 MODEL_FEATURES.MEMORIES, 1129 DEFAULT_ENGINE_ID, 1130 SERVICE_TYPES.MEMORIES 1131 ); 1132 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 1133 1134 const nonSensitiveMemoriesList = await filterSensitiveMemories( 1135 engine, 1136 NEW_MEMORIES 1137 ); 1138 1139 // Check that the non-sensitive memories list contains only non-sensitive memories 1140 Assert.equal( 1141 nonSensitiveMemoriesList.length, 1142 3, 1143 "Non-sensitive memories list should contain 3 memories" 1144 ); 1145 Assert.ok( 1146 nonSensitiveMemoriesList.includes("Loves hiking and camping"), 1147 "Non-sensitive memories should include 'Loves hiking and camping'" 1148 ); 1149 Assert.ok( 1150 nonSensitiveMemoriesList.includes("Reads science fiction novels"), 1151 "Non-sensitive memories should include 'Reads science fiction novels'" 1152 ); 1153 Assert.ok( 1154 nonSensitiveMemoriesList.includes("Likes both dogs and cats"), 1155 "Non-sensitive memories should include 'Likes both dogs and cats'" 1156 ); 1157 } finally { 1158 sb.restore(); 1159 } 1160 }); 1161 1162 /** 1163 * Tests failed memories sensitivity filtering - Empty output 1164 */ 1165 add_task(async function test_filterSensitiveMemories_sad_path_empty_output() { 1166 const sb = sinon.createSandbox(); 1167 try { 1168 // LLM returns an empty non_sensitive_memories array 1169 const fakeEngine = { 1170 run() { 1171 return { 1172 finalOutput: `{ 1173 "non_sensitive_memories": [] 1174 }`, 1175 }; 1176 }, 1177 }; 1178 1179 // Check that the stub was called 1180 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 1181 const engine = await openAIEngine.build( 1182 MODEL_FEATURES.MEMORIES, 1183 DEFAULT_ENGINE_ID, 1184 SERVICE_TYPES.MEMORIES 1185 ); 1186 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 1187 1188 const nonSensitiveMemoriesList = await filterSensitiveMemories( 1189 engine, 1190 NEW_MEMORIES 1191 ); 1192 1193 Assert.ok( 1194 Array.isArray(nonSensitiveMemoriesList), 1195 "Should return an array" 1196 ); 1197 Assert.equal( 1198 nonSensitiveMemoriesList.length, 1199 0, 1200 "Should return an empty array" 1201 ); 1202 } finally { 1203 sb.restore(); 1204 } 1205 }); 1206 1207 /** 1208 * Tests failed memories sensitivity filtering - Wrong data type 1209 */ 1210 add_task( 1211 async function test_filterSensitiveMemories_sad_path_wrong_data_type() { 1212 const sb = sinon.createSandbox(); 1213 try { 1214 // LLM returns the wrong outer data type 1215 const fakeEngine = { 1216 run() { 1217 return { 1218 finalOutput: `testing`, 1219 }; 1220 }, 1221 }; 1222 1223 // Check that the stub was called 1224 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 1225 const engine = await openAIEngine.build( 1226 MODEL_FEATURES.MEMORIES, 1227 DEFAULT_ENGINE_ID, 1228 SERVICE_TYPES.MEMORIES 1229 ); 1230 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 1231 1232 const nonSensitiveMemoriesList = await filterSensitiveMemories( 1233 engine, 1234 NEW_MEMORIES 1235 ); 1236 1237 Assert.ok( 1238 Array.isArray(nonSensitiveMemoriesList), 1239 "Should return an array" 1240 ); 1241 Assert.equal( 1242 nonSensitiveMemoriesList.length, 1243 0, 1244 "Should return an empty array" 1245 ); 1246 } finally { 1247 sb.restore(); 1248 } 1249 } 1250 ); 1251 1252 /** 1253 * Tests failed memories sensitivity filtering - Wrong inner data type 1254 */ 1255 add_task( 1256 async function test_filterSensitiveMemories_sad_path_wrong_inner_data_type() { 1257 const sb = sinon.createSandbox(); 1258 try { 1259 // LLM returns a map with the non_sensitive_memories key, but its value's data type is wrong 1260 const fakeEngine = { 1261 run() { 1262 return { 1263 finalOutput: `{ 1264 "non_sensitive_memories": "testing" 1265 }`, 1266 }; 1267 }, 1268 }; 1269 1270 // Check that the stub was called 1271 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 1272 const engine = await openAIEngine.build( 1273 MODEL_FEATURES.MEMORIES, 1274 DEFAULT_ENGINE_ID, 1275 SERVICE_TYPES.MEMORIES 1276 ); 1277 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 1278 1279 const nonSensitiveMemoriesList = await filterSensitiveMemories( 1280 engine, 1281 NEW_MEMORIES 1282 ); 1283 1284 Assert.ok( 1285 Array.isArray(nonSensitiveMemoriesList), 1286 "Should return an array" 1287 ); 1288 Assert.equal( 1289 nonSensitiveMemoriesList.length, 1290 0, 1291 "Should return an empty array" 1292 ); 1293 } finally { 1294 sb.restore(); 1295 } 1296 } 1297 ); 1298 1299 /** 1300 * Tests failed memories sensitivity filtering - Wrong outer schema 1301 */ 1302 add_task( 1303 async function test_filterSensitiveMemories_sad_path_wrong_outer_schema() { 1304 const sb = sinon.createSandbox(); 1305 try { 1306 // LLM returns a map but with the wrong top-level key 1307 const fakeEngine = { 1308 run() { 1309 return { 1310 finalOutput: `{ 1311 "these_are_non_sensitive_memories": [ 1312 "testing1", "testing2", "testing3" 1313 ] 1314 }`, 1315 }; 1316 }, 1317 }; 1318 1319 // Check that the stub was called 1320 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 1321 const engine = await openAIEngine.build( 1322 MODEL_FEATURES.MEMORIES, 1323 DEFAULT_ENGINE_ID, 1324 SERVICE_TYPES.MEMORIES 1325 ); 1326 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 1327 1328 const nonSensitiveMemoriesList = await filterSensitiveMemories( 1329 engine, 1330 NEW_MEMORIES 1331 ); 1332 1333 Assert.ok( 1334 Array.isArray(nonSensitiveMemoriesList), 1335 "Should return an array" 1336 ); 1337 Assert.equal( 1338 nonSensitiveMemoriesList.length, 1339 0, 1340 "Should return an empty array" 1341 ); 1342 } finally { 1343 sb.restore(); 1344 } 1345 } 1346 ); 1347 1348 /** 1349 * Tests failed memories sensitivity filtering - Some correct inner schema 1350 */ 1351 add_task( 1352 async function test_filterSensitiveMemories_sad_path_some_correct_inner_schema() { 1353 const sb = sinon.createSandbox(); 1354 try { 1355 // LLM returns a map with the non_sensitive_memories key, but the inner schema has a mix of correct and incorrect data types 1356 const fakeEngine = { 1357 run() { 1358 return { 1359 finalOutput: `{ 1360 "non_sensitive_memories": [ 1361 "correct", 1362 12345, 1363 {"bad": "schema"} 1364 ] 1365 }`, 1366 }; 1367 }, 1368 }; 1369 1370 // Check that the stub was called 1371 const stub = sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine); 1372 const engine = await openAIEngine.build( 1373 MODEL_FEATURES.MEMORIES, 1374 DEFAULT_ENGINE_ID, 1375 SERVICE_TYPES.MEMORIES 1376 ); 1377 Assert.ok(stub.calledOnce, "_createEngine should be called once"); 1378 1379 const nonSensitiveMemoriesList = await filterSensitiveMemories( 1380 engine, 1381 NEW_MEMORIES 1382 ); 1383 1384 Assert.ok( 1385 Array.isArray(nonSensitiveMemoriesList), 1386 "Should return an array" 1387 ); 1388 Assert.equal( 1389 nonSensitiveMemoriesList.length, 1390 1, 1391 "Should return an array with one valid memory" 1392 ); 1393 Assert.equal( 1394 nonSensitiveMemoriesList[0], 1395 "correct", 1396 "Should return the single valid memory" 1397 ); 1398 } finally { 1399 sb.restore(); 1400 } 1401 } 1402 ); 1403 1404 /** 1405 * Tests mapping filtered memories back to full memory objects 1406 */ 1407 add_task(async function test_mapFilteredMemoriesToInitialList() { 1408 // Raw mock full memories object list 1409 const initialMemoriesList = [ 1410 // Imagined duplicate - should have been filtered out 1411 { 1412 category: "Pets & Animals", 1413 intent: "Buy / Acquire", 1414 memory_summary: "Buys dog food online", 1415 score: 4, 1416 }, 1417 // Sensitive content (stocks) - should have been filtered out 1418 { 1419 category: "News", 1420 intent: "Research / Learn", 1421 memory_summary: "Likes to invest in risky stocks", 1422 score: 5, 1423 }, 1424 { 1425 category: "Games", 1426 intent: "Entertain / Relax", 1427 memory_summary: "Enjoys strategy games", 1428 score: 3, 1429 }, 1430 ]; 1431 1432 // Mock list of good memories to keep 1433 const filteredMemoriesList = ["Enjoys strategy games"]; 1434 1435 const finalMemoriesList = await mapFilteredMemoriesToInitialList( 1436 initialMemoriesList, 1437 filteredMemoriesList 1438 ); 1439 1440 // Check that only the non-duplicate, non-sensitive memory remains 1441 Assert.equal( 1442 finalMemoriesList.length, 1443 1, 1444 "Final memories should contain 1 memory" 1445 ); 1446 Assert.equal( 1447 finalMemoriesList[0].category, 1448 "Games", 1449 "Final memory should have the correct category" 1450 ); 1451 Assert.equal( 1452 finalMemoriesList[0].intent, 1453 "Entertain / Relax", 1454 "Final memory should have the correct intent" 1455 ); 1456 Assert.equal( 1457 finalMemoriesList[0].memory_summary, 1458 "Enjoys strategy games", 1459 "Final memory should match the filtered memory" 1460 ); 1461 Assert.equal( 1462 finalMemoriesList[0].score, 1463 3, 1464 "Final memory should have the correct score" 1465 ); 1466 });