2026-04-07 17:07:02 -03:00
|
|
|
"""
|
|
|
|
|
conftest.py — Shared fixtures for MemPalace tests.
|
|
|
|
|
|
|
|
|
|
Provides isolated palace and knowledge graph instances so tests never
|
|
|
|
|
touch the user's real data or leak temp files on failure.
|
2026-04-07 17:17:57 -03:00
|
|
|
|
|
|
|
|
HOME is redirected to a temp directory at module load time — before any
|
|
|
|
|
mempalace imports — so that module-level initialisations (e.g.
|
|
|
|
|
``_kg = KnowledgeGraph()`` in mcp_server) write to a throwaway location
|
|
|
|
|
instead of the real user profile.
|
2026-04-07 17:07:02 -03:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
|
|
|
|
import tempfile
|
|
|
|
|
|
2026-04-07 17:17:57 -03:00
|
|
|
# ── Isolate HOME before any mempalace imports ──────────────────────────
|
|
|
|
|
_original_env = {}
|
|
|
|
|
_session_tmp = tempfile.mkdtemp(prefix="mempalace_session_")
|
|
|
|
|
|
|
|
|
|
for _var in ("HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH"):
|
|
|
|
|
_original_env[_var] = os.environ.get(_var)
|
|
|
|
|
|
|
|
|
|
os.environ["HOME"] = _session_tmp
|
|
|
|
|
os.environ["USERPROFILE"] = _session_tmp
|
|
|
|
|
os.environ["HOMEDRIVE"] = os.path.splitdrive(_session_tmp)[0] or "C:"
|
|
|
|
|
os.environ["HOMEPATH"] = os.path.splitdrive(_session_tmp)[1] or _session_tmp
|
|
|
|
|
|
|
|
|
|
# Now it is safe to import mempalace modules that trigger initialisation.
|
2026-04-07 17:59:21 -03:00
|
|
|
import chromadb # noqa: E402
|
|
|
|
|
import pytest # noqa: E402
|
2026-04-07 17:07:02 -03:00
|
|
|
|
2026-04-07 17:59:21 -03:00
|
|
|
from mempalace.config import MempalaceConfig # noqa: E402
|
|
|
|
|
from mempalace.knowledge_graph import KnowledgeGraph # noqa: E402
|
2026-04-07 17:07:02 -03:00
|
|
|
|
|
|
|
|
|
2026-04-07 17:19:53 -03:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _reset_mcp_cache():
|
|
|
|
|
"""Reset the MCP server's cached ChromaDB client/collection between tests."""
|
|
|
|
|
|
2026-04-07 17:29:12 -03:00
|
|
|
def _clear_cache():
|
|
|
|
|
try:
|
|
|
|
|
from mempalace import mcp_server
|
|
|
|
|
|
|
|
|
|
mcp_server._client_cache = None
|
|
|
|
|
mcp_server._collection_cache = None
|
|
|
|
|
except (ImportError, AttributeError):
|
|
|
|
|
pass
|
2026-04-25 22:13:27 -07:00
|
|
|
try:
|
|
|
|
|
# Reset the per-process quarantine gate so tests don't leak
|
|
|
|
|
# state through ChromaBackend._quarantined_paths.
|
|
|
|
|
from mempalace.backends.chroma import ChromaBackend
|
|
|
|
|
|
|
|
|
|
ChromaBackend._quarantined_paths.clear()
|
|
|
|
|
except (ImportError, AttributeError):
|
|
|
|
|
pass
|
2026-04-07 17:29:12 -03:00
|
|
|
|
|
|
|
|
_clear_cache()
|
|
|
|
|
yield
|
|
|
|
|
_clear_cache()
|
2026-04-07 17:19:53 -03:00
|
|
|
|
|
|
|
|
|
2026-04-07 17:17:57 -03:00
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
2026-04-07 18:58:25 -03:00
|
|
|
def _isolate_home():
|
2026-04-07 17:17:57 -03:00
|
|
|
"""Ensure HOME points to a temp dir for the entire test session.
|
|
|
|
|
|
|
|
|
|
The env vars were already set at module level (above) so that
|
|
|
|
|
module-level initialisations are captured. This fixture simply
|
|
|
|
|
restores the originals on teardown and cleans up the temp dir.
|
|
|
|
|
"""
|
|
|
|
|
yield
|
|
|
|
|
for var, orig in _original_env.items():
|
|
|
|
|
if orig is None:
|
|
|
|
|
os.environ.pop(var, None)
|
|
|
|
|
else:
|
|
|
|
|
os.environ[var] = orig
|
|
|
|
|
shutil.rmtree(_session_tmp, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
|
2026-04-07 17:07:02 -03:00
|
|
|
@pytest.fixture
|
|
|
|
|
def tmp_dir():
|
|
|
|
|
"""Create and auto-cleanup a temporary directory."""
|
|
|
|
|
d = tempfile.mkdtemp(prefix="mempalace_test_")
|
|
|
|
|
yield d
|
|
|
|
|
shutil.rmtree(d, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def palace_path(tmp_dir):
|
|
|
|
|
"""Path to an empty palace directory inside tmp_dir."""
|
|
|
|
|
p = os.path.join(tmp_dir, "palace")
|
|
|
|
|
os.makedirs(p)
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def config(tmp_dir, palace_path):
|
|
|
|
|
"""A MempalaceConfig pointing at the temp palace."""
|
|
|
|
|
cfg_dir = os.path.join(tmp_dir, "config")
|
|
|
|
|
os.makedirs(cfg_dir)
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
with open(os.path.join(cfg_dir, "config.json"), "w") as f:
|
|
|
|
|
json.dump({"palace_path": palace_path}, f)
|
|
|
|
|
return MempalaceConfig(config_dir=cfg_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def collection(palace_path):
|
|
|
|
|
"""A ChromaDB collection pre-seeded in the temp palace."""
|
|
|
|
|
client = chromadb.PersistentClient(path=palace_path)
|
2026-04-13 18:29:48 -04:00
|
|
|
col = client.get_or_create_collection("mempalace_drawers", metadata={"hnsw:space": "cosine"})
|
2026-04-07 17:44:19 -03:00
|
|
|
yield col
|
|
|
|
|
client.delete_collection("mempalace_drawers")
|
|
|
|
|
del client
|
2026-04-07 17:07:02 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def seeded_collection(collection):
|
|
|
|
|
"""Collection with a handful of representative drawers."""
|
|
|
|
|
collection.add(
|
|
|
|
|
ids=[
|
|
|
|
|
"drawer_proj_backend_aaa",
|
|
|
|
|
"drawer_proj_backend_bbb",
|
|
|
|
|
"drawer_proj_frontend_ccc",
|
|
|
|
|
"drawer_notes_planning_ddd",
|
|
|
|
|
],
|
|
|
|
|
documents=[
|
|
|
|
|
"The authentication module uses JWT tokens for session management. "
|
|
|
|
|
"Tokens expire after 24 hours. Refresh tokens are stored in HttpOnly cookies.",
|
|
|
|
|
"Database migrations are handled by Alembic. We use PostgreSQL 15 "
|
|
|
|
|
"with connection pooling via pgbouncer.",
|
|
|
|
|
"The React frontend uses TanStack Query for server state management. "
|
|
|
|
|
"All API calls go through a centralized fetch wrapper.",
|
|
|
|
|
"Sprint planning: migrate auth to passkeys by Q3. "
|
|
|
|
|
"Evaluate ChromaDB alternatives for vector search.",
|
|
|
|
|
],
|
|
|
|
|
metadatas=[
|
2026-04-07 17:59:21 -03:00
|
|
|
{
|
|
|
|
|
"wing": "project",
|
|
|
|
|
"room": "backend",
|
|
|
|
|
"source_file": "auth.py",
|
|
|
|
|
"chunk_index": 0,
|
|
|
|
|
"added_by": "miner",
|
|
|
|
|
"filed_at": "2026-01-01T00:00:00",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"wing": "project",
|
|
|
|
|
"room": "backend",
|
|
|
|
|
"source_file": "db.py",
|
|
|
|
|
"chunk_index": 0,
|
|
|
|
|
"added_by": "miner",
|
|
|
|
|
"filed_at": "2026-01-02T00:00:00",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"wing": "project",
|
|
|
|
|
"room": "frontend",
|
|
|
|
|
"source_file": "App.tsx",
|
|
|
|
|
"chunk_index": 0,
|
|
|
|
|
"added_by": "miner",
|
|
|
|
|
"filed_at": "2026-01-03T00:00:00",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"wing": "notes",
|
|
|
|
|
"room": "planning",
|
|
|
|
|
"source_file": "sprint.md",
|
|
|
|
|
"chunk_index": 0,
|
|
|
|
|
"added_by": "miner",
|
|
|
|
|
"filed_at": "2026-01-04T00:00:00",
|
|
|
|
|
},
|
2026-04-07 17:07:02 -03:00
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
return collection
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def kg(tmp_dir):
|
|
|
|
|
"""An isolated KnowledgeGraph using a temp SQLite file."""
|
|
|
|
|
db_path = os.path.join(tmp_dir, "test_kg.sqlite3")
|
2026-04-12 07:14:23 +01:00
|
|
|
graph = KnowledgeGraph(db_path=db_path)
|
|
|
|
|
yield graph
|
|
|
|
|
graph.close()
|
2026-04-07 17:07:02 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def seeded_kg(kg):
|
|
|
|
|
"""KnowledgeGraph pre-loaded with sample triples."""
|
|
|
|
|
kg.add_entity("Alice", entity_type="person")
|
|
|
|
|
kg.add_entity("Max", entity_type="person")
|
|
|
|
|
kg.add_entity("swimming", entity_type="activity")
|
|
|
|
|
kg.add_entity("chess", entity_type="activity")
|
|
|
|
|
|
|
|
|
|
kg.add_triple("Alice", "parent_of", "Max", valid_from="2015-04-01")
|
|
|
|
|
kg.add_triple("Max", "does", "swimming", valid_from="2025-01-01")
|
|
|
|
|
kg.add_triple("Max", "does", "chess", valid_from="2024-06-01")
|
2026-04-13 18:29:48 -04:00
|
|
|
kg.add_triple("Alice", "works_at", "Acme Corp", valid_from="2020-01-01", valid_to="2024-12-31")
|
2026-04-07 17:07:02 -03:00
|
|
|
kg.add_triple("Alice", "works_at", "NewCo", valid_from="2025-01-01")
|
|
|
|
|
|
|
|
|
|
return kg
|