MP-1 (#368): Port PalaceRoom + Mempalace classes with 22 unit tests MP-2 (#369): L0-L5 retrieval order enforcer with recall-query detection MP-5 (#372): Wake-up protocol (300-900 token context), session scratchpad Modules: - mempalace.py: PalaceRoom + Mempalace dataclasses, factory constructors - retrieval_enforcer.py: Layered memory retrieval (identity → palace → scratch → gitea → skills) - wakeup.py: Session wake-up with caching (5min TTL) - scratchpad.py: JSON-based session notes with palace promotion All 65 tests pass. Pure stdlib + graceful degradation for ONNX issues (#373).
181 lines
6.2 KiB
Python
181 lines
6.2 KiB
Python
"""Tests for the mempalace skill.
|
|
|
|
Validates PalaceRoom, Mempalace class, factory constructors,
|
|
and the analyse_issues entry-point.
|
|
|
|
Refs: Epic #367, Sub-issue #368
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
# Ensure the package is importable from the repo layout
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
|
|
from mempalace.mempalace import Mempalace, PalaceRoom, analyse_issues
|
|
|
|
|
|
# ── PalaceRoom unit tests ─────────────────────────────────────────────────
|
|
|
|
class TestPalaceRoom:
|
|
def test_store_and_retrieve(self):
|
|
room = PalaceRoom(name="test", label="Test Room")
|
|
room.store("key1", 42)
|
|
assert room.retrieve("key1") == 42
|
|
|
|
def test_retrieve_default(self):
|
|
room = PalaceRoom(name="test", label="Test Room")
|
|
assert room.retrieve("missing") is None
|
|
assert room.retrieve("missing", "fallback") == "fallback"
|
|
|
|
def test_summary_format(self):
|
|
room = PalaceRoom(name="test", label="Test Room")
|
|
room.store("repos", 5)
|
|
summary = room.summary()
|
|
assert "## Test Room" in summary
|
|
assert "repos: 5" in summary
|
|
|
|
def test_contents_default_factory_isolation(self):
|
|
"""Each room gets its own dict — no shared mutable default."""
|
|
r1 = PalaceRoom(name="a", label="A")
|
|
r2 = PalaceRoom(name="b", label="B")
|
|
r1.store("x", 1)
|
|
assert r2.retrieve("x") is None
|
|
|
|
def test_entered_at_is_recent(self):
|
|
before = time.time()
|
|
room = PalaceRoom(name="t", label="T")
|
|
after = time.time()
|
|
assert before <= room.entered_at <= after
|
|
|
|
|
|
# ── Mempalace core tests ──────────────────────────────────────────────────
|
|
|
|
class TestMempalace:
|
|
def test_add_and_enter_room(self):
|
|
p = Mempalace(domain="test")
|
|
p.add_room("r1", "Room 1")
|
|
room = p.enter("r1")
|
|
assert room.name == "r1"
|
|
|
|
def test_enter_nonexistent_room_raises(self):
|
|
p = Mempalace()
|
|
with pytest.raises(KeyError, match="No room"):
|
|
p.enter("ghost")
|
|
|
|
def test_store_without_enter_raises(self):
|
|
p = Mempalace()
|
|
p.add_room("r", "R")
|
|
with pytest.raises(RuntimeError, match="Enter a room"):
|
|
p.store("k", "v")
|
|
|
|
def test_store_and_retrieve_via_palace(self):
|
|
p = Mempalace()
|
|
p.add_room("r", "R")
|
|
p.enter("r")
|
|
p.store("count", 10)
|
|
assert p.retrieve("r", "count") == 10
|
|
|
|
def test_retrieve_missing_room_returns_default(self):
|
|
p = Mempalace()
|
|
assert p.retrieve("nope", "key") is None
|
|
assert p.retrieve("nope", "key", 99) == 99
|
|
|
|
def test_render_includes_domain(self):
|
|
p = Mempalace(domain="audit")
|
|
p.add_room("r", "Room")
|
|
p.enter("r")
|
|
p.store("item", "value")
|
|
output = p.render()
|
|
assert "audit" in output
|
|
assert "Room" in output
|
|
|
|
def test_to_dict_structure(self):
|
|
p = Mempalace(domain="test")
|
|
p.add_room("r", "R")
|
|
p.enter("r")
|
|
p.store("a", 1)
|
|
d = p.to_dict()
|
|
assert d["domain"] == "test"
|
|
assert "elapsed_seconds" in d
|
|
assert d["rooms"]["r"] == {"a": 1}
|
|
|
|
def test_to_json_is_valid(self):
|
|
p = Mempalace(domain="j")
|
|
p.add_room("x", "X")
|
|
p.enter("x")
|
|
p.store("v", [1, 2, 3])
|
|
parsed = json.loads(p.to_json())
|
|
assert parsed["rooms"]["x"]["v"] == [1, 2, 3]
|
|
|
|
|
|
# ── Factory constructor tests ─────────────────────────────────────────────
|
|
|
|
class TestFactories:
|
|
def test_for_issue_analysis_rooms(self):
|
|
p = Mempalace.for_issue_analysis()
|
|
assert p.domain == "issue_analysis"
|
|
for key in ("repo_architecture", "assignment_status",
|
|
"triage_priority", "resolution_patterns"):
|
|
p.enter(key) # should not raise
|
|
|
|
def test_for_health_check_rooms(self):
|
|
p = Mempalace.for_health_check()
|
|
assert p.domain == "health_check"
|
|
for key in ("service_topology", "failure_signals", "recovery_history"):
|
|
p.enter(key)
|
|
|
|
def test_for_code_review_rooms(self):
|
|
p = Mempalace.for_code_review()
|
|
assert p.domain == "code_review"
|
|
for key in ("change_scope", "risk_surface",
|
|
"test_coverage", "reviewer_context"):
|
|
p.enter(key)
|
|
|
|
|
|
# ── analyse_issues entry-point tests ──────────────────────────────────────
|
|
|
|
class TestAnalyseIssues:
|
|
SAMPLE_DATA = [
|
|
{"repo": "the-nexus", "open_issues": 40, "assigned": 30, "unassigned": 10},
|
|
{"repo": "timmy-home", "open_issues": 30, "assigned": 25, "unassigned": 5},
|
|
{"repo": "hermes-agent", "open_issues": 20, "assigned": 15, "unassigned": 5},
|
|
{"repo": "empty-repo", "open_issues": 0, "assigned": 0, "unassigned": 0},
|
|
]
|
|
|
|
def test_returns_string(self):
|
|
result = analyse_issues(self.SAMPLE_DATA)
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
def test_contains_room_headers(self):
|
|
result = analyse_issues(self.SAMPLE_DATA)
|
|
assert "Repository Architecture" in result
|
|
assert "Assignment Status" in result
|
|
|
|
def test_coverage_below_target(self):
|
|
result = analyse_issues(self.SAMPLE_DATA, target_assignee_rate=0.90)
|
|
assert "BELOW TARGET" in result
|
|
|
|
def test_coverage_meets_target(self):
|
|
good_data = [
|
|
{"repo": "a", "open_issues": 10, "assigned": 10, "unassigned": 0},
|
|
]
|
|
result = analyse_issues(good_data, target_assignee_rate=0.80)
|
|
assert "OK" in result
|
|
|
|
def test_empty_repos_list(self):
|
|
result = analyse_issues([])
|
|
assert isinstance(result, str)
|
|
|
|
def test_single_repo(self):
|
|
data = [{"repo": "solo", "open_issues": 5, "assigned": 3, "unassigned": 2}]
|
|
result = analyse_issues(data)
|
|
assert "solo" in result or "issue_analysis" in result
|