Spaces:
Paused
Paused
| """Tests for per-user memory scoping via user_id threading. | |
| Verifies that gateway user_id flows from AIAgent -> MemoryManager -> plugins, | |
| so each gateway user gets their own memory bucket instead of sharing a static one. | |
| """ | |
| import json | |
| import os | |
| import pytest | |
| from unittest.mock import MagicMock, patch | |
| from agent.memory_provider import MemoryProvider | |
| from agent.memory_manager import MemoryManager | |
| # --------------------------------------------------------------------------- | |
| # Concrete test provider that records init kwargs | |
| # --------------------------------------------------------------------------- | |
| class RecordingProvider(MemoryProvider): | |
| """Minimal provider that records what initialize() receives.""" | |
| def __init__(self, name="recording"): | |
| self._name = name | |
| self._init_kwargs = {} | |
| self._init_session_id = None | |
| def name(self) -> str: | |
| return self._name | |
| def is_available(self) -> bool: | |
| return True | |
| def initialize(self, session_id: str, **kwargs) -> None: | |
| self._init_session_id = session_id | |
| self._init_kwargs = dict(kwargs) | |
| def system_prompt_block(self) -> str: | |
| return "" | |
| def prefetch(self, query: str, *, session_id: str = "") -> str: | |
| return "" | |
| def sync_turn(self, user_content, assistant_content, *, session_id=""): | |
| pass | |
| def get_tool_schemas(self): | |
| return [] | |
| def handle_tool_call(self, tool_name, args, **kwargs): | |
| return json.dumps({}) | |
| def shutdown(self): | |
| pass | |
| # --------------------------------------------------------------------------- | |
| # MemoryManager user_id threading tests | |
| # --------------------------------------------------------------------------- | |
| class TestMemoryManagerUserIdThreading: | |
| """Verify user_id reaches providers via initialize_all.""" | |
| def test_user_id_forwarded_to_provider(self): | |
| mgr = MemoryManager() | |
| p = RecordingProvider() | |
| mgr.add_provider(p) | |
| mgr.initialize_all( | |
| session_id="sess-123", | |
| platform="telegram", | |
| user_id="tg_user_42", | |
| ) | |
| assert p._init_kwargs.get("user_id") == "tg_user_42" | |
| assert p._init_kwargs.get("platform") == "telegram" | |
| assert p._init_session_id == "sess-123" | |
| def test_no_user_id_when_cli(self): | |
| """CLI sessions should not have user_id in kwargs.""" | |
| mgr = MemoryManager() | |
| p = RecordingProvider() | |
| mgr.add_provider(p) | |
| mgr.initialize_all( | |
| session_id="sess-456", | |
| platform="cli", | |
| ) | |
| assert "user_id" not in p._init_kwargs | |
| assert p._init_kwargs.get("platform") == "cli" | |
| def test_user_id_none_not_forwarded(self): | |
| """Explicit None user_id should not appear in kwargs.""" | |
| mgr = MemoryManager() | |
| p = RecordingProvider() | |
| mgr.add_provider(p) | |
| # Simulates what happens when AIAgent passes user_id=None | |
| # (the agent code only adds user_id to kwargs when it's truthy) | |
| mgr.initialize_all( | |
| session_id="sess-789", | |
| platform="discord", | |
| ) | |
| assert "user_id" not in p._init_kwargs | |
| def test_multiple_providers_all_receive_user_id(self): | |
| from agent.builtin_memory_provider import BuiltinMemoryProvider | |
| mgr = MemoryManager() | |
| # Use builtin + one external (MemoryManager only allows one external) | |
| builtin = BuiltinMemoryProvider() | |
| ext = RecordingProvider("external") | |
| mgr.add_provider(builtin) | |
| mgr.add_provider(ext) | |
| mgr.initialize_all( | |
| session_id="sess-multi", | |
| platform="slack", | |
| user_id="slack_U12345", | |
| ) | |
| assert ext._init_kwargs.get("user_id") == "slack_U12345" | |
| assert ext._init_kwargs.get("platform") == "slack" | |
| # --------------------------------------------------------------------------- | |
| # Mem0 provider user_id tests | |
| # --------------------------------------------------------------------------- | |
| class TestMem0UserIdScoping: | |
| """Verify Mem0 plugin uses gateway user_id when provided.""" | |
| def test_gateway_user_id_overrides_default(self): | |
| """When user_id is passed via kwargs, it should override the config default.""" | |
| from plugins.memory.mem0 import Mem0MemoryProvider | |
| provider = Mem0MemoryProvider() | |
| # Mock _load_config to return a config with default user_id | |
| with patch("plugins.memory.mem0._load_config", return_value={ | |
| "api_key": "test-key", | |
| "user_id": "hermes-user", | |
| "agent_id": "hermes", | |
| "rerank": True, | |
| }): | |
| provider.initialize(session_id="test-sess", user_id="tg_user_99") | |
| assert provider._user_id == "tg_user_99" | |
| def test_no_user_id_falls_back_to_config(self): | |
| """Without user_id in kwargs, should use config default.""" | |
| from plugins.memory.mem0 import Mem0MemoryProvider | |
| provider = Mem0MemoryProvider() | |
| with patch("plugins.memory.mem0._load_config", return_value={ | |
| "api_key": "test-key", | |
| "user_id": "custom-default", | |
| "agent_id": "hermes", | |
| "rerank": True, | |
| }): | |
| provider.initialize(session_id="test-sess") | |
| assert provider._user_id == "custom-default" | |
| def test_no_user_id_no_config_uses_hermes_user(self): | |
| """Without user_id or config override, should default to 'hermes-user'.""" | |
| from plugins.memory.mem0 import Mem0MemoryProvider | |
| provider = Mem0MemoryProvider() | |
| with patch("plugins.memory.mem0._load_config", return_value={ | |
| "api_key": "test-key", | |
| "agent_id": "hermes", | |
| "rerank": True, | |
| }): | |
| provider.initialize(session_id="test-sess") | |
| assert provider._user_id == "hermes-user" | |
| def test_different_users_get_different_ids(self): | |
| """Two providers initialized with different user_ids should be scoped differently.""" | |
| from plugins.memory.mem0 import Mem0MemoryProvider | |
| p1 = Mem0MemoryProvider() | |
| p2 = Mem0MemoryProvider() | |
| with patch("plugins.memory.mem0._load_config", return_value={ | |
| "api_key": "test-key", | |
| "user_id": "hermes-user", | |
| "agent_id": "hermes", | |
| "rerank": True, | |
| }): | |
| p1.initialize(session_id="sess-1", user_id="alice_123") | |
| p2.initialize(session_id="sess-2", user_id="bob_456") | |
| assert p1._user_id == "alice_123" | |
| assert p2._user_id == "bob_456" | |
| assert p1._user_id != p2._user_id | |
| # --------------------------------------------------------------------------- | |
| # Honcho provider user_id tests | |
| # --------------------------------------------------------------------------- | |
| class TestHonchoUserIdScoping: | |
| """Verify Honcho plugin uses gateway user_id for peer_name when provided.""" | |
| def test_gateway_user_id_overrides_peer_name(self): | |
| """When user_id is in kwargs, cfg.peer_name should be overridden.""" | |
| from plugins.memory.honcho import HonchoMemoryProvider | |
| provider = HonchoMemoryProvider() | |
| # Create a mock config with a static peer_name | |
| mock_cfg = MagicMock() | |
| mock_cfg.enabled = True | |
| mock_cfg.api_key = "test-key" | |
| mock_cfg.base_url = None | |
| mock_cfg.peer_name = "static-user" | |
| mock_cfg.recall_mode = "tools" # Use tools mode to defer session init | |
| with patch( | |
| "plugins.memory.honcho.client.HonchoClientConfig.from_global_config", | |
| return_value=mock_cfg, | |
| ): | |
| provider.initialize( | |
| session_id="test-sess", | |
| user_id="discord_user_789", | |
| platform="discord", | |
| ) | |
| # The config's peer_name should have been overridden with the user_id | |
| assert mock_cfg.peer_name == "discord_user_789" | |
| def test_no_user_id_preserves_config_peer_name(self): | |
| """Without user_id, the config peer_name should be preserved.""" | |
| from plugins.memory.honcho import HonchoMemoryProvider | |
| provider = HonchoMemoryProvider() | |
| mock_cfg = MagicMock() | |
| mock_cfg.enabled = True | |
| mock_cfg.api_key = "test-key" | |
| mock_cfg.base_url = None | |
| mock_cfg.peer_name = "my-custom-peer" | |
| mock_cfg.recall_mode = "tools" | |
| with patch( | |
| "plugins.memory.honcho.client.HonchoClientConfig.from_global_config", | |
| return_value=mock_cfg, | |
| ): | |
| provider.initialize( | |
| session_id="test-sess", | |
| platform="cli", | |
| ) | |
| # peer_name should not have been overridden | |
| assert mock_cfg.peer_name == "my-custom-peer" | |
| # --------------------------------------------------------------------------- | |
| # AIAgent user_id propagation test | |
| # --------------------------------------------------------------------------- | |
| class TestAIAgentUserIdPropagation: | |
| """Verify AIAgent stores user_id and passes it to memory init kwargs.""" | |
| def test_user_id_stored_on_agent(self): | |
| """AIAgent should store user_id as instance attribute.""" | |
| with patch.dict(os.environ, {"HERMES_HOME": "/tmp/test_hermes"}): | |
| from run_agent import AIAgent | |
| agent = object.__new__(AIAgent) | |
| # Manually set the attribute as __init__ does | |
| agent._user_id = "test_user_42" | |
| assert agent._user_id == "test_user_42" | |
| def test_user_id_none_by_default(self): | |
| """AIAgent should have None user_id when not provided (CLI mode).""" | |
| with patch.dict(os.environ, {"HERMES_HOME": "/tmp/test_hermes"}): | |
| from run_agent import AIAgent | |
| agent = object.__new__(AIAgent) | |
| agent._user_id = None | |
| assert agent._user_id is None | |