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).
109 lines
3.0 KiB
Python
109 lines
3.0 KiB
Python
"""Tests for scratchpad.py.
|
|
|
|
Refs: Epic #367, Sub-issue #372
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
|
|
from mempalace.scratchpad import (
|
|
write_scratch,
|
|
read_scratch,
|
|
delete_scratch,
|
|
list_sessions,
|
|
clear_session,
|
|
_scratch_path,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def scratch_dir(tmp_path):
|
|
"""Provide a temporary scratchpad directory."""
|
|
with patch("mempalace.scratchpad.SCRATCHPAD_DIR", tmp_path):
|
|
yield tmp_path
|
|
|
|
|
|
class TestScratchPath:
|
|
def test_sanitizes_session_id(self):
|
|
path = _scratch_path("safe-id_123")
|
|
assert "safe-id_123.json" in str(path)
|
|
|
|
def test_strips_dangerous_chars(self):
|
|
path = _scratch_path("../../etc/passwd")
|
|
assert ".." not in path.name
|
|
assert "/" not in path.name
|
|
# Dots are stripped, so only alphanumeric chars remain
|
|
assert path.name == "etcpasswd.json"
|
|
|
|
|
|
class TestWriteAndRead:
|
|
def test_write_then_read(self, scratch_dir):
|
|
write_scratch("sess1", "note", "hello world")
|
|
result = read_scratch("sess1", "note")
|
|
assert "note" in result
|
|
assert result["note"]["value"] == "hello world"
|
|
|
|
def test_read_all_keys(self, scratch_dir):
|
|
write_scratch("sess1", "a", 1)
|
|
write_scratch("sess1", "b", 2)
|
|
result = read_scratch("sess1")
|
|
assert "a" in result
|
|
assert "b" in result
|
|
|
|
def test_read_missing_key(self, scratch_dir):
|
|
write_scratch("sess1", "exists", "yes")
|
|
result = read_scratch("sess1", "missing")
|
|
assert result == {}
|
|
|
|
def test_read_missing_session(self, scratch_dir):
|
|
result = read_scratch("nonexistent")
|
|
assert result == {}
|
|
|
|
def test_overwrite_key(self, scratch_dir):
|
|
write_scratch("sess1", "key", "v1")
|
|
write_scratch("sess1", "key", "v2")
|
|
result = read_scratch("sess1", "key")
|
|
assert result["key"]["value"] == "v2"
|
|
|
|
|
|
class TestDelete:
|
|
def test_delete_existing_key(self, scratch_dir):
|
|
write_scratch("sess1", "key", "val")
|
|
assert delete_scratch("sess1", "key") is True
|
|
assert read_scratch("sess1", "key") == {}
|
|
|
|
def test_delete_missing_key(self, scratch_dir):
|
|
write_scratch("sess1", "other", "val")
|
|
assert delete_scratch("sess1", "missing") is False
|
|
|
|
|
|
class TestListSessions:
|
|
def test_lists_sessions(self, scratch_dir):
|
|
write_scratch("alpha", "k", "v")
|
|
write_scratch("beta", "k", "v")
|
|
sessions = list_sessions()
|
|
assert "alpha" in sessions
|
|
assert "beta" in sessions
|
|
|
|
def test_empty_directory(self, scratch_dir):
|
|
assert list_sessions() == []
|
|
|
|
|
|
class TestClearSession:
|
|
def test_clears_existing(self, scratch_dir):
|
|
write_scratch("sess1", "k", "v")
|
|
assert clear_session("sess1") is True
|
|
assert read_scratch("sess1") == {}
|
|
|
|
def test_clear_nonexistent(self, scratch_dir):
|
|
assert clear_session("ghost") is False
|