feat(memory): add OSS-to-Platform notices (#5494)

This commit is contained in:
youneshima
2026-06-13 06:04:20 -07:00
committed by GitHub
parent f322cf82b9
commit 3553fc79dd
29 changed files with 9694 additions and 33 deletions
+60
View File
@@ -0,0 +1,60 @@
import pytest
from mem0.memory import main as memory_main
from mem0.memory.main import AsyncMemory, Memory
def test_sync_project_update_decay_true_raises_with_notice_message(monkeypatch):
calls = []
def get_error(sync_type, trigger_function, trigger_parameter):
calls.append((sync_type, trigger_function, trigger_parameter))
return "blocked decay"
monkeypatch.setattr(memory_main, "get_decay_feature_error_message", get_error)
with pytest.raises(ValueError, match="blocked decay"):
Memory.__new__(Memory).project.update(decay=True)
assert calls == [("sync", "project.update", "decay")]
@pytest.mark.asyncio
async def test_async_project_update_decay_true_raises_with_notice_message(monkeypatch):
calls = []
async def get_error(sync_type, trigger_function, trigger_parameter):
calls.append((sync_type, trigger_function, trigger_parameter))
return "blocked async decay"
monkeypatch.setattr(memory_main, "get_decay_feature_error_message_async", get_error)
with pytest.raises(ValueError, match="blocked async decay"):
await AsyncMemory.__new__(AsyncMemory).project.update(decay=True)
assert calls == [("async", "project.update", "decay")]
@pytest.mark.parametrize("kwargs", [{}, {"decay": False}])
def test_sync_project_update_non_trigger_raises_plain_error_without_notice(monkeypatch, kwargs):
monkeypatch.setattr(
memory_main,
"get_decay_feature_error_message",
lambda *args: pytest.fail("decay feature notice should not run"),
)
with pytest.raises(ValueError, match="Project updates are not supported by the OSS Memory SDK."):
Memory.__new__(Memory).project.update(**kwargs)
@pytest.mark.asyncio
@pytest.mark.parametrize("kwargs", [{}, {"decay": False}])
async def test_async_project_update_non_trigger_raises_plain_error_without_notice(monkeypatch, kwargs):
monkeypatch.setattr(
memory_main,
"get_decay_feature_error_message_async",
lambda *args: pytest.fail("decay feature notice should not run"),
)
with pytest.raises(ValueError, match="Project updates are not supported by the OSS Memory SDK."):
await AsyncMemory.__new__(AsyncMemory).project.update(**kwargs)
+214
View File
@@ -0,0 +1,214 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from mem0.memory import main as memory_main
from mem0.memory.main import AsyncMemory, Memory
def make_sync_memory():
memory = Memory.__new__(Memory)
memory.vector_store = MagicMock()
memory._delete_memory = MagicMock()
return memory
def make_async_memory():
memory = AsyncMemory.__new__(AsyncMemory)
memory.vector_store = MagicMock()
memory._delete_memory = AsyncMock()
return memory
def test_sync_delete_decay_usage_runs_after_success(monkeypatch):
memory = make_sync_memory()
existing_memory = SimpleNamespace(id="memory-1")
memory.vector_store.get.return_value = existing_memory
decay_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(
memory_main,
"detect_decay_usage_from_delete",
MagicMock(return_value=("delete_count", "repeated_deletes", 5, None)),
)
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
result = Memory.delete(memory, "memory-1")
assert result == {"message": "Memory deleted successfully!"}
memory._delete_memory.assert_called_once_with("memory-1", existing_memory)
decay_notice.assert_called_once_with(
memory,
"sync",
"delete",
"delete_count",
"repeated_deletes",
5,
None,
)
first_run_notice.assert_not_called()
def test_sync_delete_below_threshold_uses_first_run_notice(monkeypatch):
memory = make_sync_memory()
memory.vector_store.get.return_value = SimpleNamespace(id="memory-1")
decay_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete", MagicMock(return_value=None))
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
Memory.delete(memory, "memory-1")
decay_notice.assert_not_called()
first_run_notice.assert_called_once_with(memory, "sync", "delete")
def test_sync_delete_failure_does_not_trigger_decay_usage_notice(monkeypatch):
memory = make_sync_memory()
memory.vector_store.get.return_value = None
detect_decay = MagicMock()
decay_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete", detect_decay)
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
with pytest.raises(ValueError, match="Memory with id memory-1 not found"):
Memory.delete(memory, "memory-1")
detect_decay.assert_not_called()
decay_notice.assert_not_called()
first_run_notice.assert_not_called()
def test_sync_delete_all_decay_usage_runs_after_success(monkeypatch):
memory = make_sync_memory()
memories = [SimpleNamespace(id="memory-1"), SimpleNamespace(id="memory-2")]
memory.vector_store.list.return_value = (memories, None)
decay_notice = MagicMock()
first_run_notice = MagicMock()
detect_decay = MagicMock(return_value=("delete_all", "bulk_delete", None, 2))
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete_all", detect_decay)
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
result = Memory.delete_all(memory, user_id="u1")
assert result == {"message": "Memories deleted successfully!"}
assert memory._delete_memory.call_count == 2
detect_decay.assert_called_once_with(2)
decay_notice.assert_called_once_with(
memory,
"sync",
"delete_all",
"delete_all",
"bulk_delete",
None,
2,
)
first_run_notice.assert_not_called()
def test_sync_delete_all_zero_deletes_uses_first_run_notice(monkeypatch):
memory = make_sync_memory()
memory.vector_store.list.return_value = ([], None)
decay_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete_all", MagicMock(return_value=None))
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
Memory.delete_all(memory, user_id="u1")
decay_notice.assert_not_called()
first_run_notice.assert_called_once_with(memory, "sync", "delete_all")
@pytest.mark.asyncio
async def test_async_delete_decay_usage_runs_after_success(monkeypatch):
memory = make_async_memory()
existing_memory = SimpleNamespace(id="memory-1")
memory.vector_store.get.return_value = existing_memory
decay_notice = AsyncMock()
first_run_notice = AsyncMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(
memory_main,
"detect_decay_usage_from_delete",
MagicMock(return_value=("delete_count", "repeated_deletes", 5, None)),
)
monkeypatch.setattr(memory_main, "display_decay_usage_notice_async", decay_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
result = await AsyncMemory.delete(memory, "memory-1")
assert result == {"message": "Memory deleted successfully!"}
memory._delete_memory.assert_awaited_once_with("memory-1", existing_memory)
decay_notice.assert_awaited_once_with(
memory,
"async",
"delete",
"delete_count",
"repeated_deletes",
5,
None,
)
first_run_notice.assert_not_awaited()
@pytest.mark.asyncio
async def test_async_delete_failure_does_not_trigger_decay_usage_notice(monkeypatch):
memory = make_async_memory()
memory.vector_store.get.return_value = None
detect_decay = MagicMock()
decay_notice = AsyncMock()
first_run_notice = AsyncMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete", detect_decay)
monkeypatch.setattr(memory_main, "display_decay_usage_notice_async", decay_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
with pytest.raises(ValueError, match="Memory with id memory-1 not found"):
await AsyncMemory.delete(memory, "memory-1")
detect_decay.assert_not_called()
decay_notice.assert_not_awaited()
first_run_notice.assert_not_awaited()
@pytest.mark.asyncio
async def test_async_delete_all_decay_usage_runs_after_success(monkeypatch):
memory = make_async_memory()
memories = [SimpleNamespace(id="memory-1"), SimpleNamespace(id="memory-2")]
memory.vector_store.list.return_value = (memories, None)
decay_notice = AsyncMock()
first_run_notice = AsyncMock()
detect_decay = MagicMock(return_value=("delete_all", "bulk_delete", None, 2))
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete_all", detect_decay)
monkeypatch.setattr(memory_main, "display_decay_usage_notice_async", decay_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
result = await AsyncMemory.delete_all(memory, user_id="u1")
assert result == {"message": "Memories deleted successfully!"}
assert memory._delete_memory.await_count == 2
detect_decay.assert_called_once_with(2)
decay_notice.assert_awaited_once_with(
memory,
"async",
"delete_all",
"delete_all",
"bulk_delete",
None,
2,
)
first_run_notice.assert_not_awaited()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,445 @@
from copy import deepcopy
from unittest.mock import AsyncMock, MagicMock
import pytest
from mem0.memory import notices
from mem0.memory import main as memory_main
from mem0.memory.main import AsyncMemory, Memory
class FakeFlags:
def __init__(self, variant, payload):
self.variant = variant
self.payload = payload
def get_flag(self, key):
assert key == notices.FLAG_KEY
return self.variant
def get_flag_payload(self, key):
assert key == notices.FLAG_KEY
return self.payload
@pytest.fixture(autouse=True)
def reset_notice_process_state():
notices._performance_slow_query_capacity_reached_in_process = False
yield
notices._performance_slow_query_capacity_reached_in_process = False
@pytest.fixture
def notice_harness(monkeypatch):
config = {}
telemetry = MagicMock()
telemetry.user_id = "oss-user"
def write_config(updated):
saved = deepcopy(updated)
config.clear()
config.update(saved)
monkeypatch.setattr(notices, "_load_config", lambda: config)
monkeypatch.setattr(notices, "_write_config", write_config)
monkeypatch.setattr(notices.telemetry_module, "MEM0_TELEMETRY", True)
monkeypatch.setattr(notices.telemetry_module, "_get_oss_telemetry", lambda: telemetry)
return config, telemetry
def configure_flag(telemetry, variant, payload):
flags = FakeFlags(variant, payload)
telemetry.posthog.evaluate_flags.return_value = flags
return flags
def performance_payload(copy="Performance CTA", enabled=True, notice_type="log_line"):
payload = {
"notices": {
"performance_slow_query": {
"enabled": enabled,
"notice_type": notice_type,
}
}
}
if copy is not None:
payload["notices"]["performance_slow_query"]["copy"] = copy
return payload
def make_sync_memory(search_results=None):
memory = Memory.__new__(Memory)
memory.api_version = "v1.1"
memory.reranker = None
memory._search_vector_store = MagicMock(return_value=search_results or [])
return memory
def make_async_memory(search_results=None):
memory = AsyncMemory.__new__(AsyncMemory)
memory.api_version = "v1.1"
memory.reranker = None
memory._search_vector_store = AsyncMock(return_value=search_results or [])
return memory
def test_sync_slow_search_triggers_performance_notice_after_success(monkeypatch):
results = [{"id": "m1"}, {"id": "m2"}]
memory = make_sync_memory(search_results=results)
performance_notice = MagicMock()
temporal_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
result = Memory.search(memory, "favorite drink", filters={"user_id": "u1"}, top_k=3)
assert result == {"results": results}
memory._search_vector_store.assert_called_once()
performance_notice.assert_called_once_with(memory, "sync", "search", pytest.approx(2.1), 3, 2)
temporal_notice.assert_not_called()
first_run_notice.assert_not_called()
def test_sync_fast_search_uses_first_run_notice(monkeypatch):
memory = make_sync_memory()
performance_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 101.0]))
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", MagicMock())
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
Memory.search(memory, "favorite drink", filters={"user_id": "u1"})
performance_notice.assert_not_called()
first_run_notice.assert_called_once_with(memory, "sync", "search")
def test_sync_failed_search_does_not_trigger_performance_notice(monkeypatch):
memory = make_sync_memory()
memory._search_vector_store.side_effect = RuntimeError("search failure")
performance_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(return_value=100.0))
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", MagicMock())
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
with pytest.raises(RuntimeError, match="search failure"):
Memory.search(memory, "favorite drink", filters={"user_id": "u1"})
performance_notice.assert_not_called()
first_run_notice.assert_not_called()
def test_sync_temporal_usage_takes_precedence_over_slow_search(monkeypatch):
memory = make_sync_memory()
performance_notice = MagicMock()
temporal_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
Memory.search(memory, "what happened last week?", filters={"user_id": "u1"})
temporal_notice.assert_called_once_with(memory, "sync", "search", "query", "relative_phrase")
performance_notice.assert_not_called()
first_run_notice.assert_not_called()
def test_sync_scale_takes_precedence_over_slow_search(monkeypatch):
memory = make_sync_memory()
performance_notice = MagicMock()
scale_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
monkeypatch.setattr(memory_main, "display_scale_threshold_notice", scale_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
Memory.search(memory, "favorite drink", filters={"user_id": "u1"}, top_k=50)
scale_notice.assert_called_once_with(
memory,
"sync",
"search",
"top_k",
"high_top_k",
50,
None,
notices.SCALE_TOP_K_THRESHOLD,
)
performance_notice.assert_not_called()
first_run_notice.assert_not_called()
@pytest.mark.asyncio
async def test_async_slow_search_triggers_performance_notice_after_success(monkeypatch):
results = [{"id": "m1"}]
memory = make_async_memory(search_results=results)
performance_notice = AsyncMock()
temporal_notice = AsyncMock()
first_run_notice = AsyncMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice_async", performance_notice)
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
result = await AsyncMemory.search(memory, "favorite drink", filters={"user_id": "u1"}, top_k=4)
assert result == {"results": results}
memory._search_vector_store.assert_awaited_once()
performance_notice.assert_awaited_once_with(memory, "async", "search", pytest.approx(2.1), 4, 1)
temporal_notice.assert_not_awaited()
first_run_notice.assert_not_awaited()
@pytest.mark.asyncio
async def test_async_fast_search_uses_first_run_notice(monkeypatch):
memory = make_async_memory()
performance_notice = AsyncMock()
first_run_notice = AsyncMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 101.0]))
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice_async", performance_notice)
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", AsyncMock())
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
await AsyncMemory.search(memory, "favorite drink", filters={"user_id": "u1"})
performance_notice.assert_not_awaited()
first_run_notice.assert_awaited_once_with(memory, "async", "search")
@pytest.mark.asyncio
async def test_async_failed_search_does_not_trigger_performance_notice(monkeypatch):
memory = make_async_memory()
memory._search_vector_store.side_effect = RuntimeError("search failure")
performance_notice = AsyncMock()
first_run_notice = AsyncMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(return_value=100.0))
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice_async", performance_notice)
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", AsyncMock())
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
with pytest.raises(RuntimeError, match="search failure"):
await AsyncMemory.search(memory, "favorite drink", filters={"user_id": "u1"})
performance_notice.assert_not_awaited()
first_run_notice.assert_not_awaited()
@pytest.mark.asyncio
async def test_async_temporal_usage_takes_precedence_over_slow_search(monkeypatch):
memory = make_async_memory()
performance_notice = AsyncMock()
temporal_notice = AsyncMock()
first_run_notice = AsyncMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice_async", performance_notice)
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
await AsyncMemory.search(memory, "what happened last week?", filters={"user_id": "u1"})
temporal_notice.assert_awaited_once_with(memory, "async", "search", "query", "relative_phrase")
performance_notice.assert_not_awaited()
first_run_notice.assert_not_awaited()
def test_performance_slow_query_displayed_logs_and_captures_event(notice_harness, capsys):
config, telemetry = notice_harness
flags = configure_flag(telemetry, "displayed", performance_payload())
notices.display_performance_slow_query_notice(
MagicMock(),
"sync",
"search",
elapsed_seconds=2.345,
top_k=20,
result_count=7,
)
assert capsys.readouterr().err == "Performance CTA\n"
telemetry.posthog.evaluate_flags.assert_called_once_with("oss-user", flag_keys=[notices.FLAG_KEY])
telemetry.capture_event.assert_called_once()
event_name, props = telemetry.capture_event.call_args.args
assert event_name == notices.NOTICE_EVENT
assert props["notice_id"] == "performance_slow_query"
assert props["notice_type"] == "log_line"
assert props["variant"] == "displayed"
assert props["displayed"] is True
assert props["payload"] == "Performance CTA"
assert props["bypass_reason"] is None
assert props["disabled_reason"] is None
assert props["notice_config_found"] is True
assert props["sync_type"] == "sync"
assert props["trigger_function"] == "search"
assert props["trigger_reason"] == "slow_query"
assert props["elapsed_ms"] == 2345
assert props["threshold_ms"] == 2000
assert props["top_k"] == 20
assert props["result_count"] == 7
assert telemetry.capture_event.call_args.kwargs["flags"] is flags
assert len(config["notice_state"]["performance_slow_query"]["events"]) == 1
def test_performance_slow_query_holdout_is_silent_but_captures_event(notice_harness, capsys):
_, telemetry = notice_harness
configure_flag(telemetry, "holdout", performance_payload())
notices.display_performance_slow_query_notice(
MagicMock(),
"sync",
"search",
elapsed_seconds=2.1,
top_k=10,
result_count=2,
)
assert capsys.readouterr().err == ""
props = telemetry.capture_event.call_args.args[1]
assert props["displayed"] is False
assert props["bypass_reason"] == "holdout"
assert props["trigger_reason"] == "slow_query"
@pytest.mark.parametrize(
("payload", "expected_reason", "expected_found"),
[
({}, "missing_notice_config", False),
({"notices": {}}, "missing_notice_config", False),
({"notices": "not-an-object"}, "missing_notice_config", False),
(performance_payload(copy=None), "missing_copy", True),
(performance_payload(enabled=False, copy="hidden"), "payload_disabled", True),
],
)
def test_performance_slow_query_bad_or_disabled_payload_is_silent_and_safe(
notice_harness, payload, expected_reason, expected_found, capsys
):
_, telemetry = notice_harness
configure_flag(telemetry, "displayed", payload)
notices.display_performance_slow_query_notice(
MagicMock(),
"sync",
"search",
elapsed_seconds=2.1,
top_k=10,
result_count=2,
)
assert capsys.readouterr().err == ""
props = telemetry.capture_event.call_args.args[1]
assert props["displayed"] is False
assert props["bypass_reason"] == expected_reason
assert props["notice_config_found"] is expected_found
@pytest.mark.parametrize("variant", [None, False])
def test_performance_slow_query_blunt_flag_disable_does_not_capture_or_consume(
notice_harness, variant, capsys
):
config, telemetry = notice_harness
configure_flag(telemetry, variant, performance_payload())
notices.display_performance_slow_query_notice(
MagicMock(),
"sync",
"search",
elapsed_seconds=2.1,
top_k=10,
result_count=2,
)
assert capsys.readouterr().err == ""
telemetry.capture_event.assert_not_called()
assert config.get("notice_state") is None
def test_performance_slow_query_telemetry_disabled_does_not_touch_posthog_or_state(
monkeypatch, capsys
):
load_config = MagicMock(return_value={})
write_config = MagicMock()
get_telemetry = MagicMock()
monkeypatch.setattr(notices, "_load_config", load_config)
monkeypatch.setattr(notices, "_write_config", write_config)
monkeypatch.setattr(notices.telemetry_module, "MEM0_TELEMETRY", False)
monkeypatch.setattr(notices.telemetry_module, "_get_oss_telemetry", get_telemetry)
notices.display_performance_slow_query_notice(
MagicMock(),
"sync",
"search",
elapsed_seconds=2.1,
top_k=10,
result_count=2,
)
load_config.assert_not_called()
write_config.assert_not_called()
get_telemetry.assert_not_called()
assert capsys.readouterr().err == ""
def test_performance_slow_query_cap_blocks_before_posthog_eval(notice_harness, capsys):
config, telemetry = notice_harness
configure_flag(telemetry, "displayed", performance_payload())
for _ in range(notices.PERFORMANCE_SLOW_QUERY_CAP):
notices.display_performance_slow_query_notice(
MagicMock(),
"sync",
"search",
elapsed_seconds=2.1,
top_k=10,
result_count=2,
)
notices.display_performance_slow_query_notice(
MagicMock(),
"sync",
"search",
elapsed_seconds=2.1,
top_k=10,
result_count=2,
)
assert capsys.readouterr().err == "Performance CTA\n" * notices.PERFORMANCE_SLOW_QUERY_CAP
assert telemetry.posthog.evaluate_flags.call_count == notices.PERFORMANCE_SLOW_QUERY_CAP
assert telemetry.capture_event.call_count == notices.PERFORMANCE_SLOW_QUERY_CAP
assert len(config["notice_state"]["performance_slow_query"]["events"]) == notices.PERFORMANCE_SLOW_QUERY_CAP
def test_performance_slow_query_props_do_not_include_raw_user_inputs(notice_harness):
_, telemetry = notice_harness
configure_flag(telemetry, "displayed", performance_payload(copy="safe copy"))
notices.display_performance_slow_query_notice(
MagicMock(),
"sync",
"search",
elapsed_seconds=2.1,
top_k=10,
result_count=2,
)
props = telemetry.capture_event.call_args.args[1]
assert "favorite drink" not in str(props)
assert "user_id" not in str(props)
assert "green tea" not in str(props)
@@ -0,0 +1,96 @@
import pytest
from mem0.memory import main as memory_main
from mem0.memory.main import AsyncMemory, Memory
def test_sync_add_timestamp_raises_before_validation(monkeypatch):
calls = []
def get_error(sync_type, trigger_function, trigger_parameter):
calls.append((sync_type, trigger_function, trigger_parameter))
return "blocked timestamp"
monkeypatch.setattr(memory_main, "get_temporal_feature_error_message", get_error)
with pytest.raises(ValueError, match="blocked timestamp"):
Memory.add(Memory.__new__(Memory), "hello", timestamp=123)
assert calls == [("sync", "add", "timestamp")]
def test_sync_search_reference_date_raises_before_validation(monkeypatch):
calls = []
def get_error(sync_type, trigger_function, trigger_parameter):
calls.append((sync_type, trigger_function, trigger_parameter))
return "blocked reference date"
monkeypatch.setattr(memory_main, "get_temporal_feature_error_message", get_error)
with pytest.raises(ValueError, match="blocked reference date"):
Memory.search(Memory.__new__(Memory), "what happened last week?", reference_date="2025-03-21")
assert calls == [("sync", "search", "reference_date")]
@pytest.mark.asyncio
async def test_async_add_timestamp_raises_before_validation(monkeypatch):
calls = []
async def get_error(sync_type, trigger_function, trigger_parameter):
calls.append((sync_type, trigger_function, trigger_parameter))
return "blocked async timestamp"
monkeypatch.setattr(memory_main, "get_temporal_feature_error_message_async", get_error)
with pytest.raises(ValueError, match="blocked async timestamp"):
await AsyncMemory.add(AsyncMemory.__new__(AsyncMemory), "hello", timestamp=123)
assert calls == [("async", "add", "timestamp")]
@pytest.mark.asyncio
async def test_async_search_reference_date_raises_before_validation(monkeypatch):
calls = []
async def get_error(sync_type, trigger_function, trigger_parameter):
calls.append((sync_type, trigger_function, trigger_parameter))
return "blocked async reference date"
monkeypatch.setattr(memory_main, "get_temporal_feature_error_message_async", get_error)
with pytest.raises(ValueError, match="blocked async reference date"):
await AsyncMemory.search(
AsyncMemory.__new__(AsyncMemory),
"what happened last week?",
reference_date="2025-03-21",
)
assert calls == [("async", "search", "reference_date")]
def test_sync_add_without_timestamp_does_not_call_temporal_feature_notice(monkeypatch):
get_error = monkeypatch.setattr(
memory_main,
"get_temporal_feature_error_message",
lambda *args: pytest.fail("temporal feature notice should not run"),
)
with pytest.raises(Exception, match="At least one of 'user_id', 'agent_id', or 'run_id'"):
Memory.add(Memory.__new__(Memory), "hello")
assert get_error is None
def test_sync_search_without_reference_date_does_not_call_temporal_feature_notice(monkeypatch):
get_error = monkeypatch.setattr(
memory_main,
"get_temporal_feature_error_message",
lambda *args: pytest.fail("temporal feature notice should not run"),
)
with pytest.raises(ValueError, match="filters must contain"):
Memory.search(Memory.__new__(Memory), "hello")
assert get_error is None
+202
View File
@@ -0,0 +1,202 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from mem0.memory import main as memory_main
from mem0.memory.main import AsyncMemory, Memory
def make_sync_memory():
memory = Memory.__new__(Memory)
memory.config = SimpleNamespace(llm=SimpleNamespace(config={}))
memory.api_version = "v1.1"
memory.reranker = None
memory._add_to_vector_store = MagicMock(return_value=[])
memory._search_vector_store = MagicMock(return_value=[])
return memory
def make_async_memory():
memory = AsyncMemory.__new__(AsyncMemory)
memory.config = SimpleNamespace(llm=SimpleNamespace(config={}))
memory.api_version = "v1.1"
memory.reranker = None
memory._add_to_vector_store = AsyncMock(return_value=[])
memory._search_vector_store = AsyncMock(return_value=[])
return memory
def test_sync_add_temporal_metadata_triggers_notice_after_success(monkeypatch):
memory = make_sync_memory()
temporal_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
result = Memory.add(
memory,
"The user visited Paris.",
user_id="u1",
metadata={"event_date": "2025-04-09"},
infer=False,
)
assert result == {"results": []}
memory._add_to_vector_store.assert_called_once()
temporal_notice.assert_called_once_with(memory, "sync", "add", "metadata", "date_like_metadata")
first_run_notice.assert_not_called()
def test_sync_add_non_temporal_metadata_uses_first_run_notice(monkeypatch):
memory = make_sync_memory()
temporal_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
Memory.add(memory, "The user likes tea.", user_id="u1", metadata={"topic": "drink"}, infer=False)
temporal_notice.assert_not_called()
first_run_notice.assert_called_once_with(memory, "sync", "add")
def test_sync_add_failure_does_not_trigger_temporal_usage_notice(monkeypatch):
memory = make_sync_memory()
memory._add_to_vector_store.side_effect = RuntimeError("vector failure")
temporal_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
with pytest.raises(RuntimeError, match="vector failure"):
Memory.add(
memory,
"The user visited Paris.",
user_id="u1",
metadata={"event_date": "2025-04-09"},
infer=False,
)
temporal_notice.assert_not_called()
first_run_notice.assert_not_called()
def test_sync_search_temporal_query_triggers_notice_after_success(monkeypatch):
memory = make_sync_memory()
temporal_notice = MagicMock()
first_run_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
result = Memory.search(memory, "what happened last week?", filters={"user_id": "u1"})
assert result == {"results": []}
memory._search_vector_store.assert_called_once()
temporal_notice.assert_called_once_with(memory, "sync", "search", "query", "relative_phrase")
first_run_notice.assert_not_called()
def test_sync_search_temporal_filter_triggers_notice_after_success(monkeypatch):
memory = make_sync_memory()
temporal_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", MagicMock())
Memory.search(
memory,
"favorite drink",
filters={"user_id": "u1", "created_at": {"gte": "2025-04-01"}},
)
temporal_notice.assert_called_once_with(memory, "sync", "search", "filter", "date_range_filter")
def test_sync_search_failure_does_not_trigger_temporal_usage_notice(monkeypatch):
memory = make_sync_memory()
memory._search_vector_store.side_effect = RuntimeError("search failure")
temporal_notice = MagicMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice", MagicMock())
with pytest.raises(RuntimeError, match="search failure"):
Memory.search(memory, "what happened last week?", filters={"user_id": "u1"})
temporal_notice.assert_not_called()
@pytest.mark.asyncio
async def test_async_add_temporal_metadata_triggers_notice_after_success(monkeypatch):
memory = make_async_memory()
temporal_notice = AsyncMock()
first_run_notice = AsyncMock()
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
result = await AsyncMemory.add(
memory,
"The user visited Paris.",
user_id="u1",
metadata={"event_date": "2025-04-09"},
infer=False,
)
assert result == {"results": []}
memory._add_to_vector_store.assert_awaited_once()
temporal_notice.assert_awaited_once_with(memory, "async", "add", "metadata", "date_like_metadata")
first_run_notice.assert_not_awaited()
@pytest.mark.asyncio
async def test_async_add_runs_scale_detection_in_thread(monkeypatch):
memory = make_async_memory()
scale_detector = MagicMock(return_value=("memory_count", "memory_count_threshold", None, 2000, 2000))
scale_notice = AsyncMock()
first_run_notice = AsyncMock()
to_thread_calls = []
async def to_thread(fn, *args, **kwargs):
to_thread_calls.append((fn, args, kwargs))
return fn(*args, **kwargs)
monkeypatch.setattr(memory_main, "detect_scale_threshold_from_add_result", scale_detector)
monkeypatch.setattr(memory_main.asyncio, "to_thread", to_thread)
monkeypatch.setattr(memory_main, "display_scale_threshold_notice_async", scale_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
result = await AsyncMemory.add(memory, "The user likes tea.", user_id="u1", infer=False)
assert result == {"results": []}
assert to_thread_calls == [(scale_detector, (memory, []), {})]
scale_detector.assert_called_once_with(memory, [])
scale_notice.assert_awaited_once_with(
memory,
"async",
"add",
"memory_count",
"memory_count_threshold",
None,
2000,
2000,
)
first_run_notice.assert_not_awaited()
@pytest.mark.asyncio
async def test_async_search_temporal_query_triggers_notice_after_success(monkeypatch):
memory = make_async_memory()
temporal_notice = AsyncMock()
first_run_notice = AsyncMock()
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", temporal_notice)
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
result = await AsyncMemory.search(memory, "what happened last week?", filters={"user_id": "u1"})
assert result == {"results": []}
memory._search_vector_store.assert_awaited_once()
temporal_notice.assert_awaited_once_with(memory, "async", "search", "query", "relative_phrase")
first_run_notice.assert_not_awaited()