191 lines
6.8 KiB
Python
191 lines
6.8 KiB
Python
"""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"
|