Files
wehub-resource-sync 555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:03:45 +08:00

1573 lines
64 KiB
Python

import json
import sys
from datetime import datetime
from unittest.mock import MagicMock, Mock, patch
import pytest
from mem0 import Memory
from mem0.configs.base import MemoryConfig
from mem0.memory.main import _entity_collection_name
from mem0.memory.utils import normalize_facts
class MockVectorMemory:
"""Mock memory object for testing incomplete payloads."""
def __init__(self, memory_id: str, payload: dict, score: float = 0.8):
self.id = memory_id
self.payload = payload
self.score = score
@pytest.fixture
def memory_client():
with patch.object(Memory, "__init__", return_value=None):
client = Memory()
client.add = MagicMock(return_value={"results": [{"id": "1", "memory": "Name is John Doe.", "event": "ADD"}]})
client.get = MagicMock(return_value={"id": "1", "memory": "Name is John Doe."})
client.update = MagicMock(return_value={"message": "Memory updated successfully!"})
client.delete = MagicMock(return_value={"message": "Memory deleted successfully!"})
client.history = MagicMock(return_value=[{"memory": "I like Indian food."}, {"memory": "I like Italian food."}])
client.get_all = MagicMock(return_value=["Name is John Doe.", "Name is John Doe. I like to code in Python."])
yield client
def test_create_memory(memory_client):
data = "Name is John Doe."
result = memory_client.add([{"role": "user", "content": data}], user_id="test_user")
assert result["results"][0]["memory"] == data
def test_entity_collection_name_uses_dash_for_s3_vectors():
assert _entity_collection_name("s3_vectors", "test-index") == "test-index-entities"
def test_entity_collection_name_keeps_underscore_for_other_stores():
assert _entity_collection_name("qdrant", "mem0") == "mem0_entities"
def test_get_memory(memory_client):
data = "Name is John Doe."
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
result = memory_client.get("1")
assert result["memory"] == data
def test_update_memory(memory_client):
data = "Name is John Doe."
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
new_data = "Name is John Kapoor."
update_result = memory_client.update("1", text=new_data)
assert update_result["message"] == "Memory updated successfully!"
def test_delete_memory(memory_client):
data = "Name is John Doe."
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
delete_result = memory_client.delete("1")
assert delete_result["message"] == "Memory deleted successfully!"
def test_history(memory_client):
data = "I like Indian food."
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
memory_client.update("1", text="I like Italian food.")
history = memory_client.history("1")
assert history[0]["memory"] == "I like Indian food."
assert history[1]["memory"] == "I like Italian food."
def test_list_memories(memory_client):
data1 = "Name is John Doe."
data2 = "Name is John Doe. I like to code in Python."
memory_client.add([{"role": "user", "content": data1}], user_id="test_user")
memory_client.add([{"role": "user", "content": data2}], user_id="test_user")
memories = memory_client.get_all(filters={"user_id": "test_user"})
assert data1 in memories
assert data2 in memories
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_collection_name_preserved_after_reset(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
test_collection_name = "mem0"
config = MemoryConfig()
config.vector_store.config.collection_name = test_collection_name
memory = Memory(config)
assert memory.collection_name == test_collection_name
assert memory.config.vector_store.config.collection_name == test_collection_name
memory.reset()
assert memory.collection_name == test_collection_name
assert memory.config.vector_store.config.collection_name == test_collection_name
reset_calls = [call for call in mock_vector_factory.call_args_list if len(mock_vector_factory.call_args_list) > 2]
if reset_calls:
reset_config = reset_calls[-1][0][1]
assert reset_config.collection_name == test_collection_name, f"Reset used wrong collection name: {reset_config.collection_name}"
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
def test_memory_reset_clears_messages_table(mock_llm_factory, mock_vector_factory, mock_embedder_factory, tmp_path):
"""Regression: Memory.reset() must clear the messages table, not just history."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_factory.return_value = MagicMock()
mock_llm_factory.return_value = MagicMock()
config = MemoryConfig()
config.history_db_path = str(tmp_path / "test.db")
memory = Memory(config)
memory.db.save_messages([{"role": "user", "content": "hello", "name": None}], "sess1")
memory.db.add_history(memory_id="m1", old_memory=None, new_memory="x", event="ADD")
memory.reset()
msg_count = memory.db.connection.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
hist_count = memory.db.connection.execute("SELECT COUNT(*) FROM history").fetchone()[0]
assert msg_count == 0, "messages table must be empty after Memory.reset()"
assert hist_count == 0, "history table must be empty after Memory.reset()"
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
async def test_async_memory_reset_clears_messages_table(mock_llm_factory, mock_vector_factory, mock_embedder_factory, tmp_path):
"""Regression: AsyncMemory.reset() must clear the messages table, not just history."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_factory.return_value = MagicMock()
mock_llm_factory.return_value = MagicMock()
from mem0 import AsyncMemory
config = MemoryConfig()
config.history_db_path = str(tmp_path / "test.db")
memory = AsyncMemory(config)
memory.db.save_messages([{"role": "user", "content": "hi", "name": None}], "s1")
memory.db.add_history(memory_id="m1", old_memory=None, new_memory="x", event="ADD")
await memory.reset()
msg_count = memory.db.connection.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
hist_count = memory.db.connection.execute("SELECT COUNT(*) FROM history").fetchone()[0]
assert msg_count == 0, "messages must be empty after AsyncMemory.reset()"
assert hist_count == 0, "history must be empty after AsyncMemory.reset()"
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_search_handles_incomplete_payloads(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Test that search operations handle memory objects with missing 'data' key gracefully."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
# Create test data with both complete and incomplete payloads
incomplete_memory = MockVectorMemory("mem_1", {"hash": "abc123"})
complete_memory = MockVectorMemory("mem_2", {"data": "content", "hash": "def456"})
mock_vector_store.search.return_value = [incomplete_memory, complete_memory]
mock_embedder = MagicMock()
mock_embedder.embed.return_value = [0.1, 0.2, 0.3]
memory.embedding_model = mock_embedder
result = memory._search_vector_store("test", {"user_id": "test"}, 10)
# v3 search pipeline skips entries where payload has no "data" key
assert len(result) == 1
assert result[0]["id"] == "mem_2"
assert result[0]["memory"] == "content"
@patch('mem0.memory.main.extract_entities', return_value=[])
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_search_explain_includes_score_details(
mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory, _mock_extract_entities
):
mock_embedder = MagicMock()
mock_embedder.embed.return_value = [0.1, 0.2, 0.3]
mock_embedder_factory.return_value = mock_embedder
mock_vector_store = MagicMock()
mock_vector_store.search.return_value = [
MockVectorMemory("mem_1", {"data": "content", "user_id": "test"}, score=0.8)
]
mock_vector_store.keyword_search.return_value = [
MockVectorMemory("mem_1", {"data": "content", "user_id": "test"}, score=5.0)
]
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
result = memory.search("test query", filters={"user_id": "test"}, explain=True)
details = result["results"][0]["score_details"]
assert details["semantic_score"] == 0.8
assert details["bm25_score"] > 0
assert details["entity_boost"] == 0.0
assert details["final_score"] == result["results"][0]["score"]
assert details["threshold"] == 0.1
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_get_all_handles_nested_list_from_chroma(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that get_all() handles nested list return from Chroma/Milvus.
Issue #3674: Some vector stores return [[mem1, mem2]] instead of [mem1, mem2]
This test ensures the unified unwrapping logic handles this correctly.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
# Create test data
mem1 = MockVectorMemory("mem_1", {"data": "My dog name is Sheru"})
mem2 = MockVectorMemory("mem_2", {"data": "I like to code in Python"})
mem3 = MockVectorMemory("mem_3", {"data": "I live in California"})
# Chroma/Milvus returns nested list: [[mem1, mem2, mem3]]
mock_vector_store.list.return_value = [[mem1, mem2, mem3]]
result = memory._get_all_from_vector_store({"user_id": "test"}, 100)
# Should successfully unwrap and return 3 memories
assert len(result) == 3
assert result[0]["memory"] == "My dog name is Sheru"
assert result[1]["memory"] == "I like to code in Python"
assert result[2]["memory"] == "I live in California"
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_get_all_handles_tuple_from_qdrant(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that get_all() handles tuple return from Qdrant.
Qdrant returns: ([mem1, mem2], count)
Should unwrap to [mem1, mem2]
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
mem1 = MockVectorMemory("mem_1", {"data": "Memory 1"})
mem2 = MockVectorMemory("mem_2", {"data": "Memory 2"})
# Qdrant returns tuple: ([mem1, mem2], count)
mock_vector_store.list.return_value = ([mem1, mem2], 100)
result = memory._get_all_from_vector_store({"user_id": "test"}, 100)
assert len(result) == 2
assert result[0]["memory"] == "Memory 1"
assert result[1]["memory"] == "Memory 2"
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_get_all_handles_flat_list_from_postgres(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that get_all() handles flat list return from PostgreSQL.
PostgreSQL returns: [mem1, mem2]
Should keep as-is without unwrapping
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
mem1 = MockVectorMemory("mem_1", {"data": "Memory 1"})
mem2 = MockVectorMemory("mem_2", {"data": "Memory 2"})
# PostgreSQL returns flat list: [mem1, mem2]
mock_vector_store.list.return_value = [mem1, mem2]
result = memory._get_all_from_vector_store({"user_id": "test"}, 100)
assert len(result) == 2
assert result[0]["memory"] == "Memory 1"
assert result[1]["memory"] == "Memory 2"
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_read_apis_surface_attributed_to(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
attributed_to is written to the payload on add (and the extraction prompt marks it
required), so get/get_all/search must return it instead of dropping it. It must be a
top-level field, not buried inside metadata.
"""
mock_embedder = MagicMock()
mock_embedder.embed.return_value = [0.1, 0.2, 0.3]
mock_embedder_factory.return_value = mock_embedder
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
memory.embedding_model = mock_embedder
payload = {"data": "User likes Python", "attributed_to": "user", "user_id": "u1"}
# get
mock_vector_store.get.return_value = MockVectorMemory("mem_1", payload)
got = memory.get("mem_1")
assert got["attributed_to"] == "user"
assert "attributed_to" not in (got.get("metadata") or {})
# get_all
mock_vector_store.list.return_value = [MockVectorMemory("mem_1", payload)]
listed = memory._get_all_from_vector_store({"user_id": "u1"}, 100)
assert listed[0]["attributed_to"] == "user"
assert "attributed_to" not in (listed[0].get("metadata") or {})
# search
mock_vector_store.search.return_value = [MockVectorMemory("mem_1", payload, score=0.9)]
mock_vector_store.keyword_search.return_value = []
searched = memory._search_vector_store("python", {"user_id": "u1"}, 10)
assert searched[0]["attributed_to"] == "user"
assert "attributed_to" not in (searched[0].get("metadata") or {})
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
async def test_async_read_apis_surface_attributed_to(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""AsyncMemory get/get_all/search must surface attributed_to, same as the sync path."""
mock_embedder = MagicMock()
mock_embedder.embed.return_value = [0.1, 0.2, 0.3]
mock_embedder_factory.return_value = mock_embedder
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
memory = AsyncMemory(MemoryConfig())
memory.embedding_model = mock_embedder
payload = {"data": "User likes Python", "attributed_to": "user", "user_id": "u1"}
# get
mock_vector_store.get.return_value = MockVectorMemory("mem_1", payload)
got = await memory.get("mem_1")
assert got["attributed_to"] == "user"
assert "attributed_to" not in (got.get("metadata") or {})
# get_all
mock_vector_store.list.return_value = [MockVectorMemory("mem_1", payload)]
listed = await memory._get_all_from_vector_store({"user_id": "u1"}, 100)
assert listed[0]["attributed_to"] == "user"
assert "attributed_to" not in (listed[0].get("metadata") or {})
# search
mock_vector_store.search.return_value = [MockVectorMemory("mem_1", payload, score=0.9)]
mock_vector_store.keyword_search.return_value = []
searched = await memory._search_vector_store("python", {"user_id": "u1"}, 10)
assert searched[0]["attributed_to"] == "user"
assert "attributed_to" not in (searched[0].get("metadata") or {})
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_add_infer_with_malformed_llm_facts(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Repro for: 'list' object has no attribute 'replace' on infer=true.
When an LLM (especially smaller models like llama3.1:8b) returns facts as
objects ({"fact": "..."} or {"text": "..."}) instead of plain strings,
the embedding model's .replace() call crashes with AttributeError.
"""
mock_embedder = MagicMock()
mock_embedder.embed.side_effect = lambda text, action: (_ for _ in ()).throw(
AttributeError("'dict' object has no attribute 'replace'")
) if not isinstance(text, str) else [0.1, 0.2, 0.3]
mock_embedder_factory.return_value = mock_embedder
mock_vector_store = MagicMock()
mock_vector_store.search.return_value = []
mock_vector_factory.return_value = mock_vector_store
# LLM returns malformed facts: dicts instead of strings
malformed_response = json.dumps({
"facts": [
{"fact": "User likes Python"},
{"text": "User is a developer"},
]
})
mock_llm = MagicMock()
mock_llm.generate_response.return_value = malformed_response
mock_llm_factory.return_value = mock_llm
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
# This should NOT raise AttributeError
memory._add_to_vector_store(
messages=[{"role": "user", "content": "I like Python and I'm a developer"}],
metadata={"user_id": "test_user"},
filters={"user_id": "test_user"},
infer=True,
)
def test_normalize_facts_plain_strings():
assert normalize_facts(["fact one", "fact two"]) == ["fact one", "fact two"]
def test_normalize_facts_dict_with_fact_key():
assert normalize_facts([{"fact": "User likes Python"}]) == ["User likes Python"]
def test_normalize_facts_dict_with_text_key():
assert normalize_facts([{"text": "User is a developer"}]) == ["User is a developer"]
def test_normalize_facts_mixed():
raw = [
"plain string",
{"fact": "from fact key"},
{"text": "from text key"},
]
assert normalize_facts(raw) == ["plain string", "from fact key", "from text key"]
def test_normalize_facts_filters_empty_strings():
assert normalize_facts(["", "valid", ""]) == ["valid"]
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_delete_nonexistent_memory_raises_error(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that delete() raises ValueError when memory_id does not exist
and does not attempt to delete from the vector store.
Issue #3849: memory.delete() fails with AttributeError when memory not found.
Should raise a clear ValueError instead.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
mock_vector_store.get.return_value = None
with pytest.raises(ValueError, match="Memory with id non-existent-id not found"):
memory.delete("non-existent-id")
mock_vector_store.delete.assert_not_called()
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
async def test_async_delete_nonexistent_memory_raises_error(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that async delete() raises ValueError when memory_id does not exist
and does not attempt to delete from the vector store.
Issue #3849: memory.delete() fails with AttributeError when memory not found.
Should raise a clear ValueError instead.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_store.get.return_value = None
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
with pytest.raises(ValueError, match="Memory with id non-existent-id not found"):
await memory.delete("non-existent-id")
mock_vector_store.delete.assert_not_called()
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_update_nonexistent_memory_raises_error(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that _update_memory() raises ValueError when memory_id does not exist.
Same class of bug as #3849 — vector_store.get() returns None and code
accesses .payload without a null check.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
mock_vector_store.get.return_value = None
with pytest.raises(ValueError, match="Memory with id non-existent-id not found"):
memory._update_memory("non-existent-id", "new data", {"new data": [0.1, 0.2]})
mock_vector_store.update.assert_not_called()
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
async def test_async_update_nonexistent_memory_raises_error(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that async _update_memory() raises ValueError when memory_id does not exist.
Same class of bug as #3849 — vector_store.get() returns None and code
accesses .payload without a null check.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_store.get.return_value = None
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
with pytest.raises(ValueError, match="Memory with id non-existent-id not found"):
await memory._update_memory("non-existent-id", "new data", {"new data": [0.1, 0.2]})
mock_vector_store.update.assert_not_called()
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_update_propagates_vector_store_failure(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""A backing-store failure while fetching the memory during update must
surface as the original error, not be masked as a 'provide a valid
memory_id' ValueError. The REST layer relies on this so an outage maps to
5xx instead of a misleading 4xx."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
mock_vector_store.get.side_effect = ConnectionError("vector store unreachable")
with pytest.raises(ConnectionError, match="vector store unreachable"):
memory._update_memory("mem-1", "new data", {"new data": [0.1, 0.2]})
mock_vector_store.update.assert_not_called()
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
async def test_async_update_propagates_vector_store_failure(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Async twin: a backing-store failure during update re-raises the original
error instead of masking it as a ValueError."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
mock_vector_store.get.side_effect = ConnectionError("vector store unreachable")
with pytest.raises(ConnectionError, match="vector store unreachable"):
await memory._update_memory("mem-1", "new data", {"new data": [0.1, 0.2]})
mock_vector_store.update.assert_not_called()
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_add_infer_false_embeds_once(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Regression test for issue #3723: adding with infer=False should not trigger duplicate embedding calls.
Root cause: _create_memory expected a dict for existing_embeddings but received a raw list[float],
causing the cache check `data in existing_embeddings` to always fail and trigger a redundant embed.
"""
embedder = MagicMock()
embedder.embed.return_value = [0.1, 0.2, 0.3]
embedder.config = MagicMock(embedding_dims=3)
mock_embedder_factory.return_value = embedder
mock_vector_store = MagicMock()
mock_vector_store.search.return_value = []
mock_vector_store.insert.return_value = None
mock_vector_store.get.return_value = None
telemetry_vector_store = MagicMock()
mock_vector_factory.side_effect = [mock_vector_store, telemetry_vector_store]
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
memory.add("foo", user_id="test_user", infer=False)
assert embedder.embed.call_count == 1
mock_vector_store.insert.assert_called_once()
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_add_infer_true_caches_embedding_on_llm_rewrite(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Regression test for issue #3723 (infer=True path): when the LLM rewrites a fact during the
ADD action, the embedding should be computed once and cached, not computed again inside _create_memory.
"""
embedder = MagicMock()
embedder.embed.return_value = [0.1, 0.2, 0.3]
embedder.config = MagicMock(embedding_dims=3)
mock_embedder_factory.return_value = embedder
mock_vector_store = MagicMock()
mock_vector_store.search.return_value = []
mock_vector_store.insert.return_value = None
mock_vector_store.get.return_value = None
telemetry_vector_store = MagicMock()
mock_vector_factory.side_effect = [mock_vector_store, telemetry_vector_store]
# V3 single-call extraction: LLM returns extracted memories directly
mock_llm = MagicMock()
mock_llm.generate_response.return_value = json.dumps(
{"memory": [{"text": "The user enjoys Python"}]}
)
mock_llm_factory.return_value = mock_llm
# embed_batch is used in Phase 3 for all extracted memories
embedder.embed_batch.return_value = [[0.4, 0.5, 0.6]]
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
memory.add("I like Python", user_id="test_user", infer=True)
# V3 pipeline: embed called once for search query (Phase 1),
# embed_batch called once for extracted memories (Phase 3)
assert embedder.embed.call_count == 1
assert embedder.embed_batch.call_count == 1
mock_vector_store.insert.assert_called_once()
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_update_infer_true_caches_embedding_on_llm_rewrite(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Regression test for issue #3723 (infer=True path): V3 is ADD-only, so this test verifies
that the single-call extraction pipeline embeds via embed_batch, not individual embed calls.
"""
embedder = MagicMock()
embedder.embed.return_value = [0.1, 0.2, 0.3]
embedder.config = MagicMock(embedding_dims=3)
mock_embedder_factory.return_value = embedder
# Existing memory that will be returned from search
existing_memory = MockVectorMemory(
memory_id="existing-mem-id",
payload={
"data": "User likes Python",
"hash": "abc123",
"created_at": "2025-01-01T00:00:00+00:00",
},
)
mock_vector_store = MagicMock()
mock_vector_store.search.return_value = [existing_memory]
mock_vector_store.get.return_value = existing_memory
mock_vector_store.insert.return_value = None
mock_vector_store.update.return_value = None
mock_vector_store.keyword_search.return_value = None
telemetry_vector_store = MagicMock()
mock_vector_factory.side_effect = [mock_vector_store, telemetry_vector_store]
# V3 single-call extraction: LLM returns extracted memories directly
mock_llm = MagicMock()
mock_llm.generate_response.return_value = json.dumps(
{"memory": [{"text": "The user loves Python"}]}
)
mock_llm_factory.return_value = mock_llm
# embed_batch is used in Phase 3 for all extracted memories
embedder.embed_batch.return_value = [[0.4, 0.5, 0.6]]
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
memory.add("I love Python now", user_id="test_user", infer=True)
# V3 pipeline: embed called once for search query (Phase 1),
# embed_batch called once for extracted memories (Phase 3)
assert embedder.embed.call_count == 1
assert embedder.embed_batch.call_count == 1
mock_vector_store.insert.assert_called_once()
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.main.SQLiteManager')
def test_delete_memory_history_has_timestamps(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that deleting a memory records created_at and updated_at in history.
Ensures DELETE operations preserve the original creation timestamp
and record the deletion time for proper audit trails.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
existing_memory = MagicMock()
existing_memory.payload = {
"data": "I like Python.",
"created_at": "2024-01-01T00:00:00+00:00",
"actor_id": None,
"role": None,
}
mock_vector_store.get.return_value = existing_memory
memory.delete("mem-123")
call_kwargs = memory.db.add_history.call_args.kwargs
assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00"
assert call_kwargs["updated_at"] is not None
datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.main.SQLiteManager')
def test_delete_memory_normalizes_non_utc_created_at(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Test that non-UTC created_at timestamps are normalized to UTC on delete."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
existing_memory = MagicMock()
existing_memory.payload = {
"data": "I like Python.",
"created_at": "2024-01-01T05:00:00+05:00", # UTC+5
"actor_id": None,
"role": None,
}
mock_vector_store.get.return_value = existing_memory
memory.delete("mem-123")
call_kwargs = memory.db.add_history.call_args.kwargs
assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00" # normalized to UTC
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.main.SQLiteManager')
def test_delete_memory_missing_created_at(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Test that delete works when created_at is absent from the payload (pre-existing memories)."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
existing_memory = MagicMock()
existing_memory.payload = {
"data": "I like Python.",
"actor_id": None,
"role": None,
}
mock_vector_store.get.return_value = existing_memory
memory.delete("mem-123")
call_kwargs = memory.db.add_history.call_args.kwargs
assert call_kwargs["created_at"] is None
assert call_kwargs["updated_at"] is not None
datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.main.SQLiteManager')
async def test_async_delete_memory_history_has_timestamps(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that async deleting a memory records created_at and updated_at in history.
Ensures async DELETE operations preserve the original creation timestamp
and record the deletion time for proper audit trails.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
existing_memory = MagicMock()
existing_memory.payload = {
"data": "I like Python.",
"created_at": "2024-01-01T00:00:00+00:00",
"actor_id": None,
"role": None,
}
mock_vector_store.get.return_value = existing_memory
await memory.delete("mem-123")
call_kwargs = memory.db.add_history.call_args.kwargs
assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00"
assert call_kwargs["updated_at"] is not None
datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.main.SQLiteManager')
async def test_async_delete_all_continues_on_partial_failure(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""async delete_all must not abort when a single memory fails to delete.
Without return_exceptions=True, asyncio.gather raises on the first error
and cancels remaining tasks, leaving a partial deletion.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
mem1 = MagicMock()
mem1.id = "mem-1"
mem1.payload = {"data": "one", "created_at": "2024-01-01T00:00:00+00:00", "actor_id": None, "role": None}
mem2 = MagicMock()
mem2.id = "mem-2"
mem2.payload = {"data": "two", "created_at": "2024-01-01T00:00:00+00:00", "actor_id": None, "role": None}
mem3 = MagicMock()
mem3.id = "mem-3"
mem3.payload = {"data": "three", "created_at": "2024-01-01T00:00:00+00:00", "actor_id": None, "role": None}
mock_vector_store.list.return_value = ([mem1, mem2, mem3],)
def _get_side_effect(vector_id):
if vector_id == "mem-2":
raise RuntimeError("simulated store failure")
return {
"mem-1": mem1,
"mem-3": mem3,
}.get(vector_id)
mock_vector_store.get.side_effect = _get_side_effect
result = await memory.delete_all(user_id="test-user")
assert result == {"message": "Memories deleted successfully!"}
assert mock_vector_store.delete.call_count == 2
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
class TestProcessMetadataFiltersMerge:
"""Regression tests for issue #3952: multiple operators on the same key must be merged."""
def _make_memory(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
mock_embedder_factory.return_value = MagicMock()
mock_vector_factory.return_value = MagicMock()
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
return MemoryClass(MemoryConfig())
def test_multiple_operators_same_key_merged(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Filters like created_at: {gte: X, lte: Y} must preserve both operators."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"created_at": {"gte": 1000, "lte": 2000}
})
assert result == {"created_at": {"gte": 1000, "lte": 2000}}
def test_single_operator_still_works(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Single operator filters must continue to work."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"created_at": {"gte": 1000}
})
assert result == {"created_at": {"gte": 1000}}
def test_multiple_keys_with_multiple_operators(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Multiple keys each with multiple operators."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"created_at": {"gte": 1000, "lte": 2000},
"score": {"gt": 0.5, "lt": 0.9},
})
assert result == {
"created_at": {"gte": 1000, "lte": 2000},
"score": {"gt": 0.5, "lt": 0.9},
}
def test_and_same_key_different_operators_merged(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""AND with same key in separate conditions must merge operators (issue #4850)."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"AND": [{"price": {"gt": 10}}, {"price": {"lt": 20}}]
})
assert result == {"price": {"gt": 10, "lt": 20}}
def test_and_same_key_three_operators_merged(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""AND with three conditions on the same key must merge all operators."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"AND": [{"price": {"gte": 5}}, {"price": {"lte": 100}}, {"price": {"ne": 50}}]
})
assert result == {"price": {"gte": 5, "lte": 100, "ne": 50}}
def test_and_mixed_keys_with_same_key_overlap(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""AND with a mix of same-key and different-key conditions."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"AND": [{"price": {"gt": 10}}, {"category": "electronics"}, {"price": {"lt": 20}}]
})
assert result == {"price": {"gt": 10, "lt": 20}, "category": "electronics"}
def test_and_simple_equality_no_merge(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""AND with simple equality values on the same key — last value wins."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"AND": [{"status": "active"}, {"status": "pending"}]
})
assert result == {"status": "pending"}
# --- Issue #3040: reset() should clean up graph database ---
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_reset_skips_graph_when_graph_disabled(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Test that reset() does NOT call graph.reset() when graph is disabled."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
config = MemoryConfig()
memory = Memory(config)
# Graph is disabled by default (graph is None)
memory.graph = None
memory.reset()
# graph should remain None after reset
assert memory.graph is None
# ─── Entity Param Rejection Tests ─────────────────────────────────────────────
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_search_rejects_user_id_kwarg(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""search() should reject user_id as top-level kwarg."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_factory.return_value = MagicMock()
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
config = MemoryConfig()
memory = Memory(config)
with pytest.raises(ValueError, match=r"user_id.*filters"):
memory.search("test query", user_id="u1")
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_get_all_rejects_user_id_kwarg(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""get_all() should reject user_id as top-level kwarg."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_factory.return_value = MagicMock()
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
config = MemoryConfig()
memory = Memory(config)
with pytest.raises(ValueError, match=r"user_id.*filters"):
memory.get_all(user_id="u1")
# ─── Regression: AsyncMemory._create_memory must store text_lemmatized ─────────
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_sync_create_memory_stores_text_lemmatized(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Sync Memory._create_memory must include text_lemmatized in payload for BM25 keyword search."""
embedder = MagicMock()
embedder.embed.return_value = [0.1, 0.2, 0.3]
mock_embedder_factory.return_value = embedder
mock_vector_store = MagicMock()
mock_vector_store.insert.return_value = None
telemetry_vector_store = MagicMock()
mock_vector_factory.side_effect = [mock_vector_store, telemetry_vector_store]
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
data = "I love hiking in the mountains"
embeddings = {data: [0.1, 0.2, 0.3]}
metadata = {"user_id": "test_user"}
memory._create_memory(data, embeddings, metadata)
# Check that text_lemmatized was stored in the payload
insert_call = mock_vector_store.insert.call_args
payload = insert_call.kwargs.get("payloads") or insert_call[1].get("payloads")
assert payload is not None and len(payload) == 1
assert "text_lemmatized" in payload[0], "Sync _create_memory must store text_lemmatized for BM25"
assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty"
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
async def test_async_create_memory_stores_text_lemmatized(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Regression test: AsyncMemory._create_memory must include text_lemmatized
in the vector store payload.
Without text_lemmatized, memories created via AsyncMemory with infer=False
are invisible to BM25 keyword search, silently degrading search recall for
all async users.
"""
embedder = MagicMock()
embedder.embed.return_value = [0.1, 0.2, 0.3]
mock_embedder_factory.return_value = embedder
mock_vector_store = MagicMock()
mock_vector_store.insert.return_value = None
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
memory = AsyncMemory(MemoryConfig())
data = "I love hiking in the mountains"
embeddings = {data: [0.1, 0.2, 0.3]}
metadata = {"user_id": "test_user"}
await memory._create_memory(data, embeddings, metadata)
# Check that text_lemmatized was stored in the payload
insert_call = mock_vector_store.insert.call_args
payload = insert_call.kwargs.get("payloads") or insert_call[1].get("payloads")
assert payload is not None and len(payload) == 1
assert "text_lemmatized" in payload[0], (
"AsyncMemory._create_memory must store text_lemmatized for BM25 keyword search"
)
assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty"
class TestHybridSearchWarning:
"""Warn at init when vector store does not support keyword_search."""
@patch("mem0.memory.telemetry.capture_event")
@patch("mem0.memory.main.SQLiteManager")
@patch("mem0.utils.factory.LlmFactory.create")
@patch("mem0.utils.factory.EmbedderFactory.create")
@patch("mem0.utils.factory.VectorStoreFactory.create")
def test_warning_for_store_without_keyword_search(
self, mock_vs_factory, mock_emb, mock_llm, mock_sqlite, _cap, caplog
):
from mem0.vector_stores.base import VectorStoreBase
import logging
class StoreWithoutKeywordSearch(VectorStoreBase):
def create_col(self, *a, **kw): pass
def insert(self, *a, **kw): pass
def search(self, *a, **kw): return []
def delete(self, *a, **kw): pass
def update(self, *a, **kw): pass
def get(self, *a, **kw): pass
def list_cols(self): return []
def delete_col(self): pass
def col_info(self): return {}
def list(self, *a, **kw): return []
def reset(self): pass
mock_vs_factory.return_value = StoreWithoutKeywordSearch()
mock_emb.return_value = MagicMock()
mock_llm.return_value = MagicMock()
config = MemoryConfig()
config.vector_store.provider = "chroma"
with caplog.at_level(logging.WARNING, logger="mem0.memory.main"):
Memory(config)
assert any("does not support keyword search" in r.message for r in caplog.records)
@patch("mem0.memory.telemetry.capture_event")
@patch("mem0.memory.main.SQLiteManager")
@patch("mem0.utils.factory.LlmFactory.create")
@patch("mem0.utils.factory.EmbedderFactory.create")
@patch("mem0.utils.factory.VectorStoreFactory.create")
def test_no_warning_for_store_with_keyword_search(
self, mock_vs_factory, mock_emb, mock_llm, mock_sqlite, _cap, caplog
):
from mem0.vector_stores.base import VectorStoreBase
import logging
class StoreWithKeywordSearch(VectorStoreBase):
def keyword_search(self, query, top_k=5, filters=None):
return []
def create_col(self, *a, **kw): pass
def insert(self, *a, **kw): pass
def search(self, *a, **kw): return []
def delete(self, *a, **kw): pass
def update(self, *a, **kw): pass
def get(self, *a, **kw): pass
def list_cols(self): return []
def delete_col(self): pass
def col_info(self): return {}
def list(self, *a, **kw): return []
def reset(self): pass
mock_vs_factory.return_value = StoreWithKeywordSearch()
mock_emb.return_value = MagicMock()
mock_llm.return_value = MagicMock()
config = MemoryConfig()
with caplog.at_level(logging.WARNING, logger="mem0.memory.main"):
Memory(config)
assert not any("does not support keyword search" in r.message for r in caplog.records)
class TestPreserveCustomMetadata:
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_update_preserves_custom_metadata(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
existing_payload = {
"data": "I love playing tennis",
"hash": "abc123",
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00",
"user_id": "user_1",
"category": "hobbies",
"priority": "high",
"source": "chat",
}
mock_vector_store.get.return_value = MockVectorMemory("mem-1", existing_payload)
embedder = MagicMock()
embedder.embed.return_value = [0.1, 0.2, 0.3]
mock_embedder_factory.return_value = embedder
config = MemoryConfig()
memory = Memory(config)
memory._update_memory("mem-1", "I love playing tennis and swimming", {"I love playing tennis and swimming": [0.1, 0.2, 0.3]})
call_args = mock_vector_store.update.call_args
payload = call_args.kwargs.get("payload") or call_args[1].get("payload")
assert payload["category"] == "hobbies"
assert payload["priority"] == "high"
assert payload["source"] == "chat"
assert payload["data"] == "I love playing tennis and swimming"
assert payload["user_id"] == "user_1"
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_update_with_new_metadata_overrides_existing(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
existing_payload = {
"data": "I love playing tennis",
"hash": "abc123",
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00",
"user_id": "user_1",
"category": "hobbies",
"priority": "high",
}
mock_vector_store.get.return_value = MockVectorMemory("mem-1", existing_payload)
config = MemoryConfig()
memory = Memory(config)
memory._update_memory(
"mem-1", "Updated text",
{"Updated text": [0.1, 0.2, 0.3]},
metadata={"priority": "low", "new_field": "value"},
)
call_args = mock_vector_store.update.call_args
payload = call_args.kwargs.get("payload") or call_args[1].get("payload")
assert payload["category"] == "hobbies"
assert payload["priority"] == "low"
assert payload["new_field"] == "value"
assert payload["data"] == "Updated text"
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_update_preserves_actor_id_from_original(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
existing_payload = {
"data": "I am player #1",
"hash": "abc123",
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00",
"user_id": "team",
"actor_id": "Alice",
}
mock_vector_store.get.return_value = MockVectorMemory("mem-1", existing_payload)
config = MemoryConfig()
memory = Memory(config)
memory._update_memory(
"mem-1", "Player #1 is great",
{"Player #1 is great": [0.1, 0.2, 0.3]},
metadata={"user_id": "team", "actor_id": "Bob"},
)
call_args = mock_vector_store.update.call_args
payload = call_args.kwargs.get("payload") or call_args[1].get("payload")
assert payload["actor_id"] == "Alice"
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
async def test_async_update_preserves_custom_metadata(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
existing_payload = {
"data": "I love playing tennis",
"hash": "abc123",
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00",
"user_id": "user_1",
"category": "hobbies",
"source": "chat",
}
mock_vector_store.get.return_value = MockVectorMemory("mem-1", existing_payload)
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
await memory._update_memory("mem-1", "I love swimming", {"I love swimming": [0.1, 0.2, 0.3]})
call_args = mock_vector_store.update.call_args
payload = call_args.kwargs.get("payload") or call_args[1].get("payload")
assert payload["category"] == "hobbies"
assert payload["source"] == "chat"
assert payload["data"] == "I love swimming"
assert payload["user_id"] == "user_1"
class TestAsyncDeleteAllEntityRace:
"""Tests for async delete_all entity store race condition fix."""
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
async def test_async_delete_all_bulk_clears_entity_store(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Verify that async delete_all bulk-clears entity records after
concurrent memory deletes complete, preventing both the
read-modify-write race and entity orphaning on partial failures.
"""
mock_embedder_factory.return_value = MagicMock()
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
mock_vector_store = MagicMock()
mem_a = MagicMock()
mem_a.id = "mem-a"
mem_a.payload = {"data": "Alice likes Python", "user_id": "alice"}
mem_b = MagicMock()
mem_b.id = "mem-b"
mem_b.payload = {"data": "Alice works at Acme", "user_id": "alice"}
mock_vector_store.list.return_value = ([mem_a, mem_b],)
mock_vector_store.get.side_effect = lambda vector_id: {"mem-a": mem_a, "mem-b": mem_b}[vector_id]
mock_vector_factory.return_value = mock_vector_store
mock_entity_store = MagicMock()
entity_row = MagicMock()
entity_row.id = "entity-alice"
entity_row.payload = {
"data": "alice",
"user_id": "alice",
"linked_memory_ids": ["mem-a", "mem-b"],
}
mock_entity_store.list.return_value = ([entity_row],)
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
memory._entity_store = mock_entity_store
await memory.delete_all(user_id="alice")
mock_entity_store.delete.assert_called_once_with(vector_id="entity-alice")
assert mock_vector_store.delete.call_count == 2
@pytest.mark.asyncio
@patch("mem0.memory.main.VectorStoreFactory")
@patch("mem0.memory.main.EmbedderFactory")
@patch("mem0.memory.main.LlmFactory")
async def test_async_procedural_memory_langchain_strips_code_blocks(mock_llm_factory, mock_emb, mock_vs):
"""Regression #5710: async LangChain path must call remove_code_blocks()."""
mock_vs.return_value = MagicMock()
mock_emb.return_value = MagicMock()
mock_emb.return_value.embed.return_value = [0.1] * 1536
mock_llm_factory.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
memory.vector_store = MagicMock()
memory.vector_store.insert = MagicMock()
mock_langchain_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = '```json\n{"key": "value"}\n```'
mock_langchain_llm.invoke.return_value = mock_response
messages = [{"role": "user", "content": "test"}]
metadata = {"user_id": "test_user"}
await memory._create_procedural_memory(messages, metadata=metadata, llm=mock_langchain_llm)
insert_call = memory.vector_store.insert.call_args
stored_data = insert_call[1]["payloads"][0]["data"]
assert "```" not in stored_data
@pytest.mark.asyncio
@patch("mem0.memory.main.VectorStoreFactory")
@patch("mem0.memory.main.EmbedderFactory")
@patch("mem0.memory.main.LlmFactory")
async def test_async_procedural_memory_default_path_without_langchain(mock_llm_factory, mock_emb, mock_vs):
"""Async procedural memory must not require langchain-core on the default
(llm=None) path, which uses self.llm and never calls convert_to_messages.
The sync path already works without it; this keeps the async path in parity.
"""
mock_vs.return_value = MagicMock()
mock_emb.return_value = MagicMock()
mock_emb.return_value.embed.return_value = [0.1] * 1536
mock_llm_factory.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
memory.vector_store = MagicMock()
memory.vector_store.insert = MagicMock()
memory.embedding_model.embed = Mock(return_value=[0.1] * 1536)
memory.llm.generate_response = Mock(return_value="- deploy with the release script")
messages = [{"role": "user", "content": "how do we deploy"}]
# Simulate langchain-core being unavailable; the default path must still work.
with patch.dict(
sys.modules,
{
"langchain_core": None,
"langchain_core.messages": None,
"langchain_core.messages.utils": None,
},
):
result = await memory._create_procedural_memory(messages, metadata={"agent_id": "agent_1"})
assert result["results"][0]["event"] == "ADD"
memory.llm.generate_response.assert_called_once()