from unittest.mock import Mock, patch import pytest from mem0.configs.vector_stores.chroma import ChromaDbConfig from mem0.vector_stores.chroma import ChromaDB @pytest.fixture def mock_chromadb_client(): with patch("chromadb.Client") as mock_client: yield mock_client @pytest.fixture def chromadb_instance(mock_chromadb_client): mock_collection = Mock() mock_chromadb_client.return_value.get_or_create_collection.return_value = mock_collection return ChromaDB(collection_name="test_collection", client=mock_chromadb_client.return_value) def test_insert_vectors(chromadb_instance, mock_chromadb_client): vectors = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]] payloads = [{"name": "vector1"}, {"name": "vector2"}] ids = ["id1", "id2"] chromadb_instance.insert(vectors=vectors, payloads=payloads, ids=ids) chromadb_instance.collection.add.assert_called_once_with(ids=ids, embeddings=vectors, metadatas=payloads) def test_search_vectors(chromadb_instance, mock_chromadb_client): mock_result = { "ids": [["id1", "id2"]], "distances": [[0.1, 0.2]], "metadatas": [[{"name": "vector1"}, {"name": "vector2"}]], } chromadb_instance.collection.query.return_value = mock_result vectors = [[0.1, 0.2, 0.3]] results = chromadb_instance.search(query="", vectors=vectors, top_k=2) chromadb_instance.collection.query.assert_called_once_with(query_embeddings=vectors, where=None, n_results=2) assert len(results) == 2 assert results[0].id == "id1" assert results[0].score == pytest.approx(1.0 / 1.1) assert results[0].payload == {"name": "vector1"} def test_search_vectors_with_filters(chromadb_instance, mock_chromadb_client): """Test search with agent_id and run_id filters.""" mock_result = { "ids": [["id1"]], "distances": [[0.1]], "metadatas": [[{"name": "vector1", "user_id": "alice", "agent_id": "agent1", "run_id": "run1"}]], } chromadb_instance.collection.query.return_value = mock_result vectors = [[0.1, 0.2, 0.3]] filters = {"user_id": "alice", "agent_id": "agent1", "run_id": "run1"} results = chromadb_instance.search(query="", vectors=vectors, top_k=2, filters=filters) # Verify that _generate_where_clause was called with the filters expected_where = {"$and": [{"user_id": {"$eq": "alice"}}, {"agent_id": {"$eq": "agent1"}}, {"run_id": {"$eq": "run1"}}]} chromadb_instance.collection.query.assert_called_once_with( query_embeddings=vectors, where=expected_where, n_results=2 ) assert len(results) == 1 assert results[0].id == "id1" assert results[0].payload["user_id"] == "alice" assert results[0].payload["agent_id"] == "agent1" assert results[0].payload["run_id"] == "run1" def test_search_vectors_with_single_filter(chromadb_instance, mock_chromadb_client): """Test search with single filter (should not use $and).""" mock_result = { "ids": [["id1"]], "distances": [[0.1]], "metadatas": [[{"name": "vector1", "user_id": "alice"}]], } chromadb_instance.collection.query.return_value = mock_result vectors = [[0.1, 0.2, 0.3]] filters = {"user_id": "alice"} results = chromadb_instance.search(query="", vectors=vectors, top_k=2, filters=filters) # Verify that single filter is passed with $eq operator expected_where = {"user_id": {"$eq": "alice"}} chromadb_instance.collection.query.assert_called_once_with( query_embeddings=vectors, where=expected_where, n_results=2 ) assert len(results) == 1 assert results[0].payload["user_id"] == "alice" def test_search_vectors_with_no_filters(chromadb_instance, mock_chromadb_client): """Test search with no filters.""" mock_result = { "ids": [["id1"]], "distances": [[0.1]], "metadatas": [[{"name": "vector1"}]], } chromadb_instance.collection.query.return_value = mock_result vectors = [[0.1, 0.2, 0.3]] results = chromadb_instance.search(query="", vectors=vectors, top_k=2, filters=None) chromadb_instance.collection.query.assert_called_once_with( query_embeddings=vectors, where=None, n_results=2 ) assert len(results) == 1 def test_delete_vector(chromadb_instance): vector_id = "id1" chromadb_instance.delete(vector_id=vector_id) chromadb_instance.collection.delete.assert_called_once_with(ids=[vector_id]) def test_update_vector(chromadb_instance): vector_id = "id1" new_vector = [0.7, 0.8, 0.9] new_payload = {"name": "updated_vector"} chromadb_instance.update(vector_id=vector_id, vector=new_vector, payload=new_payload) chromadb_instance.collection.update.assert_called_once_with( ids=[vector_id], embeddings=[new_vector], metadatas=[new_payload] ) def test_update_vector_metadata_only(chromadb_instance): # Metadata-only update (vector=None) must not wrap None in a list. vector_id = "id1" new_payload = {"name": "updated_vector"} chromadb_instance.update(vector_id=vector_id, vector=None, payload=new_payload) chromadb_instance.collection.update.assert_called_once_with( ids=[vector_id], embeddings=None, metadatas=[new_payload] ) def test_update_vector_embedding_only(chromadb_instance): # Vector-only update (payload=None) must not wrap None in a list. vector_id = "id1" new_vector = [0.7, 0.8, 0.9] chromadb_instance.update(vector_id=vector_id, vector=new_vector, payload=None) chromadb_instance.collection.update.assert_called_once_with( ids=[vector_id], embeddings=[new_vector], metadatas=None ) def test_get_vector(chromadb_instance): mock_result = { "ids": [["id1"]], "distances": [[0.1]], "metadatas": [[{"name": "vector1"}]], } chromadb_instance.collection.get.return_value = mock_result result = chromadb_instance.get(vector_id="id1") chromadb_instance.collection.get.assert_called_once_with(ids=["id1"]) assert result.id == "id1" assert result.score == pytest.approx(1.0 / 1.1) assert result.payload == {"name": "vector1"} def test_get_missing_vector_returns_none(chromadb_instance): # Chroma returns empty lists for an unknown id; get() must return None # rather than raising IndexError (parity with qdrant/pgvector/faiss). chromadb_instance.collection.get.return_value = {"ids": [], "metadatas": []} result = chromadb_instance.get(vector_id="does-not-exist") assert result is None def test_list_vectors(chromadb_instance): mock_result = { "ids": [["id1", "id2"]], "distances": [[0.1, 0.2]], "metadatas": [[{"name": "vector1"}, {"name": "vector2"}]], } chromadb_instance.collection.get.return_value = mock_result results = chromadb_instance.list(top_k=2) chromadb_instance.collection.get.assert_called_once_with(where=None, limit=2) assert len(results[0]) == 2 assert results[0][0].id == "id1" assert results[0][1].id == "id2" def test_list_vectors_with_filters(chromadb_instance): """Test list with agent_id and run_id filters.""" mock_result = { "ids": [["id1"]], "distances": [[0.1]], "metadatas": [[{"name": "vector1", "user_id": "alice", "agent_id": "agent1", "run_id": "run1"}]], } chromadb_instance.collection.get.return_value = mock_result filters = {"user_id": "alice", "agent_id": "agent1", "run_id": "run1"} results = chromadb_instance.list(filters=filters, top_k=2) # Verify that _generate_where_clause was called with the filters expected_where = {"$and": [{"user_id": {"$eq": "alice"}}, {"agent_id": {"$eq": "agent1"}}, {"run_id": {"$eq": "run1"}}]} chromadb_instance.collection.get.assert_called_once_with(where=expected_where, limit=2) assert len(results[0]) == 1 assert results[0][0].payload["user_id"] == "alice" assert results[0][0].payload["agent_id"] == "agent1" assert results[0][0].payload["run_id"] == "run1" def test_list_vectors_with_single_filter(chromadb_instance): """Test list with single filter (should not use $and).""" mock_result = { "ids": [["id1"]], "distances": [[0.1]], "metadatas": [[{"name": "vector1", "user_id": "alice"}]], } chromadb_instance.collection.get.return_value = mock_result filters = {"user_id": "alice"} results = chromadb_instance.list(filters=filters, top_k=2) # Verify that single filter is passed with $eq operator expected_where = {"user_id": {"$eq": "alice"}} chromadb_instance.collection.get.assert_called_once_with(where=expected_where, limit=2) assert len(results[0]) == 1 assert results[0][0].payload["user_id"] == "alice" def test_generate_where_clause_multiple_filters(): """Test _generate_where_clause with multiple filters.""" filters = {"user_id": "alice", "agent_id": "agent1", "run_id": "run1"} result = ChromaDB._generate_where_clause(filters) # ChromaDB accepts filters in {"$and": [{"field": {"$eq": "value"}}, ...]} format expected = {"$and": [{"user_id": {"$eq": "alice"}}, {"agent_id": {"$eq": "agent1"}}, {"run_id": {"$eq": "run1"}}]} assert result == expected def test_generate_where_clause_single_filter(): """Test _generate_where_clause with single filter.""" filters = {"user_id": "alice"} result = ChromaDB._generate_where_clause(filters) # ChromaDB accepts single filters in {"field": {"$eq": "value"}} format expected = {"user_id": {"$eq": "alice"}} assert result == expected def test_generate_where_clause_no_filters(): """Test _generate_where_clause with no filters returns None.""" result = ChromaDB._generate_where_clause(None) assert result is None result = ChromaDB._generate_where_clause({}) assert result is None def test_generate_where_clause_all_wildcards_returns_none(): """All-wildcard filters must return None, not {}, to avoid ChromaDB ValueError.""" result = ChromaDB._generate_where_clause({"user_id": "*"}) assert result is None def test_generate_where_clause_non_string_values(): """Test _generate_where_clause with non-string values.""" filters = {"user_id": "alice", "count": 5, "active": True} result = ChromaDB._generate_where_clause(filters) # ChromaDB accepts non-string values in filters expected = {"$and": [{"user_id": {"$eq": "alice"}}, {"count": {"$eq": 5}}, {"active": {"$eq": True}}]} assert result == expected def test_generate_where_clause_not_single_equality(): """Test $not with a single equality condition.""" filters = {"$not": [{"status": "archived"}]} result = ChromaDB._generate_where_clause(filters) assert result == {"status": {"$ne": "archived"}} def test_generate_where_clause_not_multiple_conditions(): """Test $not with multiple conditions (OR semantics, negated to AND).""" filters = {"$not": [{"status": "archived"}, {"type": "draft"}]} result = ChromaDB._generate_where_clause(filters) assert result == {"$and": [{"status": {"$ne": "archived"}}, {"type": {"$ne": "draft"}}]} def test_generate_where_clause_not_with_operators(): """Test $not negates comparison operators correctly.""" filters = {"$not": [{"count": {"gt": 5}}]} result = ChromaDB._generate_where_clause(filters) assert result == {"count": {"$lte": 5}} def test_generate_where_clause_not_in_to_nin(): """Test $not converts 'in' to $nin.""" filters = {"$not": [{"status": {"in": ["archived", "deleted"]}}]} result = ChromaDB._generate_where_clause(filters) assert result == {"status": {"$nin": ["archived", "deleted"]}} def test_generate_where_clause_not_multi_field_condition(): """Test $not with multi-field condition uses De Morgan's (AND -> OR).""" filters = {"$not": [{"status": "archived", "type": "draft"}]} result = ChromaDB._generate_where_clause(filters) assert result == {"$or": [{"status": {"$ne": "archived"}}, {"type": {"$ne": "draft"}}]} def test_generate_where_clause_not_combined_with_other_filters(): """Test $not combined with regular filters.""" filters = {"user_id": "alice", "$not": [{"status": "archived"}]} result = ChromaDB._generate_where_clause(filters) assert result == {"$and": [{"user_id": {"$eq": "alice"}}, {"status": {"$ne": "archived"}}]} def test_chroma_config_accepts_default_tmp_path(): """Test that ChromaDbConfig accepts the default /tmp/chroma path.""" config = ChromaDbConfig(path="/tmp/chroma") assert config.path == "/tmp/chroma" def test_chroma_config_rejects_no_config(): """Test that ChromaDbConfig rejects when no connection config is provided.""" with pytest.raises(ValueError): ChromaDbConfig()