The 21 the automatic pass could not make on its own. `ruff check` and `ruff format --check` are both clean now; typecheck is still red and is next. `in_reasoning` in chat/service.py was a complete state machine that nothing read: initialised False, set True when a reasoning delta arrived, set False when the summary ended — three assignments, zero reads. Ruff reported one at a time, and removing each revealed the next, so what looked like a single stray variable took three passes to bottom out. The branches themselves do real work and are untouched; only the flag is gone. Four `raise HTTPException` inside `except` blocks now chain with `from e`. Until now a failure while handling an error was indistinguishable from the error, which matters most in exactly the situation where the traceback is all you have. In biographer/tools.py the binding was unused but the call is not: MemoryType() is called for the ValueError it raises on an invalid name. The binding is gone and the call and its comment stay, because dropping the line would have removed the validation. The rest are unused bindings in tests where the assertions are on something else (call_args, mostly), plus three unused loop variables and an isinstance tuple. One correction to my own work: removing a dead comprehension in test_error_handling.py left an `if` block with nothing but comments in it, which is a SyntaxError. Ruff caught it immediately. The block now says what the test actually pins — that the stream parses without crashing, which reaching that line demonstrates — rather than computing a list nobody asserts on. `make test` is intermittent here, and it is not this change. test_tatlock_tool_call_logging_calculator failed in two of five full runs across both HEAD and this branch, and passes in the other three; it also fails in isolation at HEAD while passing in isolation here. Order- or timing-dependent. Recorded rather than chased, since tests are not gated in this repo yet. Co-Authored-By: Claude <noreply@anthropic.com>
281 lines
9.9 KiB
Python
281 lines
9.9 KiB
Python
"""
|
|
Tests for the memory service (direct access layer).
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.core.memory_service import (
|
|
MemoryRecord,
|
|
MemoryService,
|
|
MemoryType,
|
|
memory_service,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMemoryType:
|
|
"""Tests for MemoryType enum."""
|
|
|
|
def test_user_profile_type(self):
|
|
"""Test user_profile type exists."""
|
|
assert MemoryType.USER_PROFILE.value == "user_profile"
|
|
|
|
def test_preference_type(self):
|
|
"""Test preference type exists."""
|
|
assert MemoryType.PREFERENCE.value == "preference"
|
|
|
|
def test_learned_fact_type(self):
|
|
"""Test learned_fact type exists."""
|
|
assert MemoryType.LEARNED_FACT.value == "learned_fact"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMemoryRecord:
|
|
"""Tests for MemoryRecord model."""
|
|
|
|
def test_create_minimal_record(self):
|
|
"""Test creating record with minimal fields."""
|
|
record = MemoryRecord(
|
|
id="test_1",
|
|
type=MemoryType.USER_PROFILE,
|
|
key="location",
|
|
value="Amsterdam",
|
|
)
|
|
|
|
assert record.id == "test_1"
|
|
assert record.type == MemoryType.USER_PROFILE
|
|
assert record.key == "location"
|
|
assert record.value == "Amsterdam"
|
|
assert record.importance == 0.5 # Default
|
|
assert record.source == "explicit" # Default
|
|
|
|
def test_create_full_record(self):
|
|
"""Test creating record with all fields."""
|
|
record = MemoryRecord(
|
|
id="test_2",
|
|
type=MemoryType.LEARNED_FACT,
|
|
key="car",
|
|
value="Tesla Model 3",
|
|
keywords=["car", "vehicle", "tesla"],
|
|
importance=0.8,
|
|
source="conversation",
|
|
)
|
|
|
|
assert record.keywords == ["car", "vehicle", "tesla"]
|
|
assert record.importance == 0.8
|
|
assert record.source == "conversation"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMemoryServiceInit:
|
|
"""Tests for MemoryService initialization."""
|
|
|
|
def test_service_has_lazy_clients(self):
|
|
"""Test service initializes with lazy client loading."""
|
|
service = MemoryService()
|
|
|
|
assert service._qdrant is None
|
|
assert service._embedding is None
|
|
assert service._cache is None
|
|
|
|
def test_global_instance_exists(self):
|
|
"""Test global memory_service instance exists."""
|
|
assert memory_service is not None
|
|
assert isinstance(memory_service, MemoryService)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMemoryServiceProfileMethods:
|
|
"""Tests for profile-related methods."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_profile_uses_context(self):
|
|
"""Test get_profile uses request context for user."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "_get_memory", new_callable=AsyncMock) as mock_get:
|
|
mock_get.return_value = "Amsterdam"
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
result = await service.get_profile("location")
|
|
|
|
mock_get.assert_called_once_with("testuser", MemoryType.USER_PROFILE, "location")
|
|
assert result == "Amsterdam"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_profile_explicit_user(self):
|
|
"""Test get_profile with explicit user parameter."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "_get_memory", new_callable=AsyncMock) as mock_get:
|
|
mock_get.return_value = "Berlin"
|
|
|
|
result = await service.get_profile("location", user="otheruser")
|
|
|
|
mock_get.assert_called_once_with("otheruser", MemoryType.USER_PROFILE, "location")
|
|
assert result == "Berlin"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_profile_high_importance(self):
|
|
"""Test set_profile uses high importance (0.9)."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "_set_memory", new_callable=AsyncMock) as mock_set:
|
|
mock_set.return_value = True
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
result = await service.set_profile("timezone", "Europe/Amsterdam")
|
|
|
|
call_kwargs = mock_set.call_args[1]
|
|
assert call_kwargs["importance"] == 0.9
|
|
assert result is True
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMemoryServicePreferenceMethods:
|
|
"""Tests for preference-related methods."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_preference(self):
|
|
"""Test get_preference retrieves correctly."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "_get_memory", new_callable=AsyncMock) as mock_get:
|
|
mock_get.return_value = "celsius"
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
result = await service.get_preference("temperature_unit")
|
|
|
|
mock_get.assert_called_once_with("testuser", MemoryType.PREFERENCE, "temperature_unit")
|
|
assert result == "celsius"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_preference_medium_importance(self):
|
|
"""Test set_preference uses medium importance (0.7)."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "_set_memory", new_callable=AsyncMock) as mock_set:
|
|
mock_set.return_value = True
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
await service.set_preference("theme", "dark")
|
|
|
|
call_kwargs = mock_set.call_args[1]
|
|
assert call_kwargs["importance"] == 0.7
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMemoryServiceFactMethods:
|
|
"""Tests for fact-related methods."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_store_fact_default_importance(self):
|
|
"""Test store_fact uses default importance (0.5)."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "_set_memory", new_callable=AsyncMock) as mock_set:
|
|
mock_set.return_value = True
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
await service.store_fact("car", "Tesla Model 3")
|
|
|
|
call_kwargs = mock_set.call_args[1]
|
|
assert call_kwargs["importance"] == 0.5
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_store_fact_custom_importance(self):
|
|
"""Test store_fact with custom importance."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "_set_memory", new_callable=AsyncMock) as mock_set:
|
|
mock_set.return_value = True
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
await service.store_fact(
|
|
"employer",
|
|
"Acme Corp",
|
|
importance=0.8,
|
|
)
|
|
|
|
call_kwargs = mock_set.call_args[1]
|
|
assert call_kwargs["importance"] == 0.8
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_fact(self):
|
|
"""Test get_fact retrieves correctly."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "_get_memory", new_callable=AsyncMock) as mock_get:
|
|
mock_get.return_value = "Tesla Model 3"
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
result = await service.get_fact("car")
|
|
|
|
mock_get.assert_called_once_with("testuser", MemoryType.LEARNED_FACT, "car")
|
|
assert result == "Tesla Model 3"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMemoryServicePrefetch:
|
|
"""Tests for prefetch_context method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_prefetch_default_keys(self):
|
|
"""Test prefetch with default profile keys."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "get_profile", new_callable=AsyncMock) as mock_profile:
|
|
with patch.object(service, "get_all_preferences", new_callable=AsyncMock) as mock_prefs:
|
|
mock_profile.side_effect = [
|
|
"Amsterdam", # location
|
|
"Europe/Amsterdam", # timezone
|
|
"John", # name
|
|
]
|
|
mock_prefs.return_value = {"temperature_unit": "celsius"}
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
result = await service.prefetch_context()
|
|
|
|
assert result["profile"]["location"] == "Amsterdam"
|
|
assert result["profile"]["timezone"] == "Europe/Amsterdam"
|
|
assert result["profile"]["name"] == "John"
|
|
assert result["preferences"]["temperature_unit"] == "celsius"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_prefetch_specific_keys(self):
|
|
"""Test prefetch with specific profile keys."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "get_profile", new_callable=AsyncMock) as mock_profile:
|
|
with patch.object(service, "get_all_preferences", new_callable=AsyncMock) as mock_prefs:
|
|
mock_profile.return_value = "Amsterdam"
|
|
mock_prefs.return_value = {}
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
await service.prefetch_context(
|
|
profile_keys=["location"],
|
|
include_preferences=False,
|
|
)
|
|
|
|
# Should only fetch location
|
|
mock_profile.assert_called_once()
|
|
mock_prefs.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_prefetch_no_profile(self):
|
|
"""Test prefetch without profile data."""
|
|
service = MemoryService()
|
|
|
|
with patch.object(service, "get_profile", new_callable=AsyncMock) as mock_profile:
|
|
with patch.object(service, "get_all_preferences", new_callable=AsyncMock) as mock_prefs:
|
|
mock_prefs.return_value = {"theme": "dark"}
|
|
|
|
with patch("src.core.memory_service.get_user", return_value="testuser"):
|
|
result = await service.prefetch_context(include_profile=False)
|
|
|
|
mock_profile.assert_not_called()
|
|
assert "profile" not in result
|
|
assert result["preferences"]["theme"] == "dark"
|