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).
144 lines
5.9 KiB
Python
144 lines
5.9 KiB
Python
"""Tests for retrieval_enforcer.py.
|
|
|
|
Refs: Epic #367, Sub-issue #369
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
|
|
from mempalace.retrieval_enforcer import (
|
|
is_recall_query,
|
|
load_identity,
|
|
load_scratchpad,
|
|
enforce_retrieval_order,
|
|
search_skills,
|
|
RECALL_PATTERNS,
|
|
)
|
|
|
|
|
|
class TestRecallDetection:
|
|
"""Test the recall-query pattern matcher."""
|
|
|
|
@pytest.mark.parametrize("query", [
|
|
"what did we work on yesterday",
|
|
"status of the mempalace integration",
|
|
"remember the fleet audit results",
|
|
"last time we deployed the nexus",
|
|
"previously you mentioned a CI fix",
|
|
"we discussed the sovereign deployment",
|
|
])
|
|
def test_recall_queries_detected(self, query):
|
|
assert is_recall_query(query) is True
|
|
|
|
@pytest.mark.parametrize("query", [
|
|
"create a new file called test.py",
|
|
"run the test suite",
|
|
"deploy to production",
|
|
"write a function that sums numbers",
|
|
"install the package",
|
|
])
|
|
def test_non_recall_queries_skipped(self, query):
|
|
assert is_recall_query(query) is False
|
|
|
|
|
|
class TestLoadIdentity:
|
|
def test_loads_existing_identity(self, tmp_path):
|
|
identity_file = tmp_path / "identity.txt"
|
|
identity_file.write_text("I am Timmy. A sovereign AI.")
|
|
with patch("mempalace.retrieval_enforcer.IDENTITY_PATH", identity_file):
|
|
result = load_identity()
|
|
assert "Timmy" in result
|
|
|
|
def test_returns_empty_on_missing_file(self, tmp_path):
|
|
identity_file = tmp_path / "nonexistent.txt"
|
|
with patch("mempalace.retrieval_enforcer.IDENTITY_PATH", identity_file):
|
|
result = load_identity()
|
|
assert result == ""
|
|
|
|
def test_truncates_long_identity(self, tmp_path):
|
|
identity_file = tmp_path / "identity.txt"
|
|
identity_file.write_text(" ".join(["word"] * 300))
|
|
with patch("mempalace.retrieval_enforcer.IDENTITY_PATH", identity_file):
|
|
result = load_identity()
|
|
assert result.endswith("...")
|
|
assert len(result.split()) <= 201 # 200 words + "..."
|
|
|
|
|
|
class TestLoadScratchpad:
|
|
def test_loads_valid_scratchpad(self, tmp_path):
|
|
scratch_file = tmp_path / "session123.json"
|
|
scratch_file.write_text(json.dumps({"note": "test value", "key2": 42}))
|
|
with patch("mempalace.retrieval_enforcer.SCRATCHPAD_DIR", tmp_path):
|
|
result = load_scratchpad("session123")
|
|
assert "note: test value" in result
|
|
assert "key2: 42" in result
|
|
|
|
def test_returns_empty_on_missing_file(self, tmp_path):
|
|
with patch("mempalace.retrieval_enforcer.SCRATCHPAD_DIR", tmp_path):
|
|
result = load_scratchpad("nonexistent")
|
|
assert result == ""
|
|
|
|
def test_returns_empty_on_invalid_json(self, tmp_path):
|
|
scratch_file = tmp_path / "bad.json"
|
|
scratch_file.write_text("not valid json{{{")
|
|
with patch("mempalace.retrieval_enforcer.SCRATCHPAD_DIR", tmp_path):
|
|
result = load_scratchpad("bad")
|
|
assert result == ""
|
|
|
|
|
|
class TestEnforceRetrievalOrder:
|
|
def test_skips_non_recall_query(self):
|
|
result = enforce_retrieval_order("create a new file")
|
|
assert result["retrieved_from"] is None
|
|
assert result["tokens"] == 0
|
|
|
|
def test_runs_for_recall_query(self, tmp_path):
|
|
identity_file = tmp_path / "identity.txt"
|
|
identity_file.write_text("I am Timmy.")
|
|
with patch("mempalace.retrieval_enforcer.IDENTITY_PATH", identity_file), \
|
|
patch("mempalace.retrieval_enforcer.search_palace", return_value=""), \
|
|
patch("mempalace.retrieval_enforcer.search_gitea", return_value=""), \
|
|
patch("mempalace.retrieval_enforcer.search_skills", return_value=""):
|
|
result = enforce_retrieval_order("what did we work on yesterday")
|
|
assert "Identity" in result["context"]
|
|
assert "L0" in result["layers_checked"]
|
|
|
|
def test_palace_hit_sets_l1(self, tmp_path):
|
|
identity_file = tmp_path / "identity.txt"
|
|
identity_file.write_text("I am Timmy.")
|
|
with patch("mempalace.retrieval_enforcer.IDENTITY_PATH", identity_file), \
|
|
patch("mempalace.retrieval_enforcer.search_palace", return_value="Found: fleet audit results"), \
|
|
patch("mempalace.retrieval_enforcer.search_gitea", return_value=""):
|
|
result = enforce_retrieval_order("what did we discuss yesterday")
|
|
assert result["retrieved_from"] == "L1"
|
|
assert "Palace Memory" in result["context"]
|
|
|
|
def test_falls_through_to_l5(self, tmp_path):
|
|
identity_file = tmp_path / "nonexistent.txt"
|
|
with patch("mempalace.retrieval_enforcer.IDENTITY_PATH", identity_file), \
|
|
patch("mempalace.retrieval_enforcer.search_palace", return_value=""), \
|
|
patch("mempalace.retrieval_enforcer.search_gitea", return_value=""), \
|
|
patch("mempalace.retrieval_enforcer.search_skills", return_value=""):
|
|
result = enforce_retrieval_order("remember the old deployment", skip_if_not_recall=True)
|
|
assert result["retrieved_from"] == "L5"
|
|
|
|
def test_force_mode_skips_recall_check(self, tmp_path):
|
|
identity_file = tmp_path / "identity.txt"
|
|
identity_file.write_text("I am Timmy.")
|
|
with patch("mempalace.retrieval_enforcer.IDENTITY_PATH", identity_file), \
|
|
patch("mempalace.retrieval_enforcer.search_palace", return_value=""), \
|
|
patch("mempalace.retrieval_enforcer.search_gitea", return_value=""), \
|
|
patch("mempalace.retrieval_enforcer.search_skills", return_value=""):
|
|
result = enforce_retrieval_order("deploy now", skip_if_not_recall=False)
|
|
assert "Identity" in result["context"]
|