chore: merge origin/main into mintlify/af82bda4 and resolve conflicts
This commit is contained in:
@@ -192,3 +192,16 @@ class TestProcessTelemetryFilters:
|
||||
class TestRemoveCodeBlocks:
|
||||
def test_none_content_returns_empty_string(self):
|
||||
assert remove_code_blocks(None) == ""
|
||||
|
||||
def test_list_content_from_langchain_aimessage_is_joined(self):
|
||||
assert remove_code_blocks([{"type": "text", "text": "hello"}]) == "hello"
|
||||
|
||||
def test_list_content_joins_multiple_blocks_and_bare_strings(self):
|
||||
content = [{"type": "text", "text": "hello "}, "world", {"type": "thinking", "thinking": "ignored"}]
|
||||
assert remove_code_blocks(content) == "hello world"
|
||||
|
||||
def test_list_content_still_strips_code_fences(self):
|
||||
assert remove_code_blocks([{"type": "text", "text": "```json\n{}\n```"}]) == "{}"
|
||||
|
||||
def test_unsupported_content_type_returns_empty_string(self):
|
||||
assert remove_code_blocks(42) == ""
|
||||
|
||||
+113
-21
@@ -56,6 +56,7 @@ def notice_harness(monkeypatch):
|
||||
config = {}
|
||||
telemetry = MagicMock()
|
||||
telemetry.user_id = "oss-user"
|
||||
evaluate_mock = MagicMock()
|
||||
|
||||
def write_config(updated):
|
||||
saved = deepcopy(updated)
|
||||
@@ -66,13 +67,15 @@ def notice_harness(monkeypatch):
|
||||
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)
|
||||
monkeypatch.setattr(notices, "_evaluate_notice_flags", evaluate_mock)
|
||||
telemetry._evaluate_mock = evaluate_mock
|
||||
|
||||
return config, telemetry
|
||||
|
||||
|
||||
def configure_flag(telemetry, variant, payload):
|
||||
flags = FakeFlags(variant, payload)
|
||||
telemetry.posthog.evaluate_flags.return_value = flags
|
||||
telemetry._evaluate_mock.return_value = flags
|
||||
return flags
|
||||
|
||||
|
||||
@@ -176,7 +179,7 @@ def test_displayed_notice_logs_once_and_captures_event(notice_harness, capsys):
|
||||
config, telemetry, flags = display_notice(notice_harness)
|
||||
|
||||
assert capsys.readouterr().err == "Mem0 OSS notice\n"
|
||||
telemetry.posthog.evaluate_flags.assert_called_once_with("oss-user", flag_keys=[notices.FLAG_KEY])
|
||||
telemetry._evaluate_mock.assert_called_once_with("oss-user")
|
||||
telemetry.capture_event.assert_called_once()
|
||||
event_name, props = telemetry.capture_event.call_args.args
|
||||
assert event_name == notices.NOTICE_EVENT
|
||||
@@ -254,7 +257,7 @@ def test_telemetry_disabled_does_not_touch_posthog_or_state(monkeypatch, capsys)
|
||||
|
||||
def test_posthog_failure_is_silent_and_consumes_first_run(notice_harness, capsys):
|
||||
config, telemetry = notice_harness
|
||||
telemetry.posthog.evaluate_flags.side_effect = RuntimeError("network unavailable")
|
||||
telemetry._evaluate_mock.side_effect = RuntimeError("network unavailable")
|
||||
|
||||
notices.display_first_run_notice(MagicMock(), "sync", "add")
|
||||
|
||||
@@ -265,7 +268,7 @@ def test_posthog_failure_is_silent_and_consumes_first_run(notice_harness, capsys
|
||||
|
||||
def test_public_add_succeeds_when_first_run_flag_eval_fails(notice_harness):
|
||||
_, telemetry = notice_harness
|
||||
telemetry.posthog.evaluate_flags.side_effect = RuntimeError("network unavailable")
|
||||
telemetry._evaluate_mock.side_effect = RuntimeError("network unavailable")
|
||||
memory = Memory.__new__(Memory)
|
||||
memory.config = SimpleNamespace(llm=SimpleNamespace(config={}))
|
||||
memory._add_to_vector_store = MagicMock(return_value=[{"event": "ADD", "memory": "likes tea"}])
|
||||
@@ -277,7 +280,7 @@ def test_public_add_succeeds_when_first_run_flag_eval_fails(notice_harness):
|
||||
|
||||
def test_public_search_succeeds_when_first_run_flag_eval_fails(notice_harness, monkeypatch):
|
||||
_, telemetry = notice_harness
|
||||
telemetry.posthog.evaluate_flags.side_effect = RuntimeError("network unavailable")
|
||||
telemetry._evaluate_mock.side_effect = RuntimeError("network unavailable")
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
memory = Memory.__new__(Memory)
|
||||
memory.api_version = "v1.1"
|
||||
@@ -325,7 +328,7 @@ def test_temporal_feature_displayed_returns_payload_copy_and_captures_event(noti
|
||||
|
||||
assert message == "Temporal CTA"
|
||||
assert capsys.readouterr().err == ""
|
||||
telemetry.posthog.evaluate_flags.assert_called_once_with("oss-user", flag_keys=[notices.FLAG_KEY])
|
||||
telemetry._evaluate_mock.assert_called_once_with("oss-user")
|
||||
telemetry.capture_event.assert_called_once()
|
||||
event_name, props = telemetry.capture_event.call_args.args
|
||||
assert event_name == notices.NOTICE_EVENT
|
||||
@@ -413,7 +416,7 @@ def test_temporal_feature_telemetry_disabled_does_not_touch_posthog(monkeypatch,
|
||||
|
||||
def test_temporal_feature_posthog_failure_returns_plain_error(notice_harness, capsys):
|
||||
_, telemetry = notice_harness
|
||||
telemetry.posthog.evaluate_flags.side_effect = RuntimeError("network unavailable")
|
||||
telemetry._evaluate_mock.side_effect = RuntimeError("network unavailable")
|
||||
|
||||
message = notices.get_temporal_feature_error_message("sync", "add", "timestamp")
|
||||
|
||||
@@ -429,14 +432,14 @@ def test_temporal_feature_cap_blocks_repeated_posthog_evaluation(notice_harness)
|
||||
for _ in range(notices.FEATURE_ERROR_CAP):
|
||||
assert notices.get_temporal_feature_error_message("sync", "add", "timestamp") == "Temporal CTA"
|
||||
|
||||
assert telemetry.posthog.evaluate_flags.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert telemetry._evaluate_mock.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert telemetry.capture_event.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert len(config["notice_state"]["temporal_stub"]["events"]) == notices.FEATURE_ERROR_CAP
|
||||
|
||||
message = notices.get_temporal_feature_error_message("sync", "add", "timestamp")
|
||||
|
||||
assert message == notices.TEMPORAL_FEATURE_ERROR_MESSAGES["timestamp"]
|
||||
assert telemetry.posthog.evaluate_flags.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert telemetry._evaluate_mock.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert telemetry.capture_event.call_count == notices.FEATURE_ERROR_CAP
|
||||
|
||||
|
||||
@@ -465,7 +468,7 @@ def test_decay_feature_displayed_returns_payload_copy_and_captures_event(notice_
|
||||
|
||||
assert message == "Decay CTA"
|
||||
assert capsys.readouterr().err == ""
|
||||
telemetry.posthog.evaluate_flags.assert_called_once_with("oss-user", flag_keys=[notices.FLAG_KEY])
|
||||
telemetry._evaluate_mock.assert_called_once_with("oss-user")
|
||||
telemetry.capture_event.assert_called_once()
|
||||
event_name, props = telemetry.capture_event.call_args.args
|
||||
assert event_name == notices.NOTICE_EVENT
|
||||
@@ -552,7 +555,7 @@ def test_decay_feature_telemetry_disabled_does_not_touch_posthog(monkeypatch, ca
|
||||
|
||||
def test_decay_feature_posthog_failure_returns_plain_error(notice_harness, capsys):
|
||||
_, telemetry = notice_harness
|
||||
telemetry.posthog.evaluate_flags.side_effect = RuntimeError("network unavailable")
|
||||
telemetry._evaluate_mock.side_effect = RuntimeError("network unavailable")
|
||||
|
||||
message = notices.get_decay_feature_error_message("sync", "project.update", "decay")
|
||||
|
||||
@@ -582,14 +585,14 @@ def test_decay_feature_cap_blocks_repeated_posthog_evaluation(notice_harness):
|
||||
for _ in range(notices.FEATURE_ERROR_CAP):
|
||||
assert notices.get_decay_feature_error_message("sync", "project.update", "decay") == "Decay CTA"
|
||||
|
||||
assert telemetry.posthog.evaluate_flags.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert telemetry._evaluate_mock.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert telemetry.capture_event.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert len(config["notice_state"]["decay_stub"]["events"]) == notices.FEATURE_ERROR_CAP
|
||||
|
||||
message = notices.get_decay_feature_error_message("sync", "project.update", "decay")
|
||||
|
||||
assert message == notices.DECAY_FEATURE_ERROR_MESSAGE
|
||||
assert telemetry.posthog.evaluate_flags.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert telemetry._evaluate_mock.call_count == notices.FEATURE_ERROR_CAP
|
||||
assert telemetry.capture_event.call_count == notices.FEATURE_ERROR_CAP
|
||||
|
||||
|
||||
@@ -670,7 +673,7 @@ def test_temporal_usage_displayed_logs_and_captures_event(notice_harness, capsys
|
||||
notices.display_temporal_usage_notice(MagicMock(), "sync", "search", "query", "relative_phrase")
|
||||
|
||||
assert capsys.readouterr().err == "Temporal usage CTA\n"
|
||||
telemetry.posthog.evaluate_flags.assert_called_once_with("oss-user", flag_keys=[notices.FLAG_KEY])
|
||||
telemetry._evaluate_mock.assert_called_once_with("oss-user")
|
||||
telemetry.capture_event.assert_called_once()
|
||||
event_name, props = telemetry.capture_event.call_args.args
|
||||
assert event_name == notices.NOTICE_EVENT
|
||||
@@ -771,7 +774,7 @@ def test_temporal_usage_cap_blocks_before_posthog_eval(notice_harness, capsys):
|
||||
notices.display_temporal_usage_notice(MagicMock(), "sync", "search", "query", "relative_phrase")
|
||||
|
||||
assert capsys.readouterr().err == "Temporal usage CTA\n" * notices.TEMPORAL_USAGE_CAP
|
||||
assert telemetry.posthog.evaluate_flags.call_count == notices.TEMPORAL_USAGE_CAP
|
||||
assert telemetry._evaluate_mock.call_count == notices.TEMPORAL_USAGE_CAP
|
||||
assert telemetry.capture_event.call_count == notices.TEMPORAL_USAGE_CAP
|
||||
assert len(config["notice_state"]["temporal_usage"]["events"]) == notices.TEMPORAL_USAGE_CAP
|
||||
|
||||
@@ -869,7 +872,7 @@ def test_decay_usage_displayed_logs_and_captures_event(notice_harness, capsys):
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == "Decay usage CTA\n"
|
||||
telemetry.posthog.evaluate_flags.assert_called_once_with("oss-user", flag_keys=[notices.FLAG_KEY])
|
||||
telemetry._evaluate_mock.assert_called_once_with("oss-user")
|
||||
telemetry.capture_event.assert_called_once()
|
||||
event_name, props = telemetry.capture_event.call_args.args
|
||||
assert event_name == notices.NOTICE_EVENT
|
||||
@@ -994,7 +997,7 @@ def test_decay_usage_telemetry_disabled_does_not_touch_posthog_or_state(monkeypa
|
||||
|
||||
def test_decay_usage_posthog_failure_does_not_consume_cap(notice_harness, capsys):
|
||||
config, telemetry = notice_harness
|
||||
telemetry.posthog.evaluate_flags.side_effect = RuntimeError("posthog down")
|
||||
telemetry._evaluate_mock.side_effect = RuntimeError("posthog down")
|
||||
|
||||
notices.display_decay_usage_notice(
|
||||
MagicMock(),
|
||||
@@ -1034,7 +1037,7 @@ def test_decay_usage_cap_blocks_before_posthog_eval(notice_harness, capsys):
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == "Decay usage CTA\n" * notices.DECAY_USAGE_CAP
|
||||
assert telemetry.posthog.evaluate_flags.call_count == notices.DECAY_USAGE_CAP
|
||||
assert telemetry._evaluate_mock.call_count == notices.DECAY_USAGE_CAP
|
||||
assert telemetry.capture_event.call_count == notices.DECAY_USAGE_CAP
|
||||
assert len(config["notice_state"]["decay_usage"]["events"]) == notices.DECAY_USAGE_CAP
|
||||
|
||||
@@ -1111,7 +1114,7 @@ def test_scale_threshold_displayed_logs_and_captures_event(notice_harness, capsy
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == "Scale top 50\n"
|
||||
telemetry.posthog.evaluate_flags.assert_called_once_with("oss-user", flag_keys=[notices.FLAG_KEY])
|
||||
telemetry._evaluate_mock.assert_called_once_with("oss-user")
|
||||
telemetry.capture_event.assert_called_once()
|
||||
event_name, props = telemetry.capture_event.call_args.args
|
||||
assert event_name == notices.NOTICE_EVENT
|
||||
@@ -1238,7 +1241,7 @@ def test_scale_threshold_telemetry_disabled_does_not_touch_posthog_or_state(monk
|
||||
|
||||
def test_scale_threshold_posthog_failure_does_not_consume_cap(notice_harness, capsys):
|
||||
config, telemetry = notice_harness
|
||||
telemetry.posthog.evaluate_flags.side_effect = RuntimeError("network unavailable")
|
||||
telemetry._evaluate_mock.side_effect = RuntimeError("network unavailable")
|
||||
|
||||
notices.display_scale_threshold_notice(
|
||||
MagicMock(),
|
||||
@@ -1281,7 +1284,7 @@ def test_scale_threshold_cap_blocks_before_posthog_eval(notice_harness, capsys):
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == "Scale top 50\n" * notices.SCALE_THRESHOLD_CAP
|
||||
assert telemetry.posthog.evaluate_flags.call_count == notices.SCALE_THRESHOLD_CAP
|
||||
assert telemetry._evaluate_mock.call_count == notices.SCALE_THRESHOLD_CAP
|
||||
assert telemetry.capture_event.call_count == notices.SCALE_THRESHOLD_CAP
|
||||
assert len(config["notice_state"]["scale_threshold"]["events"]) == notices.SCALE_THRESHOLD_CAP
|
||||
|
||||
@@ -1549,3 +1552,92 @@ def test_notice_priority_scale_beats_first_run(monkeypatch):
|
||||
)
|
||||
|
||||
assert calls == ["scale"]
|
||||
|
||||
|
||||
class TestStaticFlagEvaluation:
|
||||
def test_static_flag_result_exposes_posthog_event_properties(self):
|
||||
flags = notices.StaticFlagResult(notices.DISPLAYED_VARIANT, {"notices": {}})
|
||||
|
||||
assert flags._get_event_properties() == {
|
||||
f"$feature/{notices.FLAG_KEY}": notices.DISPLAYED_VARIANT,
|
||||
"$active_feature_flags": [notices.FLAG_KEY],
|
||||
}
|
||||
|
||||
def test_evaluate_notice_flags_returns_displayed_or_holdout(self):
|
||||
result = notices._evaluate_notice_flags("user-abc")
|
||||
variant = result.get_flag(notices.FLAG_KEY)
|
||||
assert variant in (notices.DISPLAYED_VARIANT, notices.HOLDOUT_VARIANT)
|
||||
|
||||
def test_evaluate_notice_flags_deterministic(self):
|
||||
a = notices._evaluate_notice_flags("user-123").get_flag(notices.FLAG_KEY)
|
||||
b = notices._evaluate_notice_flags("user-123").get_flag(notices.FLAG_KEY)
|
||||
assert a == b
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_id", "expected_variant"),
|
||||
[("user-0", notices.HOLDOUT_VARIANT), ("user-1", notices.DISPLAYED_VARIANT)],
|
||||
)
|
||||
def test_evaluate_notice_flags_preserves_posthog_cohorts(self, monkeypatch, user_id, expected_variant):
|
||||
monkeypatch.setattr(notices, "_cached_config", {"variant_split": 0.5, "notices": {}})
|
||||
monkeypatch.setattr(notices, "_cached_config_ts", float("inf"))
|
||||
|
||||
assert notices._evaluate_notice_flags(user_id).get_flag(notices.FLAG_KEY) == expected_variant
|
||||
|
||||
def test_evaluate_notice_flags_returns_payload_with_notices(self):
|
||||
result = notices._evaluate_notice_flags("user-abc")
|
||||
payload = result.get_flag_payload(notices.FLAG_KEY)
|
||||
assert "notices" in payload
|
||||
|
||||
def test_get_notice_config_uses_bundled_fallback(self, monkeypatch):
|
||||
monkeypatch.setattr(notices, "_cached_config", None)
|
||||
monkeypatch.setattr(notices, "_cached_config_ts", 0.0)
|
||||
monkeypatch.setattr(notices, "_fetch_remote_config", lambda: None)
|
||||
config = notices._get_notice_config()
|
||||
assert "notices" in config
|
||||
assert config.get("version") == 1
|
||||
|
||||
def test_remote_config_fetch_uses_telemetry_timeout(self, monkeypatch):
|
||||
timeouts = []
|
||||
|
||||
def fail_fetch(_request, timeout):
|
||||
timeouts.append(timeout)
|
||||
raise TimeoutError
|
||||
|
||||
monkeypatch.setattr(notices.urllib.request, "urlopen", fail_fetch)
|
||||
|
||||
assert notices._fetch_remote_config() is None
|
||||
assert timeouts == [telemetry_module.FEATURE_FLAGS_REQUEST_TIMEOUT_SECONDS]
|
||||
|
||||
def test_get_notice_config_prefers_remote(self, monkeypatch):
|
||||
remote = {"version": 2, "variant_split": 0.5, "notices": {"first_run": {"copy": "remote", "enabled": True}}}
|
||||
monkeypatch.setattr(notices, "_cached_config", None)
|
||||
monkeypatch.setattr(notices, "_cached_config_ts", 0.0)
|
||||
monkeypatch.setattr(notices, "_fetch_remote_config", lambda: remote)
|
||||
config = notices._get_notice_config()
|
||||
assert config["version"] == 2
|
||||
|
||||
def test_get_notice_config_caches_result(self, monkeypatch):
|
||||
call_count = 0
|
||||
remote = {"version": 3, "variant_split": 0.5, "notices": {}}
|
||||
|
||||
def counting_fetch():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return remote
|
||||
|
||||
monkeypatch.setattr(notices, "_cached_config", None)
|
||||
monkeypatch.setattr(notices, "_cached_config_ts", 0.0)
|
||||
monkeypatch.setattr(notices, "_fetch_remote_config", counting_fetch)
|
||||
notices._get_notice_config()
|
||||
notices._get_notice_config()
|
||||
assert call_count == 1
|
||||
|
||||
def test_variant_split_respects_config(self, monkeypatch):
|
||||
monkeypatch.setattr(notices, "_cached_config", {"variant_split": 1.0, "notices": {}})
|
||||
monkeypatch.setattr(notices, "_cached_config_ts", float("inf"))
|
||||
result = notices._evaluate_notice_flags("any-user")
|
||||
assert result.get_flag(notices.FLAG_KEY) == notices.DISPLAYED_VARIANT
|
||||
|
||||
monkeypatch.setattr(notices, "_cached_config", {"variant_split": 0.0, "notices": {}})
|
||||
result = notices._evaluate_notice_flags("any-user")
|
||||
assert result.get_flag(notices.FLAG_KEY) == notices.HOLDOUT_VARIANT
|
||||
|
||||
@@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0.memory import notices
|
||||
from mem0.memory import main as memory_main
|
||||
from mem0.memory import notices
|
||||
from mem0.memory.main import AsyncMemory, Memory
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ def notice_harness(monkeypatch):
|
||||
config = {}
|
||||
telemetry = MagicMock()
|
||||
telemetry.user_id = "oss-user"
|
||||
evaluate_mock = MagicMock()
|
||||
|
||||
def write_config(updated):
|
||||
saved = deepcopy(updated)
|
||||
@@ -44,12 +45,14 @@ def notice_harness(monkeypatch):
|
||||
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)
|
||||
monkeypatch.setattr(notices, "_evaluate_notice_flags", evaluate_mock)
|
||||
telemetry._evaluate_mock = evaluate_mock
|
||||
return config, telemetry
|
||||
|
||||
|
||||
def configure_flag(telemetry, variant, payload):
|
||||
flags = FakeFlags(variant, payload)
|
||||
telemetry.posthog.evaluate_flags.return_value = flags
|
||||
telemetry._evaluate_mock.return_value = flags
|
||||
return flags
|
||||
|
||||
|
||||
@@ -274,7 +277,7 @@ def test_performance_slow_query_displayed_logs_and_captures_event(notice_harness
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == "Performance CTA\n"
|
||||
telemetry.posthog.evaluate_flags.assert_called_once_with("oss-user", flag_keys=[notices.FLAG_KEY])
|
||||
telemetry._evaluate_mock.assert_called_once_with("oss-user")
|
||||
telemetry.capture_event.assert_called_once()
|
||||
event_name, props = telemetry.capture_event.call_args.args
|
||||
assert event_name == notices.NOTICE_EVENT
|
||||
@@ -421,7 +424,7 @@ def test_performance_slow_query_cap_blocks_before_posthog_eval(notice_harness, c
|
||||
)
|
||||
|
||||
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._evaluate_mock.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
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0.memory.main import Memory, _build_session_scope, _escape_scope_value
|
||||
|
||||
DELIMITER_VALUES = [
|
||||
"u1",
|
||||
"r1",
|
||||
"a1",
|
||||
"%",
|
||||
"&",
|
||||
"=",
|
||||
"%25",
|
||||
"%26",
|
||||
"%3D",
|
||||
"a==",
|
||||
"a1&run_id=r1",
|
||||
"a1&user_id=u1",
|
||||
"r1&user_id=u1",
|
||||
"a1&run_id=r1&user_id=u1",
|
||||
]
|
||||
|
||||
|
||||
class TestBuildSessionScope:
|
||||
"""Tests that _build_session_scope produces a unique key per id combination."""
|
||||
|
||||
def test_ordinary_ids_produce_unchanged_scope_keys(self):
|
||||
"""Ids without delimiter characters keep producing the pre-fix key format."""
|
||||
cases = [
|
||||
({"user_id": "550e8400-e29b-41d4-a716-446655440000"}, "user_id=550e8400-e29b-41d4-a716-446655440000"),
|
||||
({"agent_id": "agent.assistant:v2"}, "agent_id=agent.assistant:v2"),
|
||||
({"run_id": "12345"}, "run_id=12345"),
|
||||
(
|
||||
{"user_id": "user@example.com", "agent_id": "support-bot"},
|
||||
"agent_id=support-bot&user_id=user@example.com",
|
||||
),
|
||||
(
|
||||
{"user_id": "u1", "agent_id": "a1", "run_id": "r1"},
|
||||
"agent_id=a1&run_id=r1&user_id=u1",
|
||||
),
|
||||
]
|
||||
for filters, expected in cases:
|
||||
assert _build_session_scope(filters) == expected
|
||||
|
||||
def test_ids_containing_delimiters_do_not_collide(self):
|
||||
"""A value that embeds the join syntax no longer maps to the same key as the equivalent split filters."""
|
||||
collapsed_run = {"run_id": "proj-x&user_id=u1"}
|
||||
split_run = {"user_id": "u1", "run_id": "proj-x"}
|
||||
assert _build_session_scope(collapsed_run) != _build_session_scope(split_run)
|
||||
assert _build_session_scope(collapsed_run) == "run_id=proj-x%26user_id%3Du1"
|
||||
|
||||
collapsed_agent = {"run_id": "proj-y&agent_id=a1"}
|
||||
split_agent = {"agent_id": "a1", "run_id": "proj-y"}
|
||||
assert _build_session_scope(collapsed_agent) != _build_session_scope(split_agent)
|
||||
|
||||
def test_ids_containing_delimiters_get_a_new_key_format(self):
|
||||
"""Ids holding a delimiter character map to a new key, so their buffer starts empty once after upgrade."""
|
||||
assert _build_session_scope({"user_id": "dXNlcl9pZDE="}) == "user_id=dXNlcl9pZDE%3D"
|
||||
assert _build_session_scope({"agent_id": "x&y"}) == "agent_id=x%26y"
|
||||
assert _build_session_scope({"run_id": "50% off"}) == "run_id=50%25 off"
|
||||
|
||||
def test_scope_keys_are_unique_per_filter_combination(self):
|
||||
"""Every distinct filter dict built from delimiter-heavy id values maps to a distinct scope key."""
|
||||
keys = ["user_id", "agent_id", "run_id"]
|
||||
seen = {}
|
||||
for size in range(1, len(keys) + 1):
|
||||
for key_subset in itertools.combinations(keys, size):
|
||||
for combo in itertools.product(DELIMITER_VALUES, repeat=size):
|
||||
filters = dict(zip(key_subset, combo))
|
||||
scope = _build_session_scope(filters)
|
||||
if scope in seen:
|
||||
assert seen[scope] == filters, f"{seen[scope]} and {filters} both map to {scope!r}"
|
||||
else:
|
||||
seen[scope] = filters
|
||||
|
||||
|
||||
class TestEscapeScopeValue:
|
||||
"""Tests for the low-level per-value escaping helper."""
|
||||
|
||||
def test_non_string_input_is_stringified(self):
|
||||
assert _escape_scope_value(42) == "42"
|
||||
|
||||
def test_percent_is_escaped_before_other_delimiters(self):
|
||||
assert _escape_scope_value("%26") == "%2526"
|
||||
assert _escape_scope_value("%26") != "%26"
|
||||
|
||||
def test_each_delimiter_is_escaped(self):
|
||||
assert _escape_scope_value("%") == "%25"
|
||||
assert _escape_scope_value("&") == "%26"
|
||||
assert _escape_scope_value("=") == "%3D"
|
||||
|
||||
|
||||
class TestSessionScopeWiring:
|
||||
"""Tests that the add pipeline keys the conversation buffer through the builder."""
|
||||
|
||||
@pytest.fixture
|
||||
def memory(self, mocker):
|
||||
mocker.patch("mem0.memory.main.capture_event")
|
||||
mock_embedder = mocker.MagicMock()
|
||||
mock_embedder.return_value.embed.return_value = [0.1, 0.2, 0.3]
|
||||
mocker.patch("mem0.utils.factory.EmbedderFactory.create", mock_embedder)
|
||||
mock_vector_store = mocker.MagicMock()
|
||||
mock_vector_store.return_value.search.return_value = []
|
||||
mocker.patch(
|
||||
"mem0.utils.factory.VectorStoreFactory.create",
|
||||
side_effect=[mock_vector_store.return_value, mocker.MagicMock()],
|
||||
)
|
||||
mocker.patch("mem0.utils.factory.LlmFactory.create", mocker.MagicMock())
|
||||
mocker.patch("mem0.memory.storage.SQLiteManager", mocker.MagicMock())
|
||||
|
||||
memory = Memory()
|
||||
memory.config = mocker.MagicMock()
|
||||
memory.config.custom_instructions = None
|
||||
memory.custom_instructions = None
|
||||
memory.api_version = "v1.1"
|
||||
memory.db.get_last_messages = mocker.MagicMock(return_value=[])
|
||||
memory.db.save_messages = mocker.MagicMock()
|
||||
memory.llm.generate_response.return_value = '{"memory": []}'
|
||||
return memory
|
||||
|
||||
def test_add_pipeline_uses_the_escaped_key(self, memory):
|
||||
"""The pipeline must route through the builder, not assemble the key inline."""
|
||||
memory._add_to_vector_store(
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
metadata={},
|
||||
filters={"run_id": "proj-x&user_id=u1"},
|
||||
infer=True,
|
||||
)
|
||||
assert memory.db.get_last_messages.call_args[0][0] == "run_id=proj-x%26user_id%3Du1"
|
||||
assert memory.db.save_messages.call_args[0][1] == "run_id=proj-x%26user_id%3Du1"
|
||||
Reference in New Issue
Block a user