Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:03:45 +08:00

231 lines
8.2 KiB
Python

"""Tests for Redis vector store update() — embedding corruption fix.
Regression tests for #4336: when update() is called with vector=None
(metadata-only update), np.array(None) silently creates a 4-byte scalar,
overwriting the real embedding. The fix skips the embedding field entirely
when vector is None.
"""
from datetime import datetime
from unittest.mock import MagicMock, patch
import numpy as np
import pytz
def _make_redis_db():
"""Create a RedisDB instance with mocked internals, bypassing __init__
to avoid the redis module name collision with mem0.vector_stores.redis."""
from mem0.vector_stores.redis import RedisDB
db = RedisDB.__new__(RedisDB)
mock_index = MagicMock()
db.index = mock_index
db.schema = {"index": {"prefix": "mem0:test"}}
return db, mock_index
def test_update_with_none_vector_preserves_embedding():
"""update() with vector=None should not include embedding in the data."""
db, mock_index = _make_redis_db()
payload = {
"hash": "test_hash",
"data": "updated_data",
"created_at": datetime.now(pytz.timezone("UTC")).isoformat(),
"updated_at": datetime.now(pytz.timezone("UTC")).isoformat(),
"user_id": "test_user",
}
db.update(vector_id="test_id", vector=None, payload=payload)
mock_index.load.assert_called_once()
call_kwargs = mock_index.load.call_args
data_dict = call_kwargs[1]["data"][0] if "data" in call_kwargs[1] else call_kwargs[0][0][0]
assert "embedding" not in data_dict, (
"embedding should not be in data when vector is None"
)
assert data_dict["memory_id"] == "test_id"
def test_update_with_vector_includes_embedding():
"""update() with a real vector should include embedding in the data."""
db, mock_index = _make_redis_db()
vector = np.random.rand(1536).tolist()
payload = {
"hash": "test_hash",
"data": "updated_data",
"created_at": datetime.now(pytz.timezone("UTC")).isoformat(),
"updated_at": datetime.now(pytz.timezone("UTC")).isoformat(),
"user_id": "test_user",
}
db.update(vector_id="test_id", vector=vector, payload=payload)
mock_index.load.assert_called_once()
call_kwargs = mock_index.load.call_args
data_dict = call_kwargs[1]["data"][0] if "data" in call_kwargs[1] else call_kwargs[0][0][0]
assert "embedding" in data_dict, (
"embedding should be in data when vector is provided"
)
expected_bytes = np.array(vector, dtype=np.float32).tobytes()
assert data_dict["embedding"] == expected_bytes
def test_search_with_none_filters_does_not_crash():
"""search() with the base-class default filters=None must run an unfiltered
query, not raise. Regression: filters.items() on None raised AttributeError
and reduce() over an empty list raised TypeError. keyword_search() in the
same file already guards this; search() did not."""
db, mock_index = _make_redis_db()
mock_index.query.return_value = []
assert db.search("query", [0.1, 0.2, 0.3, 0.4], top_k=5, filters=None) == []
mock_index.query.assert_called_once()
def test_search_with_empty_or_all_none_filters_does_not_crash():
db, mock_index = _make_redis_db()
mock_index.query.return_value = []
assert db.search("query", [0.1, 0.2, 0.3, 0.4], filters={}) == []
assert db.search("query", [0.1, 0.2, 0.3, 0.4], filters={"user_id": None}) == []
def test_list_with_none_filters_matches_all():
"""list() with no filters must issue a match-all ('*') query, not raise."""
db, mock_index = _make_redis_db()
mock_index.search.return_value = MagicMock(docs=[])
db.list(filters=None)
query = mock_index.search.call_args[0][0]
assert query.query_string() == "*"
def test_list_with_filter_builds_query():
"""A real filter is still translated into a tag query (no regression)."""
db, mock_index = _make_redis_db()
mock_index.search.return_value = MagicMock(docs=[])
db.list(filters={"user_id": "alice"})
query = mock_index.search.call_args[0][0]
assert query.query_string() == "@user_id:{alice}"
def test_create_col_keeps_distinct_dims_across_instances():
"""Building the schema for two collections with different embedding
dimensions must keep them distinct. Regression: DEFAULT_FIELDS.copy() is a
shallow copy, so the shared "embedding" field dict was mutated in place and
the second create_col() clobbered the first one's dims (and the module
global)."""
import mem0.vector_stores.redis as redis_module
db, _ = _make_redis_db()
db.client = MagicMock()
captured = []
def capture_schema(schema):
captured.append(schema)
return MagicMock()
with patch.object(redis_module.SearchIndex, "from_dict", side_effect=capture_schema):
db.create_col(name="col_384", vector_size=384)
db.create_col(name="col_1536", vector_size=1536)
assert captured[0]["fields"][-1]["attrs"]["dims"] == 384
assert captured[1]["fields"][-1]["attrs"]["dims"] == 1536
# the module-level default must never be mutated by building a schema
assert "dims" not in redis_module.DEFAULT_FIELDS[-1]["attrs"]
def test_init_keeps_distinct_dims_from_module_global():
"""__init__ must stamp the requested dims into the index schema without
mutating the shared module-level DEFAULT_FIELDS. The create_col test above
covers the other deepcopy site; all other tests build RedisDB via __new__
and so skip __init__, leaving this call site otherwise uncovered."""
import mem0.vector_stores.redis as redis_module
from mem0.vector_stores.redis import RedisDB
captured = []
def capture_schema(schema):
captured.append(schema)
return MagicMock()
with (
patch("mem0.vector_stores.redis.redis.Redis.from_url", return_value=MagicMock()),
patch.object(redis_module.SearchIndex, "from_dict", side_effect=capture_schema),
):
RedisDB("redis://localhost:6379", "col_384", 384)
assert captured[0]["fields"][-1]["attrs"]["dims"] == 384
# the module-level default must never be mutated by constructing an instance
assert "dims" not in redis_module.DEFAULT_FIELDS[-1]["attrs"]
def test_get_returns_none_for_missing_id():
"""get() must return None for a missing id.
redisvl's SearchIndex.fetch() returns None when the id is not found, so the
old code raised ``TypeError: 'NoneType' object is not subscriptable`` on
``result["hash"]``. Every other vector store returns None for a missing id,
and Memory relies on it (``if existing_memory is None``), so get() must too.
"""
db, mock_index = _make_redis_db()
mock_index.fetch.return_value = None
assert db.get("missing_id") is None
mock_index.fetch.assert_called_once_with("missing_id")
def test_insert_entity_payload_without_hash_and_created_at():
"""insert() must not crash on entity payloads that lack hash/created_at."""
db, mock_index = _make_redis_db()
entity_payload = {
"data": "OpenAI",
"entity_type": "organization",
"linked_memory_ids": ["mem-1"],
"user_id": "test_user",
}
db.insert(
vectors=[[0.1, 0.2, 0.3]],
payloads=[entity_payload],
ids=["entity-1"],
)
mock_index.load.assert_called_once()
data = mock_index.load.call_args[0][0]
assert data[0]["memory_id"] == "entity-1"
assert data[0]["memory"] == "OpenAI"
assert data[0]["hash"] == ""
assert data[0]["created_at"] == 0
def test_update_entity_payload_without_hash_and_timestamps():
"""update() must not crash on entity payloads that lack hash/created_at/updated_at."""
db, mock_index = _make_redis_db()
entity_payload = {
"data": "OpenAI",
"entity_type": "organization",
"linked_memory_ids": ["mem-1"],
"user_id": "test_user",
}
db.update(vector_id="entity-1", vector=[0.1, 0.2, 0.3], payload=entity_payload)
mock_index.load.assert_called_once()
call_kwargs = mock_index.load.call_args
data_dict = call_kwargs[1]["data"][0] if "data" in call_kwargs[1] else call_kwargs[0][0][0]
assert data_dict["memory_id"] == "entity-1"
assert data_dict["memory"] == "OpenAI"
assert data_dict["hash"] == ""
assert data_dict["created_at"] == 0
assert data_dict["updated_at"] == 0