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,313 @@
|
||||
"""Tests for mempalace.entity_registry."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from mempalace.entity_registry import (
|
||||
COMMON_ENGLISH_WORDS,
|
||||
PERSON_CONTEXT_PATTERNS,
|
||||
EntityRegistry,
|
||||
)
|
||||
|
||||
|
||||
# ── COMMON_ENGLISH_WORDS ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_common_english_words_has_expected_entries():
|
||||
assert "ever" in COMMON_ENGLISH_WORDS
|
||||
assert "grace" in COMMON_ENGLISH_WORDS
|
||||
assert "will" in COMMON_ENGLISH_WORDS
|
||||
assert "may" in COMMON_ENGLISH_WORDS
|
||||
assert "monday" in COMMON_ENGLISH_WORDS
|
||||
|
||||
|
||||
def test_common_english_words_is_lowercase():
|
||||
for word in COMMON_ENGLISH_WORDS:
|
||||
assert word == word.lower(), f"{word} should be lowercase"
|
||||
|
||||
|
||||
# ── PERSON_CONTEXT_PATTERNS ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_person_context_patterns_is_nonempty():
|
||||
assert len(PERSON_CONTEXT_PATTERNS) > 0
|
||||
|
||||
|
||||
# ── EntityRegistry creation and empty state ─────────────────────────────
|
||||
|
||||
|
||||
def test_load_from_nonexistent_dir(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
assert registry.people == {}
|
||||
assert registry.projects == []
|
||||
assert registry.mode == "personal"
|
||||
assert registry.ambiguous_flags == []
|
||||
|
||||
|
||||
def test_save_and_load_roundtrip(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="work",
|
||||
people=[{"name": "Alice", "relationship": "colleague", "context": "work"}],
|
||||
projects=["MemPalace"],
|
||||
)
|
||||
# Load again from same dir
|
||||
loaded = EntityRegistry.load(config_dir=tmp_path)
|
||||
assert loaded.mode == "work"
|
||||
assert "Alice" in loaded.people
|
||||
assert "MemPalace" in loaded.projects
|
||||
|
||||
|
||||
def test_save_creates_file(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.save()
|
||||
assert (tmp_path / "entity_registry.json").exists()
|
||||
|
||||
|
||||
# ── seed ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_seed_registers_people(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[
|
||||
{"name": "Riley", "relationship": "daughter", "context": "personal"},
|
||||
{"name": "Devon", "relationship": "friend", "context": "personal"},
|
||||
],
|
||||
projects=["MemPalace"],
|
||||
)
|
||||
assert "Riley" in registry.people
|
||||
assert "Devon" in registry.people
|
||||
assert registry.people["Riley"]["relationship"] == "daughter"
|
||||
assert registry.people["Riley"]["source"] == "onboarding"
|
||||
assert registry.people["Riley"]["confidence"] == 1.0
|
||||
|
||||
|
||||
def test_seed_registers_projects(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(mode="work", people=[], projects=["Acme", "Widget"])
|
||||
assert registry.projects == ["Acme", "Widget"]
|
||||
|
||||
|
||||
def test_seed_sets_mode(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(mode="combo", people=[], projects=[])
|
||||
assert registry.mode == "combo"
|
||||
|
||||
|
||||
def test_seed_flags_ambiguous_names(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[
|
||||
{"name": "Grace", "relationship": "friend", "context": "personal"},
|
||||
{"name": "Riley", "relationship": "daughter", "context": "personal"},
|
||||
],
|
||||
projects=[],
|
||||
)
|
||||
assert "grace" in registry.ambiguous_flags
|
||||
# Riley is not a common English word
|
||||
assert "riley" not in registry.ambiguous_flags
|
||||
|
||||
|
||||
def test_seed_with_aliases(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[{"name": "Maxwell", "relationship": "friend", "context": "personal"}],
|
||||
projects=[],
|
||||
aliases={"Max": "Maxwell"},
|
||||
)
|
||||
assert "Maxwell" in registry.people
|
||||
assert "Max" in registry.people
|
||||
assert registry.people["Max"].get("canonical") == "Maxwell"
|
||||
|
||||
|
||||
def test_seed_skips_empty_names(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[{"name": "", "relationship": "", "context": "personal"}],
|
||||
projects=[],
|
||||
)
|
||||
assert len(registry.people) == 0
|
||||
|
||||
|
||||
# ── lookup ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_lookup_known_person(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[{"name": "Riley", "relationship": "daughter", "context": "personal"}],
|
||||
projects=[],
|
||||
)
|
||||
result = registry.lookup("Riley")
|
||||
assert result["type"] == "person"
|
||||
assert result["confidence"] == 1.0
|
||||
assert result["name"] == "Riley"
|
||||
|
||||
|
||||
def test_lookup_known_project(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(mode="work", people=[], projects=["MemPalace"])
|
||||
result = registry.lookup("MemPalace")
|
||||
assert result["type"] == "project"
|
||||
assert result["confidence"] == 1.0
|
||||
|
||||
|
||||
def test_lookup_unknown_word(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(mode="personal", people=[], projects=[])
|
||||
result = registry.lookup("Xyzzy")
|
||||
assert result["type"] == "unknown"
|
||||
assert result["confidence"] == 0.0
|
||||
|
||||
|
||||
def test_lookup_case_insensitive(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[{"name": "Riley", "relationship": "daughter", "context": "personal"}],
|
||||
projects=[],
|
||||
)
|
||||
result = registry.lookup("riley")
|
||||
assert result["type"] == "person"
|
||||
|
||||
|
||||
def test_lookup_alias(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[{"name": "Maxwell", "relationship": "friend", "context": "personal"}],
|
||||
projects=[],
|
||||
aliases={"Max": "Maxwell"},
|
||||
)
|
||||
result = registry.lookup("Max")
|
||||
assert result["type"] == "person"
|
||||
|
||||
|
||||
# ── disambiguation ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_lookup_ambiguous_word_as_person(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[{"name": "Grace", "relationship": "friend", "context": "personal"}],
|
||||
projects=[],
|
||||
)
|
||||
result = registry.lookup("Grace", context="I went with Grace today")
|
||||
assert result["type"] == "person"
|
||||
|
||||
|
||||
def test_lookup_ambiguous_word_as_concept(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[{"name": "Ever", "relationship": "friend", "context": "personal"}],
|
||||
projects=[],
|
||||
)
|
||||
result = registry.lookup("Ever", context="have you ever tried this")
|
||||
assert result["type"] == "concept"
|
||||
|
||||
|
||||
# ── research (Wikipedia) — mocked ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_research_caches_result(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(mode="personal", people=[], projects=[])
|
||||
|
||||
mock_result = {
|
||||
"inferred_type": "person",
|
||||
"confidence": 0.80,
|
||||
"wiki_summary": "Saoirse is an Irish given name.",
|
||||
"wiki_title": "Saoirse",
|
||||
}
|
||||
|
||||
with patch("mempalace.entity_registry._wikipedia_lookup", return_value=mock_result):
|
||||
result = registry.research("Saoirse", auto_confirm=True)
|
||||
assert result["inferred_type"] == "person"
|
||||
|
||||
# Second call should use cache, not call Wikipedia again
|
||||
with patch(
|
||||
"mempalace.entity_registry._wikipedia_lookup",
|
||||
side_effect=AssertionError("should not be called"),
|
||||
):
|
||||
cached = registry.research("Saoirse")
|
||||
assert cached["inferred_type"] == "person"
|
||||
|
||||
|
||||
def test_confirm_research_adds_to_people(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(mode="personal", people=[], projects=[])
|
||||
|
||||
mock_result = {
|
||||
"inferred_type": "person",
|
||||
"confidence": 0.80,
|
||||
"wiki_summary": "Saoirse is a name",
|
||||
"wiki_title": "Saoirse",
|
||||
}
|
||||
with patch("mempalace.entity_registry._wikipedia_lookup", return_value=mock_result):
|
||||
registry.research("Saoirse", auto_confirm=False)
|
||||
|
||||
registry.confirm_research("Saoirse", entity_type="person", relationship="friend")
|
||||
assert "Saoirse" in registry.people
|
||||
assert registry.people["Saoirse"]["source"] == "wiki"
|
||||
|
||||
|
||||
# ── extract_people_from_query ───────────────────────────────────────────
|
||||
|
||||
|
||||
def test_extract_people_from_query(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[
|
||||
{"name": "Riley", "relationship": "daughter", "context": "personal"},
|
||||
{"name": "Devon", "relationship": "friend", "context": "personal"},
|
||||
],
|
||||
projects=[],
|
||||
)
|
||||
found = registry.extract_people_from_query("What did Riley say about the weather?")
|
||||
assert "Riley" in found
|
||||
assert "Devon" not in found
|
||||
|
||||
|
||||
# ── extract_unknown_candidates ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_extract_unknown_candidates(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(mode="personal", people=[], projects=[])
|
||||
unknowns = registry.extract_unknown_candidates("Saoirse went to the store")
|
||||
assert "Saoirse" in unknowns
|
||||
|
||||
|
||||
def test_extract_unknown_candidates_skips_known(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[{"name": "Riley", "relationship": "daughter", "context": "personal"}],
|
||||
projects=[],
|
||||
)
|
||||
unknowns = registry.extract_unknown_candidates("Riley went to the store")
|
||||
assert "Riley" not in unknowns
|
||||
|
||||
|
||||
# ── summary ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_summary(tmp_path):
|
||||
registry = EntityRegistry.load(config_dir=tmp_path)
|
||||
registry.seed(
|
||||
mode="personal",
|
||||
people=[{"name": "Riley", "relationship": "daughter", "context": "personal"}],
|
||||
projects=["MemPalace"],
|
||||
)
|
||||
s = registry.summary()
|
||||
assert "personal" in s
|
||||
assert "Riley" in s
|
||||
assert "MemPalace" in s
|
||||
Reference in New Issue
Block a user