test: add comprehensive test coverage (35% → 58%, threshold 50%)
Add 180+ new tests across 10 test files covering previously untested modules: - instructions_cli (0% → 100%), hooks_cli (73% → 96%), spellcheck (28% → 84%) - palace_graph (9% → 91%), general_extractor (0% → 92%), entity_detector (0% → 69%) - entity_registry (0% → 70%), room_detector_local (0% → 55%), layers (0% → 28%) - onboarding (0% → 36%) Also fixes Windows encoding bug in onboarding.py (write_text without encoding="utf-8"). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
"""Tests for mempalace.palace_graph — graph traversal layer.
|
||||
|
||||
All ChromaDB access is mocked — no real database needed.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
def _make_fake_collection(metadatas, ids=None):
|
||||
"""Create a mock collection that returns the given metadata in batches."""
|
||||
if ids is None:
|
||||
ids = [f"id_{i}" for i in range(len(metadatas))]
|
||||
|
||||
col = MagicMock()
|
||||
col.count.return_value = len(metadatas)
|
||||
|
||||
def fake_get(limit=1000, offset=0, include=None):
|
||||
batch_meta = metadatas[offset:offset + limit]
|
||||
batch_ids = ids[offset:offset + limit]
|
||||
return {"ids": batch_ids, "metadatas": batch_meta}
|
||||
|
||||
col.get.side_effect = fake_get
|
||||
return col
|
||||
|
||||
|
||||
# Patch chromadb at import time so palace_graph can be imported
|
||||
with patch.dict("sys.modules", {"chromadb": MagicMock()}):
|
||||
from mempalace.palace_graph import (
|
||||
_fuzzy_match,
|
||||
build_graph,
|
||||
find_tunnels,
|
||||
graph_stats,
|
||||
traverse,
|
||||
)
|
||||
|
||||
|
||||
# --- build_graph ---
|
||||
|
||||
|
||||
class TestBuildGraph:
|
||||
def test_empty_collection(self):
|
||||
col = _make_fake_collection([])
|
||||
nodes, edges = build_graph(col=col)
|
||||
assert nodes == {}
|
||||
assert edges == []
|
||||
|
||||
def test_falsy_collection(self):
|
||||
"""When col is explicitly falsy, build_graph returns empty."""
|
||||
nodes, edges = build_graph(col=0)
|
||||
assert nodes == {}
|
||||
assert edges == []
|
||||
|
||||
def test_single_wing_no_edges(self):
|
||||
col = _make_fake_collection([
|
||||
{"room": "auth", "wing": "wing_code", "hall": "security", "date": "2026-01-01"},
|
||||
{"room": "auth", "wing": "wing_code", "hall": "security", "date": "2026-01-02"},
|
||||
])
|
||||
nodes, edges = build_graph(col=col)
|
||||
assert "auth" in nodes
|
||||
assert nodes["auth"]["count"] == 2
|
||||
assert edges == []
|
||||
|
||||
def test_multi_wing_creates_edges(self):
|
||||
col = _make_fake_collection([
|
||||
{"room": "chromadb", "wing": "wing_code", "hall": "databases", "date": "2026-01-01"},
|
||||
{"room": "chromadb", "wing": "wing_project", "hall": "databases", "date": "2026-01-02"},
|
||||
])
|
||||
nodes, edges = build_graph(col=col)
|
||||
assert "chromadb" in nodes
|
||||
assert len(edges) == 1
|
||||
assert edges[0]["wing_a"] == "wing_code"
|
||||
assert edges[0]["wing_b"] == "wing_project"
|
||||
assert edges[0]["hall"] == "databases"
|
||||
|
||||
def test_general_room_excluded(self):
|
||||
col = _make_fake_collection([
|
||||
{"room": "general", "wing": "wing_code", "hall": "misc", "date": ""},
|
||||
])
|
||||
nodes, edges = build_graph(col=col)
|
||||
assert "general" not in nodes
|
||||
|
||||
def test_missing_wing_excluded(self):
|
||||
col = _make_fake_collection([
|
||||
{"room": "orphan", "wing": "", "hall": "misc", "date": ""},
|
||||
])
|
||||
nodes, edges = build_graph(col=col)
|
||||
assert "orphan" not in nodes
|
||||
|
||||
def test_dates_capped_at_five(self):
|
||||
col = _make_fake_collection([
|
||||
{"room": "busy", "wing": "w", "hall": "h", "date": f"2026-01-{i:02d}"}
|
||||
for i in range(1, 10)
|
||||
])
|
||||
nodes, _ = build_graph(col=col)
|
||||
assert len(nodes["busy"]["dates"]) <= 5
|
||||
|
||||
|
||||
# --- traverse ---
|
||||
|
||||
|
||||
class TestTraverse:
|
||||
def _build_col(self):
|
||||
return _make_fake_collection([
|
||||
{"room": "auth", "wing": "wing_code", "hall": "security", "date": "2026-01-01"},
|
||||
{"room": "login", "wing": "wing_code", "hall": "security", "date": "2026-01-01"},
|
||||
{"room": "deploy", "wing": "wing_ops", "hall": "infra", "date": "2026-01-01"},
|
||||
])
|
||||
|
||||
def test_traverse_known_room(self):
|
||||
col = self._build_col()
|
||||
result = traverse("auth", col=col)
|
||||
assert isinstance(result, list)
|
||||
rooms = [r["room"] for r in result]
|
||||
assert "auth" in rooms
|
||||
# login shares wing_code with auth
|
||||
assert "login" in rooms
|
||||
|
||||
def test_traverse_unknown_room(self):
|
||||
col = self._build_col()
|
||||
result = traverse("nonexistent", col=col)
|
||||
assert isinstance(result, dict)
|
||||
assert "error" in result
|
||||
assert "suggestions" in result
|
||||
|
||||
def test_traverse_max_hops(self):
|
||||
col = self._build_col()
|
||||
result = traverse("auth", col=col, max_hops=0)
|
||||
# Only the start room itself at hop 0
|
||||
assert len(result) == 1
|
||||
assert result[0]["room"] == "auth"
|
||||
|
||||
|
||||
# --- find_tunnels ---
|
||||
|
||||
|
||||
class TestFindTunnels:
|
||||
def _build_tunnel_col(self):
|
||||
return _make_fake_collection([
|
||||
{"room": "chromadb", "wing": "wing_code", "hall": "db", "date": "2026-01-01"},
|
||||
{"room": "chromadb", "wing": "wing_project", "hall": "db", "date": "2026-01-02"},
|
||||
{"room": "auth", "wing": "wing_code", "hall": "security", "date": "2026-01-01"},
|
||||
])
|
||||
|
||||
def test_find_all_tunnels(self):
|
||||
col = self._build_tunnel_col()
|
||||
tunnels = find_tunnels(col=col)
|
||||
assert len(tunnels) == 1
|
||||
assert tunnels[0]["room"] == "chromadb"
|
||||
|
||||
def test_find_tunnels_with_wing_filter(self):
|
||||
col = self._build_tunnel_col()
|
||||
tunnels = find_tunnels(wing_a="wing_code", col=col)
|
||||
assert len(tunnels) == 1
|
||||
|
||||
def test_find_tunnels_no_match(self):
|
||||
col = self._build_tunnel_col()
|
||||
tunnels = find_tunnels(wing_a="wing_nonexistent", col=col)
|
||||
assert tunnels == []
|
||||
|
||||
def test_find_tunnels_both_wings(self):
|
||||
col = self._build_tunnel_col()
|
||||
tunnels = find_tunnels(wing_a="wing_code", wing_b="wing_project", col=col)
|
||||
assert len(tunnels) == 1
|
||||
assert tunnels[0]["room"] == "chromadb"
|
||||
|
||||
|
||||
# --- graph_stats ---
|
||||
|
||||
|
||||
class TestGraphStats:
|
||||
def test_empty_graph(self):
|
||||
col = _make_fake_collection([])
|
||||
stats = graph_stats(col=col)
|
||||
assert stats["total_rooms"] == 0
|
||||
assert stats["tunnel_rooms"] == 0
|
||||
assert stats["total_edges"] == 0
|
||||
|
||||
def test_stats_with_data(self):
|
||||
col = _make_fake_collection([
|
||||
{"room": "chromadb", "wing": "wing_code", "hall": "db", "date": "2026-01-01"},
|
||||
{"room": "chromadb", "wing": "wing_project", "hall": "db", "date": "2026-01-02"},
|
||||
{"room": "auth", "wing": "wing_code", "hall": "security", "date": "2026-01-01"},
|
||||
])
|
||||
stats = graph_stats(col=col)
|
||||
assert stats["total_rooms"] == 2
|
||||
assert stats["tunnel_rooms"] == 1
|
||||
assert stats["total_edges"] == 1
|
||||
assert "wing_code" in stats["rooms_per_wing"]
|
||||
|
||||
|
||||
# --- _fuzzy_match ---
|
||||
|
||||
|
||||
class TestFuzzyMatch:
|
||||
def test_exact_substring(self):
|
||||
nodes = {"chromadb-setup": {}, "auth-module": {}, "deploy-config": {}}
|
||||
result = _fuzzy_match("chromadb", nodes)
|
||||
assert "chromadb-setup" in result
|
||||
|
||||
def test_partial_word_match(self):
|
||||
nodes = {"chromadb-setup": {}, "auth-module": {}, "deploy-config": {}}
|
||||
result = _fuzzy_match("auth", nodes)
|
||||
assert "auth-module" in result
|
||||
|
||||
def test_no_match(self):
|
||||
nodes = {"chromadb-setup": {}, "auth-module": {}}
|
||||
result = _fuzzy_match("zzzzz", nodes)
|
||||
assert result == []
|
||||
|
||||
def test_hyphenated_query(self):
|
||||
nodes = {"riley-college-apps": {}, "college-prep": {}}
|
||||
result = _fuzzy_match("riley-college", nodes)
|
||||
assert "riley-college-apps" in result
|
||||
|
||||
def test_max_results(self):
|
||||
nodes = {f"room-{i}": {} for i in range(20)}
|
||||
result = _fuzzy_match("room", nodes, n=3)
|
||||
assert len(result) <= 3
|
||||
Reference in New Issue
Block a user