mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
723cad24bb
* feat: structured memory system — typed/scoped memories with BM25 relevance and metacognitive prompting Replace flat key-value memories table with structured_memories (migration 014). Four memory types (user/project/feedback/reference), three scopes (global/workstream/user). Consolidate remember/recall/forget into two tools: memory (action-based: save/search/delete/list) and recall (conversation history only). BM25 relevance scoring (extracted to turnstone/core/bm25.py) selects top-5 memories for system message injection based on conversation context. Metacognitive prompting injects ephemeral nudges after corrections, tool denials, workstream resume, and completion signals. Scope isolation enforced: system message injection and nudge counts filtered to visible memories only (global + current workstream + authenticated user). User scope requires authentication. Content capped at 32KB. ILIKE/LIKE metacharacters escaped in both backends. 113 new tests (2053 total). * fix: CI failure + copilot review feedback - Fix time.monotonic() cooldown: use None sentinel instead of 0.0 default (monotonic clock starts at boot, not epoch — fresh CI runners have uptime < 300s so cooldown check always triggered) - Catch sa.exc.IntegrityError specifically in upsert instead of broad Exception (copilot review) - Preserve existing description/type on upsert when caller doesn't explicitly set them (copilot review) - Add last_accessed + access_count columns to schema/migration for future LRU/LFU eviction support
83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
"""Tests for turnstone.core.memory — structured memory facade functions."""
|
|
|
|
from turnstone.core.memory import (
|
|
count_structured_memories,
|
|
delete_structured_memory,
|
|
list_structured_memories,
|
|
normalize_key,
|
|
save_structured_memory,
|
|
search_structured_memories,
|
|
)
|
|
|
|
|
|
class TestSaveStructuredMemory:
|
|
def test_save_new(self, tmp_db):
|
|
mid, old = save_structured_memory("test_key", "hello world")
|
|
assert mid != ""
|
|
assert old is None
|
|
|
|
def test_save_upsert(self, tmp_db):
|
|
save_structured_memory("test_key", "first")
|
|
mid, old = save_structured_memory("test_key", "second")
|
|
assert old == "first"
|
|
assert mid != ""
|
|
|
|
def test_save_normalizes_key(self, tmp_db):
|
|
save_structured_memory("My-Key", "value")
|
|
mems = list_structured_memories()
|
|
assert any(m["name"] == "my_key" for m in mems)
|
|
|
|
def test_save_with_type_and_scope(self, tmp_db):
|
|
save_structured_memory("k", "v", mem_type="user", scope="workstream", scope_id="ws1")
|
|
mems = list_structured_memories(scope="workstream", scope_id="ws1")
|
|
assert len(mems) == 1
|
|
assert mems[0]["type"] == "user"
|
|
|
|
|
|
class TestDeleteStructuredMemory:
|
|
def test_delete_existing(self, tmp_db):
|
|
save_structured_memory("mykey", "val")
|
|
assert delete_structured_memory("mykey")
|
|
|
|
def test_delete_nonexistent(self, tmp_db):
|
|
assert not delete_structured_memory("nope")
|
|
|
|
def test_delete_normalizes_key(self, tmp_db):
|
|
save_structured_memory("my_key", "val")
|
|
assert delete_structured_memory("My-Key")
|
|
|
|
|
|
class TestListStructuredMemories:
|
|
def test_list_empty(self, tmp_db):
|
|
assert list_structured_memories() == []
|
|
|
|
def test_list_returns_saved(self, tmp_db):
|
|
save_structured_memory("a", "alpha")
|
|
save_structured_memory("b", "beta")
|
|
mems = list_structured_memories()
|
|
assert len(mems) == 2
|
|
|
|
|
|
class TestSearchStructuredMemories:
|
|
def test_search_finds_match(self, tmp_db):
|
|
save_structured_memory("db_host", "localhost", description="database hostname")
|
|
save_structured_memory("api_url", "http://example.com")
|
|
results = search_structured_memories("database")
|
|
assert len(results) >= 1
|
|
assert any(r["name"] == "db_host" for r in results)
|
|
|
|
|
|
class TestCountStructuredMemories:
|
|
def test_count_zero(self, tmp_db):
|
|
assert count_structured_memories() == 0
|
|
|
|
def test_count_after_save(self, tmp_db):
|
|
save_structured_memory("a", "1")
|
|
save_structured_memory("b", "2")
|
|
assert count_structured_memories() == 2
|
|
|
|
|
|
class TestNormalizeKey:
|
|
def test_basic(self):
|
|
assert normalize_key("My-Key Name") == "my_key_name"
|