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): self.api_key = api_key self.timeout = timeout self.container_tag = container_tag 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=""): self.add_calls.append({ "content": content, "metadata": metadata, "entity_context": entity_context, }) return {"id": "mem_123"} def search_memories(self, query, *, limit=5): return self.search_results def get_profile(self, query=None): return self.profile_response def forget_memory(self, memory_id): self.forgotten_ids.append(memory_id) def forget_by_query(self, query): 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)) 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=""): 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