Files
plugin-registry/plugins/mempalace-indexer/tests/test_after_turn.py
T
ixoblakp 8dec8f6038 Initial Arka plugin registry: Official plugins mirror + Arka-signed index
11 plugins from github.com/librefang/librefang-registry plugins/.
index.json / index.json.sig are signed with Arka's Ed25519 key
(not upstream stats.librefang.ai). Private key is not in this repo.
2026-09-01 10:22:07 +03:00

277 lines
10 KiB
Python

"""Tests for the after_turn hook (stdin/stdout interface)."""
import importlib.util
import json
import subprocess
import sys
import tempfile
from pathlib import Path
HOOK = Path(__file__).parent.parent / "hooks" / "after_turn.py"
def run_hook(payload: dict, env=None) -> dict:
import os
run_env = os.environ.copy()
if env:
run_env.update(env)
result = subprocess.run(
[sys.executable, str(HOOK)],
input=json.dumps(payload),
capture_output=True,
text=True,
env=run_env,
)
return json.loads(result.stdout.strip())
def _load_module(env_overrides=None):
import os
saved = {}
if env_overrides:
for k, v in env_overrides.items():
saved[k] = os.environ.get(k)
os.environ[k] = v
spec = importlib.util.spec_from_file_location("after_turn", HOOK)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
for k, v in saved.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
return mod
_mod = _load_module()
# ---------------------------------------------------------------------------
# Skip cases
# ---------------------------------------------------------------------------
def test_empty_messages_skipped():
out = run_hook({"type": "after_turn", "agent_id": "a1", "messages": []})
assert out["status"] == "skip"
assert out["reason"] == "no messages"
def test_bad_json_skipped():
result = subprocess.run(
[sys.executable, str(HOOK)],
input="not json",
capture_output=True,
text=True,
)
out = json.loads(result.stdout.strip())
assert out["status"] == "skip"
assert out["reason"] == "bad input"
def test_short_exchange_skipped():
messages = [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "hello"},
]
out = run_hook({"type": "after_turn", "agent_id": "a1", "messages": messages})
assert out["status"] == "skip"
assert out["reason"] == "too short"
def test_irrelevant_exchange_skipped():
messages = [
{"role": "user", "content": "What is the capital of France and why is it historically significant?"},
{"role": "assistant", "content": "Paris has been the capital since the 10th century and is the cultural hub."},
]
out = run_hook({"type": "after_turn", "agent_id": "a1", "messages": messages})
assert out["status"] == "skip"
assert out["reason"] == "not relevant"
def test_agent_mempalace_tool_call_skipped():
messages = [
{"role": "user", "content": "Save this to my memory please."},
{"role": "tool", "content": '{"tool": "mcp_mempalace_add_drawer", "result": "ok"}'},
{"role": "assistant", "content": "Done, I saved it using mcp_mempalace."},
]
out = run_hook({"type": "after_turn", "agent_id": "a1", "messages": messages})
assert out["status"] == "skip"
assert out["reason"] == "agent used mcp_mempalace"
def test_user_mentioning_mempalace_not_skipped():
"""User saying 'mcp_mempalace' should NOT trigger the dedup skip."""
messages = [
{"role": "user", "content": "Can you use mcp_mempalace to save my dentist appointment on April 15th?"},
{"role": "assistant", "content": "I will remember your dentist appointment on April 15th."},
]
out = run_hook({"type": "after_turn", "agent_id": "a1", "messages": messages})
assert out.get("reason") != "agent used mcp_mempalace"
# ---------------------------------------------------------------------------
# Code block stripping
# ---------------------------------------------------------------------------
def test_noise_re_does_not_match_common_prose():
"""'exception' and 'traceback' as normal words must not trigger noise filter."""
assert not _mod.NOISE_RE.search("There's no exception to this rule.")
assert not _mod.NOISE_RE.search("Let me traceback the history of this decision.")
def test_noise_re_matches_python_traceback():
assert _mod.NOISE_RE.search("Traceback (most recent call last):\n File foo.py")
def test_noise_re_matches_error_line():
assert _mod.NOISE_RE.search("Exception: something went wrong")
assert _mod.NOISE_RE.search("Error: connection refused")
def test_code_block_stripped_not_whole_message_skipped():
"""A message with a code block but meaningful surrounding text should not be skipped."""
text = "My email is alice@example.com. Here is the script:\n```bash\necho hello\n```\nRun it daily."
result = _mod._strip_code_blocks(text)
assert "alice@example.com" in result
assert "```" not in result
assert "[code]" in result
def test_pure_code_block_skipped_after_strip():
"""A message that is only a code block becomes empty after stripping → too short → skip."""
messages = [
{"role": "user", "content": "here's the script"},
{"role": "assistant", "content": "```python\nfor i in range(10):\n print(i)\n```"},
]
out = run_hook({"type": "after_turn", "agent_id": "a1", "messages": messages})
# Should be skipped due to length or irrelevance after stripping
assert out["status"] == "skip"
# ---------------------------------------------------------------------------
# Multi-room classification
# ---------------------------------------------------------------------------
def test_classify_single_room():
assert _mod._classify_rooms("Her email is alice@example.com") == [("people", "contacts")]
def test_classify_multiple_rooms():
# "meeting" → calendar, "email" → contacts — both should match
rooms = _mod._classify_rooms("Schedule a meeting with alice@example.com next Tuesday")
assert ("people", "contacts") in rooms
assert ("time", "calendar") in rooms
def test_classify_finance():
assert _mod._classify_rooms("Invoice for $500 is due next week") == [("finance", "transactions")]
def test_classify_logistics():
assert _mod._classify_rooms("The shipment arrived today") == [("logistics", "orders")]
def test_classify_decisions():
assert _mod._classify_rooms("We decided to use Postgres going forward") == [("knowledge", "decisions")]
def test_classify_default_fallback():
assert _mod._classify_rooms("The sky is blue and the grass is green.") == [("default", "sessions")]
# ---------------------------------------------------------------------------
# Content deduplication
# ---------------------------------------------------------------------------
def test_duplicate_detection():
with tempfile.TemporaryDirectory() as tmpdir:
mod = _load_module({"MEMPALACE_PALACE_PATH": tmpdir})
text = "I have a dentist appointment on April 15th with Dr. Smith."
assert mod._is_duplicate(text, "agent-1") is False # first time: not a duplicate
assert mod._is_duplicate(text, "agent-1") is True # second time: duplicate
def test_different_content_not_duplicate():
with tempfile.TemporaryDirectory() as tmpdir:
mod = _load_module({"MEMPALACE_PALACE_PATH": tmpdir})
assert mod._is_duplicate("dentist appointment April 15th", "agent-1") is False
assert mod._is_duplicate("meeting with Bob on Friday afternoon", "agent-1") is False
def test_dedup_is_per_agent():
"""Two different agents should have independent dedup stores."""
with tempfile.TemporaryDirectory() as tmpdir:
mod = _load_module({"MEMPALACE_PALACE_PATH": tmpdir})
text = "I have a dentist appointment on April 15th with Dr. Smith."
mod._is_duplicate(text, "agent-1") # agent-1 saves it
assert mod._is_duplicate(text, "agent-2") is False # agent-2 has not seen it
def test_dedup_rolling_max():
with tempfile.TemporaryDirectory() as tmpdir:
mod = _load_module({"MEMPALACE_PALACE_PATH": tmpdir, "MEMPALACE_DEDUP_MAX": "3"})
for i in range(5):
mod._is_duplicate(f"unique content number {i} with enough chars to matter", "agent-1")
seen = json.loads((Path(tmpdir) / ".after_turn_seen_agent-1.json").read_text())
assert len(seen) == 3 # capped at DEDUP_MAX
# ---------------------------------------------------------------------------
# Configurable parameters
# ---------------------------------------------------------------------------
def test_custom_min_chars_env():
mod = _load_module({"MEMPALACE_MIN_CHARS": "200"})
assert mod.MIN_CONTENT_LENGTH == 200
def test_custom_window_size_env():
mod = _load_module({"MEMPALACE_WINDOW_SIZE": "10"})
assert mod.WINDOW_SIZE == 10
# ---------------------------------------------------------------------------
# Language detection
# ---------------------------------------------------------------------------
def test_detect_language_returns_string():
lang = _mod._detect_language("I have a meeting with the client tomorrow afternoon.")
assert isinstance(lang, str)
assert len(lang) > 0
def test_is_english_known_english():
assert _mod._is_english("en") is True
def test_is_english_unknown_treated_as_english():
# unknown = langdetect unavailable or failed → don't block on keyword check
assert _mod._is_english("unknown") is True
def test_is_english_other_language():
assert _mod._is_english("zh") is False
assert _mod._is_english("ja") is False
assert _mod._is_english("fr") is False
def test_lang_detect_disabled_returns_unknown():
mod = _load_module({"MEMPALACE_LANG_DETECT": "0"})
lang = mod._detect_language("Ich habe morgen einen Termin beim Zahnarzt.")
assert lang == "unknown"
def test_non_english_skips_relevance_check():
"""A non-English exchange long enough to pass length check should not be
blocked by the English-only RELEVANCE_RE."""
# This text is in Chinese and contains no English keywords from RELEVANCE_RE,
# so without language detection it would be skipped as "not relevant".
messages = [
{"role": "user",
"content": "我明天下午三点有个牙医预约,在城市医院,请帮我记住这件事。"},
{"role": "assistant",
"content": "好的,我已经记住了您明天下午三点在城市医院的牙医预约。"},
]
out = run_hook({"type": "after_turn", "agent_id": "a1", "messages": messages})
# Should NOT be skipped for "not relevant" — may fail at mempalace import (error) or dedup
assert out.get("reason") != "not relevant"