"""Tests for nexus.mempalace.searcher and nexus.mempalace.config.""" from __future__ import annotations import os from pathlib import Path from unittest.mock import MagicMock, patch import pytest from nexus.mempalace.config import CORE_ROOMS, MEMPALACE_PATH, COLLECTION_NAME from nexus.mempalace.searcher import ( MemPalaceResult, MemPalaceUnavailable, _get_client, search_memories, add_memory, ) # ── MemPalaceResult ────────────────────────────────────────────────────────── def test_result_short_truncates(): r = MemPalaceResult(text="x" * 300, room="forge", wing="bezalel") short = r.short(200) assert len(short) <= 204 # 200 + ellipsis assert short.endswith("…") def test_result_short_no_truncation_needed(): r = MemPalaceResult(text="hello", room="nexus", wing="bezalel") assert r.short() == "hello" def test_result_defaults(): r = MemPalaceResult(text="test", room="general", wing="") assert r.score == 0.0 assert r.source_file == "" assert r.metadata == {} # ── Config ─────────────────────────────────────────────────────────────────── def test_core_rooms_contains_required_rooms(): required = {"forge", "hermes", "nexus", "issues", "experiments"} assert required.issubset(set(CORE_ROOMS)) def test_mempalace_path_env_override(monkeypatch, tmp_path): monkeypatch.setenv("MEMPALACE_PATH", str(tmp_path)) # Re-import to pick up env var (config reads at import time so we patch) import importlib import nexus.mempalace.config as cfg importlib.reload(cfg) assert Path(os.environ["MEMPALACE_PATH"]) == tmp_path importlib.reload(cfg) # restore # ── _get_client ────────────────────────────────────────────────────────────── def test_get_client_raises_when_chromadb_missing(tmp_path): with patch.dict("sys.modules", {"chromadb": None}): with pytest.raises(MemPalaceUnavailable, match="ChromaDB"): _get_client(tmp_path) def test_get_client_raises_when_path_missing(tmp_path): missing = tmp_path / "nonexistent_palace" # chromadb importable but path missing mock_chroma = MagicMock() with patch.dict("sys.modules", {"chromadb": mock_chroma}): with pytest.raises(MemPalaceUnavailable, match="Palace directory"): _get_client(missing) # ── search_memories ────────────────────────────────────────────────────────── def _make_mock_collection(docs, metas=None, distances=None): """Build a mock ChromaDB collection that returns canned results.""" if metas is None: metas = [{"room": "forge", "wing": "bezalel", "source_file": ""} for _ in docs] if distances is None: distances = [0.1 * i for i in range(len(docs))] collection = MagicMock() collection.query.return_value = { "documents": [docs], "metadatas": [metas], "distances": [distances], } return collection def _mock_chroma_client(collection): client = MagicMock() client.get_or_create_collection.return_value = collection return client def test_search_memories_returns_results(tmp_path): docs = ["CI pipeline failed on main", "Forge build log 2026-04-01"] collection = _make_mock_collection(docs) mock_chroma = MagicMock() mock_chroma.PersistentClient.return_value = _mock_chroma_client(collection) with patch.dict("sys.modules", {"chromadb": mock_chroma}): # Palace path must exist for _get_client check (tmp_path / "chroma.sqlite3").touch() results = search_memories("CI failures", palace_path=tmp_path) assert len(results) == 2 assert results[0].room == "forge" assert results[0].wing == "bezalel" assert "CI pipeline" in results[0].text def test_search_memories_empty_collection(tmp_path): collection = MagicMock() collection.query.return_value = {"documents": [[]], "metadatas": [[]], "distances": [[]]} mock_chroma = MagicMock() mock_chroma.PersistentClient.return_value = _mock_chroma_client(collection) with patch.dict("sys.modules", {"chromadb": mock_chroma}): (tmp_path / "chroma.sqlite3").touch() results = search_memories("anything", palace_path=tmp_path) assert results == [] def test_search_memories_with_wing_filter(tmp_path): docs = ["test doc"] collection = _make_mock_collection(docs) mock_chroma = MagicMock() mock_chroma.PersistentClient.return_value = _mock_chroma_client(collection) with patch.dict("sys.modules", {"chromadb": mock_chroma}): (tmp_path / "chroma.sqlite3").touch() search_memories("query", palace_path=tmp_path, wing="bezalel") call_kwargs = collection.query.call_args[1] assert call_kwargs["where"] == {"wing": "bezalel"} def test_search_memories_with_room_filter(tmp_path): collection = _make_mock_collection(["doc"]) mock_chroma = MagicMock() mock_chroma.PersistentClient.return_value = _mock_chroma_client(collection) with patch.dict("sys.modules", {"chromadb": mock_chroma}): (tmp_path / "chroma.sqlite3").touch() search_memories("query", palace_path=tmp_path, room="forge") call_kwargs = collection.query.call_args[1] assert call_kwargs["where"] == {"room": "forge"} def test_search_memories_unavailable(tmp_path): with patch.dict("sys.modules", {"chromadb": None}): with pytest.raises(MemPalaceUnavailable): search_memories("anything", palace_path=tmp_path) # ── add_memory ─────────────────────────────────────────────────────────────── def test_add_memory_returns_id(tmp_path): collection = MagicMock() mock_chroma = MagicMock() mock_chroma.PersistentClient.return_value = _mock_chroma_client(collection) with patch.dict("sys.modules", {"chromadb": mock_chroma}): (tmp_path / "chroma.sqlite3").touch() doc_id = add_memory( "We decided to use ChromaDB.", room="hall_facts", wing="bezalel", palace_path=tmp_path, ) assert isinstance(doc_id, str) assert len(doc_id) == 36 # UUID format collection.add.assert_called_once() call_kwargs = collection.add.call_args[1] assert call_kwargs["documents"] == ["We decided to use ChromaDB."] assert call_kwargs["metadatas"][0]["room"] == "hall_facts" assert call_kwargs["metadatas"][0]["wing"] == "bezalel"