555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
347 lines
12 KiB
Python
347 lines
12 KiB
Python
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()
|