From fbff2a9356a015048994b486adf698caf51f9772 Mon Sep 17 00:00:00 2001 From: Perplexity Computer Date: Tue, 7 Apr 2026 14:14:02 +0000 Subject: [PATCH] test(mempalace): add comprehensive unit tests for mempalace skill 22 tests covering PalaceRoom, Mempalace core, factory constructors, and analyse_issues entry-point. Import guard gracefully skips if skill not yet merged (PR #191). Refs: #190, #191, #200 --- tests/skills/test_mempalace.py | 183 +++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 tests/skills/test_mempalace.py diff --git a/tests/skills/test_mempalace.py b/tests/skills/test_mempalace.py new file mode 100644 index 000000000..8487b6927 --- /dev/null +++ b/tests/skills/test_mempalace.py @@ -0,0 +1,183 @@ +"""Tests for the mempalace skill (skills/memory/mempalace.py). + +Validates PalaceRoom, Mempalace class, factory constructors, +and the analyse_issues entry-point. + +Refs: hermes-agent PR #191, Issue #190 +""" + +from __future__ import annotations + +import json +import time + +import pytest + +# --------------------------------------------------------------------------- +# Import guard — the skill lives in skills/memory/mempalace.py which may +# not be on sys.path in every CI layout. We add a fallback so the tests +# are importable even before the skill is merged to main. +# --------------------------------------------------------------------------- +try: + from skills.memory.mempalace import Mempalace, PalaceRoom, analyse_issues +except ImportError: + pytest.skip("mempalace skill not yet installed", allow_module_level=True) + + +# ── 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 -- 2.43.0