"""
Tests for the user-specific persistent memory store.
These tests validate that UserMemoryStore:
- Stores persistent memories in a dedicated Chroma-backed collection
- Retrieves relevant memories for a given user
- Isolates one user's memories from another user's memories
- Validates empty inputs
- Enforces the config/config_path initialization guardrail
- Automatically adds created_at timestamp metadata
To run this test, enter the following code in the terminal:
PYTHONPATH=src pytest tests/test_user_memory_store.py -q -s
"""
from __future__ import annotations
from typing import Iterator
import pytest
from llama_index.core import Settings
from llama_index.core.embeddings import MockEmbedding
from walkxr_ai.rag.user_memory_store import UserMemoryStore
# Define a fixture saves the original embedding model and replaces it with a mock for
# testing, then restores the original after tests are complete
@pytest.fixture(autouse=True)
def configure_test_embed_model() -> Iterator[None]:
"""
Configure a mock embedding model so memory tests run offline and deterministically.
"""
original_embed_model = Settings._embed_model
Settings.embed_model = MockEmbedding(embed_dim=8)
try:
yield
finally:
Settings._embed_model = original_embed_model
def test_add_and_query_single_user(tmp_path) -> None:
"""
Test #1:
Verify that a memory can be added for one user and later retrieved by a relevant query.
Input:
tmp_path: pytest fixture providing a temporary directory for the vector store to ensure test isolation.
"""
# Store Chroma inside the temp directory called test_user_memory_store
# When retrieving, default to the top 3 most similar results
config = {
"storage": {
"persist_dir": str(tmp_path),
"user_memory_collection_name": "test_user_memory_store",
},
"memory_retrieval": {
"similarity_top_k": 3,
},
}
# Create the UserMemoryStore with the test config, add a memory for user_1, and query it
store = UserMemoryStore(config=config)
store.add_memory(user_id="user_1", memory_text="I enjoy long walks by the lake.")
results = store.query_memories(user_id="user_1", query="lake walks")
# Verify that at least one relevant result comes back
assert len(results) >= 1
def test_user_isolation(tmp_path) -> None:
"""
Test #2:
Verify that retrieval for one user does not return memories stored for a different user.
"""
config = {
"storage": {
"persist_dir": str(tmp_path),
"user_memory_collection_name": "test_user_memory_store",
},
"memory_retrieval": {
"similarity_top_k": 3,
},
}
# Add memory for two different users within the same collection, then query only for user_1
store = UserMemoryStore(config=config)
store.add_memory(user_id="user_1", memory_text="I love hiking")
store.add_memory(user_id="user_2", memory_text="I hate hiking")
results = store.query_memories(user_id="user_1", query="hiking")
# Verify that results are returned only for user_1 and not user_2
assert results
assert all(result.node.metadata["user_id"] == "user_1" for result in results)
assert all(result.node.get_text() != "I hate hiking" for result in results)
def test_empty_inputs_validation(tmp_path) -> None:
"""
Test #3:
Verify that empty user IDs, memory text, and query strings raise clear errors.
"""
config = {
"storage": {
"persist_dir": str(tmp_path),
"user_memory_collection_name": "test_user_memory_store",
},
"memory_retrieval": {
"similarity_top_k": 3,
},
}
store = UserMemoryStore(config=config)
# Expect ValueErrors with specific messages for empty user_id, memory_text, and query inputs
with pytest.raises(ValueError, match="user_id must be a non-empty string"):
store.add_memory(user_id="", memory_text="Valid memory")
with pytest.raises(ValueError, match="memory_text must be a non-empty string"):
store.add_memory(user_id="user_1", memory_text="")
with pytest.raises(ValueError, match="query must be a non-empty string"):
store.query_memories(user_id="user_1", query="")
def test_config_guardrail(tmp_path) -> None:
"""
Test #4:
Verify that initialization fails when both config and config_path are provided.
"""
config = {
"storage": {
"persist_dir": str(tmp_path),
"user_memory_collection_name": "test_user_memory_store",
},
"memory_retrieval": {
"similarity_top_k": 3,
},
}
# Expect a ValueError if both config and config_path are provided to the constructor
with pytest.raises(ValueError, match="Provide either 'config' or 'config_path'"):
UserMemoryStore(config=config, config_path="unused.yaml")
def test_timestamp_metadata_added(tmp_path) -> None:
"""
Test #5:
Verify that add_memory automatically attaches created_at metadata when none is provided.
"""
config = {
"storage": {
"persist_dir": str(tmp_path),
"user_memory_collection_name": "test_user_memory_store",
},
"memory_retrieval": {
"similarity_top_k": 3,
},
}
# Add a memory without explicitly passing created_at metadata
store = UserMemoryStore(config=config)
store.add_memory(user_id="user_1", memory_text="I prefer tea in the morning.")
results = store.query_memories(user_id="user_1", query="tea")
# Then verify that created_at metadata was automatically added and exists in the retrieved result
assert results
assert "created_at" in results[0].node.metadata