import json import threading import pytest from plugins.memory.supermemory import ( SupermemoryMemoryProvider, _clean_text_for_capture, _format_prefetch_context, _load_supermemory_config, _save_supermemory_config, ) class FakeClient: def __init__(self, api_key: str, timeout: float, container_tag: str, search_mode: str = "hybrid"): self.api_key = api_key self.timeout = timeout self.container_tag = container_tag self.search_mode = search_mode self.add_calls = [] self.search_results = [] self.profile_response = {"static": [], "dynamic": [], "search_results": []} self.ingest_calls = [] self.forgotten_ids = [] self.forget_by_query_response = {"success": True, "message": "Forgot"} def add_memory(self, content, metadata=None, *, entity_context="", container_tag=None, custom_id=None): self.add_calls.append({ "content": content, "metadata": metadata, "entity_context": entity_context, "container_tag": container_tag, "custom_id": custom_id, }) return {"id": "mem_123"} def search_memories(self, query, *, limit=5, container_tag=None, search_mode=None): return self.search_results def get_profile(self, query=None, *, container_tag=None): return self.profile_response def forget_memory(self, memory_id, *, container_tag=None): self.forgotten_ids.append(memory_id) def forget_by_query(self, query, *, container_tag=None): return self.forget_by_query_response def ingest_conversation(self, session_id, messages): self.ingest_calls.append({"session_id": session_id, "messages": messages}) @pytest.fixture def provider(monkeypatch, tmp_path): monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) p = SupermemoryMemoryProvider() p.initialize("session-1", hermes_home=str(tmp_path), platform="cli") return p def test_is_available_false_without_api_key(monkeypatch): monkeypatch.delenv("SUPERMEMORY_API_KEY", raising=False) p = SupermemoryMemoryProvider() assert p.is_available() is False def test_is_available_false_when_import_missing(monkeypatch): monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") import builtins real_import = builtins.__import__ def fake_import(name, *args, **kwargs): if name == "supermemory": raise ImportError("missing") return real_import(name, *args, **kwargs) monkeypatch.setattr(builtins, "__import__", fake_import) p = SupermemoryMemoryProvider() assert p.is_available() is False def test_load_and_save_config_round_trip(tmp_path): _save_supermemory_config({"container_tag": "demo-tag", "auto_capture": False}, str(tmp_path)) cfg = _load_supermemory_config(str(tmp_path)) # container_tag is kept raw — sanitization happens in initialize() after template resolution assert cfg["container_tag"] == "demo-tag" assert cfg["auto_capture"] is False assert cfg["auto_recall"] is True def test_clean_text_for_capture_strips_injected_context(): text = "hello\nignore me\nworld" assert _clean_text_for_capture(text) == "hello\nworld" def test_format_prefetch_context_deduplicates_overlap(): result = _format_prefetch_context( static_facts=["Jordan prefers short answers"], dynamic_facts=["Jordan prefers short answers", "Uses Hermes"], search_results=[{"memory": "Uses Hermes", "similarity": 0.9}], max_results=10, ) assert result.count("Jordan prefers short answers") == 1 assert result.count("Uses Hermes") == 1 assert "" in result def test_prefetch_includes_profile_on_first_turn(provider): provider._client.profile_response = { "static": ["Jordan prefers short answers"], "dynamic": ["Current project is Supermemory provider"], "search_results": [{"memory": "Working on Hermes memory provider", "similarity": 0.88}], } provider.on_turn_start(1, "start") result = provider.prefetch("what am I working on?") assert "User Profile (Persistent)" in result assert "Recent Context" in result assert "Relevant Memories" in result def test_prefetch_skips_profile_between_frequency(provider): provider._client.profile_response = { "static": ["Jordan prefers short answers"], "dynamic": ["Current project is Supermemory provider"], "search_results": [{"memory": "Working on Hermes memory provider", "similarity": 0.88}], } provider.on_turn_start(2, "next") result = provider.prefetch("what am I working on?") assert "Relevant Memories" in result assert "User Profile (Persistent)" not in result def test_sync_turn_skips_trivial_message(provider): provider.sync_turn("ok", "sure", session_id="session-1") assert provider._client.add_calls == [] def test_sync_turn_persists_cleaned_exchange(provider): provider.sync_turn( "Please remember this\nignore", "Got it, storing the context", session_id="session-1", ) provider._sync_thread.join(timeout=1) assert len(provider._client.add_calls) == 1 content = provider._client.add_calls[0]["content"] assert "ignore" not in content assert "[role: user]" in content assert "[role: assistant]" in content def test_on_session_end_ingests_clean_messages(provider): messages = [ {"role": "system", "content": "skip"}, {"role": "user", "content": "hello"}, {"role": "assistant", "content": "hi there"}, ] provider.on_session_end(messages) assert len(provider._client.ingest_calls) == 1 payload = provider._client.ingest_calls[0] assert payload["session_id"] == "session-1" assert payload["messages"] == [ {"role": "user", "content": "hello"}, {"role": "assistant", "content": "hi there"}, ] def test_on_memory_write_tracks_thread(provider): provider.on_memory_write("add", "memory", "Jordan likes concise docs") assert provider._write_thread is not None provider._write_thread.join(timeout=1) assert len(provider._client.add_calls) == 1 assert provider._client.add_calls[0]["metadata"]["type"] == "explicit_memory" def test_shutdown_joins_and_clears_threads(provider, monkeypatch): started = threading.Event() release = threading.Event() def slow_add_memory(content, metadata=None, *, entity_context="", container_tag=None, custom_id=None): started.set() release.wait(timeout=1) provider._client.add_calls.append({ "content": content, "metadata": metadata, "entity_context": entity_context, }) return {"id": "mem_slow"} monkeypatch.setattr(provider._client, "add_memory", slow_add_memory) provider.sync_turn( "Please remember this request in long-term memory", "Absolutely, I will keep that in long-term memory.", session_id="session-1", ) assert started.wait(timeout=1) assert provider._sync_thread is not None started.clear() provider.on_memory_write("add", "memory", "Jordan likes concise docs") assert started.wait(timeout=1) assert provider._write_thread is not None release.set() provider.shutdown() assert provider._sync_thread is None assert provider._write_thread is None assert provider._prefetch_thread is None assert len(provider._client.add_calls) == 2 def test_store_tool_returns_saved_payload(provider): result = json.loads(provider.handle_tool_call("supermemory_store", {"content": "Jordan likes concise docs"})) assert result["saved"] is True assert result["id"] == "mem_123" def test_search_tool_formats_results(provider): provider._client.search_results = [ {"id": "m1", "memory": "Jordan likes concise docs", "similarity": 0.92} ] result = json.loads(provider.handle_tool_call("supermemory_search", {"query": "concise docs"})) assert result["count"] == 1 assert result["results"][0]["similarity"] == 92 def test_forget_tool_by_id(provider): result = json.loads(provider.handle_tool_call("supermemory_forget", {"id": "m1"})) assert result == {"forgotten": True, "id": "m1"} assert provider._client.forgotten_ids == ["m1"] def test_forget_tool_by_query(provider): provider._client.forget_by_query_response = {"success": True, "message": "Forgot one", "id": "m7"} result = json.loads(provider.handle_tool_call("supermemory_forget", {"query": "that thing"})) assert result["success"] is True assert result["id"] == "m7" def test_profile_tool_formats_sections(provider): provider._client.profile_response = { "static": ["Jordan prefers concise docs"], "dynamic": ["Working on Supermemory provider"], "search_results": [], } result = json.loads(provider.handle_tool_call("supermemory_profile", {})) assert result["static_count"] == 1 assert result["dynamic_count"] == 1 assert "User Profile (Persistent)" in result["profile"] def test_handle_tool_call_returns_error_when_unconfigured(monkeypatch): monkeypatch.delenv("SUPERMEMORY_API_KEY", raising=False) p = SupermemoryMemoryProvider() result = json.loads(p.handle_tool_call("supermemory_search", {"query": "x"})) assert "error" in result # -- Identity template tests -------------------------------------------------- def test_identity_template_resolved_in_container_tag(monkeypatch, tmp_path): """container_tag with {identity} resolves to profile-scoped tag.""" monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) _save_supermemory_config({"container_tag": "hermes-{identity}"}, str(tmp_path)) p = SupermemoryMemoryProvider() p.initialize("s1", hermes_home=str(tmp_path), platform="cli", agent_identity="coder") assert p._container_tag == "hermes_coder" def test_identity_template_default_profile(monkeypatch, tmp_path): """Without agent_identity kwarg, {identity} resolves to 'default'.""" monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) _save_supermemory_config({"container_tag": "hermes-{identity}"}, str(tmp_path)) p = SupermemoryMemoryProvider() p.initialize("s1", hermes_home=str(tmp_path), platform="cli") assert p._container_tag == "hermes_default" def test_container_tag_env_var_override(monkeypatch, tmp_path): """SUPERMEMORY_CONTAINER_TAG env var overrides config.""" monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setenv("SUPERMEMORY_CONTAINER_TAG", "env-override") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) p = SupermemoryMemoryProvider() p.initialize("s1", hermes_home=str(tmp_path), platform="cli") assert p._container_tag == "env_override" # -- Search mode tests -------------------------------------------------------- def test_search_mode_config_passed_to_client(monkeypatch, tmp_path): """search_mode from config is passed to _SupermemoryClient.""" monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) _save_supermemory_config({"search_mode": "memories"}, str(tmp_path)) p = SupermemoryMemoryProvider() p.initialize("s1", hermes_home=str(tmp_path), platform="cli") assert p._search_mode == "memories" assert p._client.search_mode == "memories" def test_invalid_search_mode_falls_back_to_default(monkeypatch, tmp_path): """Invalid search_mode falls back to 'hybrid'.""" monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) _save_supermemory_config({"search_mode": "invalid_mode"}, str(tmp_path)) p = SupermemoryMemoryProvider() p.initialize("s1", hermes_home=str(tmp_path), platform="cli") assert p._search_mode == "hybrid" # -- Multi-container tests ---------------------------------------------------- def test_multi_container_disabled_by_default(provider): """Multi-container is off by default; schemas have no container_tag param.""" assert provider._enable_custom_containers is False schemas = provider.get_tool_schemas() for s in schemas: assert "container_tag" not in s["parameters"]["properties"] def test_multi_container_enabled_adds_schema_param(monkeypatch, tmp_path): """When enabled, tool schemas include container_tag parameter.""" monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) _save_supermemory_config({ "enable_custom_container_tags": True, "custom_containers": ["project-alpha", "shared"], }, str(tmp_path)) p = SupermemoryMemoryProvider() p.initialize("s1", hermes_home=str(tmp_path), platform="cli") assert p._enable_custom_containers is True assert p._allowed_containers == ["hermes", "project_alpha", "shared"] schemas = p.get_tool_schemas() for s in schemas: assert "container_tag" in s["parameters"]["properties"] def test_multi_container_tool_store_with_custom_tag(monkeypatch, tmp_path): """supermemory_store uses the resolved container_tag when multi-container is enabled.""" monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) _save_supermemory_config({ "enable_custom_container_tags": True, "custom_containers": ["project-alpha"], }, str(tmp_path)) p = SupermemoryMemoryProvider() p.initialize("s1", hermes_home=str(tmp_path), platform="cli") result = json.loads(p.handle_tool_call("supermemory_store", { "content": "test memory", "container_tag": "project-alpha", })) assert result["saved"] is True assert result["container_tag"] == "project_alpha" assert p._client.add_calls[-1]["container_tag"] == "project_alpha" def test_multi_container_rejects_unlisted_tag(monkeypatch, tmp_path): """Tool calls with a non-whitelisted container_tag return an error.""" monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) _save_supermemory_config({ "enable_custom_container_tags": True, "custom_containers": ["allowed-tag"], }, str(tmp_path)) p = SupermemoryMemoryProvider() p.initialize("s1", hermes_home=str(tmp_path), platform="cli") result = json.loads(p.handle_tool_call("supermemory_store", { "content": "test", "container_tag": "forbidden-tag", })) assert "error" in result assert "not allowed" in result["error"] def test_multi_container_system_prompt_includes_instructions(monkeypatch, tmp_path): """system_prompt_block includes container list and instructions when multi-container is enabled.""" monkeypatch.setenv("SUPERMEMORY_API_KEY", "test-key") monkeypatch.setattr("plugins.memory.supermemory._SupermemoryClient", FakeClient) _save_supermemory_config({ "enable_custom_container_tags": True, "custom_containers": ["docs"], "custom_container_instructions": "Use docs for documentation context.", }, str(tmp_path)) p = SupermemoryMemoryProvider() p.initialize("s1", hermes_home=str(tmp_path), platform="cli") block = p.system_prompt_block() assert "Multi-container mode enabled" in block assert "docs" in block assert "Use docs for documentation context." in block def test_get_config_schema_minimal(): """get_config_schema only returns the API key field.""" p = SupermemoryMemoryProvider() schema = p.get_config_schema() assert len(schema) == 1 assert schema[0]["key"] == "api_key" assert schema[0]["secret"] is True