"""Tests for Mem0 Local memory provider - ChromaDB-backed, no API key.""" import json import os import tempfile from pathlib import Path from unittest.mock import MagicMock, patch import pytest # Fact extraction tests class TestFactExtraction: """Test the regex-based fact extraction.""" def _extract(self, text): from plugins.memory.mem0_local import _extract_facts return _extract_facts(text) def test_name_extraction(self): facts = self._extract("My name is Alexander Whitestone.") assert any("alexander whitestone" in f["content"].lower() for f in facts) def test_preference_extraction(self): facts = self._extract("I prefer using vim for editing.") assert any("vim" in f["content"].lower() for f in facts) def test_timezone_extraction(self): facts = self._extract("My timezone is America/New_York.") assert any("america/new_york" in f["content"].lower() for f in facts) def test_explicit_remember(self): facts = self._extract("Remember: always use f-strings in Python.") assert len(facts) > 0 def test_correction_extraction(self): facts = self._extract("Actually: the port is 8080, not 3000.") assert len(facts) > 0 def test_empty_input(self): facts = self._extract("") assert facts == [] def test_short_input_ignored(self): facts = self._extract("Hi") assert facts == [] def test_no_crash_on_random_text(self): facts = self._extract("The quick brown fox jumps over the lazy dog. " * 10) assert isinstance(facts, list) # Config tests class TestConfig: """Test configuration loading.""" def test_default_storage_path(self, tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes")) from plugins.memory.mem0_local import _load_config config = _load_config() assert "mem0-local" in config["storage_path"] def test_env_override(self, tmp_path, monkeypatch): custom_path = str(tmp_path / "custom-mem0") monkeypatch.setenv("MEM0_LOCAL_PATH", custom_path) from plugins.memory.mem0_local import _load_config config = _load_config() assert config["storage_path"] == custom_path # Provider interface tests class TestProviderInterface: """Test provider interface methods.""" def test_name(self): from plugins.memory.mem0_local import Mem0LocalProvider provider = Mem0LocalProvider() assert provider.name == "mem0-local" def test_tool_schemas(self): from plugins.memory.mem0_local import Mem0LocalProvider provider = Mem0LocalProvider() schemas = provider.get_tool_schemas() names = {s["name"] for s in schemas} assert names == {"mem0_profile", "mem0_search", "mem0_conclude"} def test_schema_required_params(self): from plugins.memory.mem0_local import Mem0LocalProvider provider = Mem0LocalProvider() schemas = {s["name"]: s for s in provider.get_tool_schemas()} assert "query" in schemas["mem0_search"]["parameters"]["required"] assert "conclusion" in schemas["mem0_conclude"]["parameters"]["required"] # ChromaDB integration tests chromadb = None try: import chromadb except ImportError: pass @pytest.mark.skipif(chromadb is None, reason="chromadb not installed") class TestChromaDBIntegration: """Integration tests with real ChromaDB.""" @pytest.fixture def provider(self, tmp_path, monkeypatch): from plugins.memory.mem0_local import Mem0LocalProvider monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes")) provider = Mem0LocalProvider() provider.initialize("test-session") provider._storage_path = str(tmp_path / "mem0-test") return provider def test_store_and_search(self, provider): result = provider.handle_tool_call("mem0_conclude", {"conclusion": "User prefers Python over JavaScript"}) data = json.loads(result) assert data.get("result") == "Fact stored locally." result = provider.handle_tool_call("mem0_search", {"query": "programming language preference"}) data = json.loads(result) assert data["count"] > 0 assert any("python" in item["memory"].lower() for item in data["results"]) def test_profile_empty(self, provider): result = provider.handle_tool_call("mem0_profile", {}) data = json.loads(result) assert "No memories" in data.get("result", "") or data.get("count", 0) == 0 def test_profile_after_store(self, provider): provider.handle_tool_call("mem0_conclude", {"conclusion": "User name is Alexander"}) provider.handle_tool_call("mem0_conclude", {"conclusion": "User timezone is UTC"}) result = provider.handle_tool_call("mem0_profile", {}) data = json.loads(result) assert data["count"] >= 2 def test_dedup(self, provider): provider.handle_tool_call("mem0_conclude", {"conclusion": "Project uses SQLite"}) provider.handle_tool_call("mem0_conclude", {"conclusion": "Project uses SQLite"}) result = provider.handle_tool_call("mem0_profile", {}) data = json.loads(result) assert data["count"] == 1 def test_search_no_results(self, provider): result = provider.handle_tool_call("mem0_search", {"query": "nonexistent topic xyz123"}) data = json.loads(result) assert data.get("result") == "No relevant memories found." or data.get("count", 0) == 0 def test_sync_turn_extraction(self, provider): provider.sync_turn( "My name is TestUser and I prefer dark mode.", "Hello TestUser! I'll remember your preference.", ) result = provider.handle_tool_call("mem0_profile", {}) data = json.loads(result) assert "count" in data def test_conclude_missing_param(self, provider): result = provider.handle_tool_call("mem0_conclude", {}) data = json.loads(result) assert "error" in data def test_search_missing_query(self, provider): result = provider.handle_tool_call("mem0_search", {}) data = json.loads(result) assert "error" in data