e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
141 lines
5.4 KiB
Python
141 lines
5.4 KiB
Python
"""Partner-only memory + history tools.
|
|
|
|
Covers the split-memory model: ``partner_memorize`` writes ONLY the partner's
|
|
own memory (never the owner's), ``partner_read`` folds the owner's shared L3 on
|
|
top of the partner's own, and ``partner_search`` greps the partner's sessions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from deeptutor.multi_user.paths import get_admin_path_service, user_context
|
|
from deeptutor.partners.config.paths import get_partner_sessions_dir, get_partner_workspace
|
|
from deeptutor.services.partners.scope import partner_user
|
|
from deeptutor.services.partners.sessions import PartnerSessionStore
|
|
from deeptutor.tools.partner_memory import (
|
|
PartnerMemorizeTool,
|
|
PartnerReadTool,
|
|
PartnerSearchTool,
|
|
)
|
|
|
|
PID = "alice"
|
|
|
|
|
|
def _run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _fresh_memory_singleton(monkeypatch):
|
|
"""Each test gets a fresh MemoryStore so its write locks don't leak."""
|
|
from deeptutor.services.memory import store
|
|
|
|
monkeypatch.setattr(store, "_singleton", None)
|
|
|
|
|
|
def _write_owner_preference(text: str) -> None:
|
|
"""Seed the owner's (admin) L3 preferences directly on disk."""
|
|
admin_mem = get_admin_path_service().get_memory_dir()
|
|
pref = admin_mem / "L3" / "preferences.md"
|
|
pref.parent.mkdir(parents=True, exist_ok=True)
|
|
pref.write_text(f"# Preferences\n\n## Preferences\n- {text}\n", encoding="utf-8")
|
|
|
|
|
|
def test_memorize_writes_own_not_owner(partners_root: Path) -> None:
|
|
with user_context(partner_user(PID, name="Alice")):
|
|
result = _run(PartnerMemorizeTool().execute(op="add", text="prefers terse answers"))
|
|
assert result.success, result.content
|
|
|
|
own_pref = get_partner_workspace(PID) / "memory" / "L3" / "preferences.md"
|
|
assert own_pref.exists()
|
|
assert "prefers terse answers" in own_pref.read_text(encoding="utf-8")
|
|
|
|
# The owner's memory must be untouched.
|
|
admin_pref = get_admin_path_service().get_memory_dir() / "L3" / "preferences.md"
|
|
assert not admin_pref.exists()
|
|
|
|
|
|
def test_read_concats_owner_and_own(partners_root: Path) -> None:
|
|
_write_owner_preference("owner wants formal tone")
|
|
with user_context(partner_user(PID, name="Alice")):
|
|
_run(PartnerMemorizeTool().execute(op="add", text="alice likes calculus examples"))
|
|
result = _run(PartnerReadTool().execute())
|
|
|
|
assert "Shared memory" in result.content
|
|
assert "Your own memory" in result.content
|
|
assert "owner wants formal tone" in result.content
|
|
assert "alice likes calculus examples" in result.content
|
|
assert result.metadata["has_shared"] is True
|
|
assert result.metadata["has_own"] is True
|
|
|
|
|
|
def test_read_labels_empty_layers(partners_root: Path) -> None:
|
|
with user_context(partner_user(PID, name="Alice")):
|
|
result = _run(PartnerReadTool().execute())
|
|
assert "(none yet" in result.content
|
|
assert result.metadata["has_shared"] is False
|
|
assert result.metadata["has_own"] is False
|
|
|
|
|
|
def test_read_own_does_not_leak_into_owner(partners_root: Path) -> None:
|
|
"""A partner's own note must not appear under the shared (owner) section."""
|
|
with user_context(partner_user(PID, name="Alice")):
|
|
_run(PartnerMemorizeTool().execute(op="add", text="secret partner note"))
|
|
result = _run(PartnerReadTool().execute())
|
|
shared_part, _, own_part = result.content.partition("## Your own memory")
|
|
assert "secret partner note" not in shared_part
|
|
assert "secret partner note" in own_part
|
|
|
|
|
|
def test_search_matches_history(partners_root: Path) -> None:
|
|
store = PartnerSessionStore(get_partner_sessions_dir(PID))
|
|
store.append("s1", "user", "Can you explain calculus limits to me?")
|
|
store.append("s1", "assistant", "Sure — a limit describes the value a function approaches.")
|
|
store.append("s2", "user", "Let's talk about linear algebra instead.")
|
|
|
|
with user_context(partner_user(PID, name="Alice")):
|
|
result = _run(PartnerSearchTool().execute(query="calculus"))
|
|
|
|
assert result.success
|
|
assert result.metadata["count"] == 1
|
|
assert "calculus limits" in result.content
|
|
|
|
|
|
def test_search_skips_tool_messages(partners_root: Path) -> None:
|
|
store = PartnerSessionStore(get_partner_sessions_dir(PID))
|
|
store.append("s1", "tool", "calculus tool payload noise")
|
|
store.append("s1", "user", "real question about calculus")
|
|
|
|
with user_context(partner_user(PID, name="Alice")):
|
|
result = _run(PartnerSearchTool().execute(query="calculus"))
|
|
assert result.metadata["count"] == 1
|
|
assert "real question" in result.content
|
|
|
|
|
|
def test_search_no_match(partners_root: Path) -> None:
|
|
store = PartnerSessionStore(get_partner_sessions_dir(PID))
|
|
store.append("s1", "user", "hello there")
|
|
|
|
with user_context(partner_user(PID, name="Alice")):
|
|
result = _run(PartnerSearchTool().execute(query="nonexistent-term"))
|
|
assert result.metadata["count"] == 0
|
|
assert "No past messages matched" in result.content
|
|
|
|
|
|
def test_search_requires_partner_scope(partners_root: Path) -> None:
|
|
# No partner user_context → the admin/local scope is active.
|
|
result = _run(PartnerSearchTool().execute(query="anything"))
|
|
assert not result.success
|
|
assert "only available inside a partner" in result.content
|
|
|
|
|
|
def test_memorize_rejects_bad_op(partners_root: Path) -> None:
|
|
with user_context(partner_user(PID, name="Alice")):
|
|
result = _run(PartnerMemorizeTool().execute(op="delete", text="x"))
|
|
assert not result.success
|
|
assert "op must be" in result.content
|