feat(memory): complete M.2-M.5 memory upgrade phases with tests
- M.2: ForgettingCurve, MemoryDecay, MemoryReinforcement (selective forgetting) - M.3: DailyDigestGenerator, ReminderScheduler, ProactiveInformer (proactive reminders) - M.4: MemoryExtractor with LLM-based memory extraction from conversations - M.5: MemoryRecallInjector with token budget control for prompt injection - All phases include comprehensive unit tests (109 tests passing) - Updated checklist.md to mark all tasks complete
This commit is contained in:
213
backend/tests/services/test_reinforcement.py
Normal file
213
backend/tests/services/test_reinforcement.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
Tests for MemoryReinforcement (M.2)
|
||||
|
||||
Tests: trigger(), auto_reinforce().
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from app.services.memory.reinforcement import MemoryReinforcement
|
||||
|
||||
|
||||
def create_mock_memory(
|
||||
frequency_count: int = 0,
|
||||
last_accessed_at=None,
|
||||
last_recalled_at=None,
|
||||
decay_score: float = 1.0,
|
||||
importance_level: str = "medium",
|
||||
):
|
||||
"""Create a mock UserMemory for testing."""
|
||||
memory = MagicMock()
|
||||
memory.frequency_count = frequency_count
|
||||
memory.last_accessed_at = last_accessed_at
|
||||
memory.last_recalled_at = last_recalled_at
|
||||
memory.decay_score = decay_score
|
||||
memory.importance_level = importance_level
|
||||
return memory
|
||||
|
||||
|
||||
class TestMemoryReinforcementTrigger:
|
||||
"""Test trigger() method - called on memory recall."""
|
||||
|
||||
def test_trigger_increments_frequency(self):
|
||||
"""trigger() increments frequency_count by 1."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory(frequency_count=5)
|
||||
|
||||
result = reinforcement.trigger(memory)
|
||||
|
||||
assert result.frequency_count == 6
|
||||
|
||||
def test_trigger_frequency_capped_at_max(self):
|
||||
"""trigger() caps frequency_count at MAX_FREQUENCY (10)."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory(frequency_count=10)
|
||||
|
||||
result = reinforcement.trigger(memory)
|
||||
|
||||
assert result.frequency_count == 10
|
||||
|
||||
def test_trigger_updates_last_accessed_at(self):
|
||||
"""trigger() updates last_accessed_at to now."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
old_time = datetime.now(UTC) - timedelta(days=10)
|
||||
memory = create_mock_memory(last_accessed_at=old_time)
|
||||
|
||||
before = datetime.now(UTC)
|
||||
result = reinforcement.trigger(memory)
|
||||
after = datetime.now(UTC)
|
||||
|
||||
assert before <= result.last_accessed_at <= after
|
||||
|
||||
def test_trigger_updates_last_recalled_at(self):
|
||||
"""trigger() updates last_recalled_at to now."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory()
|
||||
|
||||
before = datetime.now(UTC)
|
||||
result = reinforcement.trigger(memory)
|
||||
after = datetime.now(UTC)
|
||||
|
||||
assert before <= result.last_recalled_at <= after
|
||||
|
||||
def test_trigger_boosts_decay_score(self):
|
||||
"""trigger() boosts decay_score by 0.1 (capped at 0.95)."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory(decay_score=0.5)
|
||||
|
||||
result = reinforcement.trigger(memory)
|
||||
|
||||
assert result.decay_score > 0.5
|
||||
assert result.decay_score <= 0.95
|
||||
|
||||
def test_trigger_decay_score_capped_at_095(self):
|
||||
"""trigger() decay_score boost is capped at 0.95."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory(decay_score=0.95)
|
||||
|
||||
result = reinforcement.trigger(memory)
|
||||
|
||||
assert result.decay_score == 0.95
|
||||
|
||||
def test_trigger_from_zero_frequency(self):
|
||||
"""trigger() works from frequency_count = 0."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory(frequency_count=0)
|
||||
|
||||
result = reinforcement.trigger(memory)
|
||||
|
||||
assert result.frequency_count == 1
|
||||
|
||||
def test_trigger_returns_same_memory_object(self):
|
||||
"""trigger() returns the same memory object (modified in place)."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory()
|
||||
|
||||
result = reinforcement.trigger(memory)
|
||||
|
||||
assert result is memory
|
||||
|
||||
|
||||
class TestMemoryReinforcementAutoReinforce:
|
||||
"""Test auto_reinforce() method - weekly maintenance for high-importance memories."""
|
||||
|
||||
def test_auto_reinforce_skips_non_high_importance(self):
|
||||
"""auto_reinforce() skips memories that are not high importance."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory_low = create_mock_memory(importance_level="low", frequency_count=5)
|
||||
memory_medium = create_mock_memory(importance_level="medium", frequency_count=5)
|
||||
|
||||
result = reinforcement.auto_reinforce([memory_low, memory_medium])
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
def test_auto_reinforce_includes_high_importance(self):
|
||||
"""auto_reinforce() includes high-importance memories."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory_high = create_mock_memory(importance_level="high", frequency_count=5)
|
||||
|
||||
result = reinforcement.auto_reinforce([memory_high])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] is memory_high
|
||||
|
||||
def test_auto_reinforce_skips_max_frequency(self):
|
||||
"""auto_reinforce() skips high-importance memories already at MAX_FREQUENCY."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory(importance_level="high", frequency_count=10)
|
||||
|
||||
result = reinforcement.auto_reinforce([memory])
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
def test_auto_reinforce_boosts_frequency(self):
|
||||
"""auto_reinforce() applies 10% boost to frequency_count."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory(importance_level="high", frequency_count=5)
|
||||
|
||||
result = reinforcement.auto_reinforce([memory])
|
||||
|
||||
# 5 * 1.1 + 1 = 6.5 → int = 6
|
||||
assert result[0].frequency_count == 6
|
||||
|
||||
def test_auto_reinforce_frequency_capped(self):
|
||||
"""auto_reinforce() caps frequency at MAX_FREQUENCY."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory(importance_level="high", frequency_count=9)
|
||||
|
||||
result = reinforcement.auto_reinforce([memory])
|
||||
|
||||
assert result[0].frequency_count == 10
|
||||
|
||||
def test_auto_reinforce_improves_decay_score(self):
|
||||
"""auto_reinforce() improves decay_score by 5%."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory = create_mock_memory(importance_level="high", frequency_count=5, decay_score=0.5)
|
||||
|
||||
result = reinforcement.auto_reinforce([memory])
|
||||
|
||||
assert result[0].decay_score > 0.5
|
||||
assert result[0].decay_score == pytest.approx(0.525, abs=0.001)
|
||||
|
||||
def test_auto_reinforce_updates_last_accessed(self):
|
||||
"""auto_reinforce() updates last_accessed_at to now."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
old_time = datetime.now(UTC) - timedelta(days=30)
|
||||
memory = create_mock_memory(
|
||||
importance_level="high", frequency_count=5, last_accessed_at=old_time
|
||||
)
|
||||
|
||||
before = datetime.now(UTC)
|
||||
result = reinforcement.auto_reinforce([memory])
|
||||
after = datetime.now(UTC)
|
||||
|
||||
assert before <= result[0].last_accessed_at <= after
|
||||
|
||||
def test_auto_reinforce_empty_list(self):
|
||||
"""auto_reinforce() handles empty list gracefully."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
|
||||
result = reinforcement.auto_reinforce([])
|
||||
|
||||
assert result == []
|
||||
|
||||
def test_auto_reinforce_mixed_memories(self):
|
||||
"""auto_reinforce() processes only high-importance, leaves others untouched."""
|
||||
reinforcement = MemoryReinforcement()
|
||||
memory_high = create_mock_memory(importance_level="high", frequency_count=5)
|
||||
memory_low = create_mock_memory(importance_level="low", frequency_count=5)
|
||||
memory_medium = create_mock_memory(importance_level="medium", frequency_count=5)
|
||||
|
||||
result = reinforcement.auto_reinforce([memory_high, memory_low, memory_medium])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] is memory_high
|
||||
# Others should not be modified
|
||||
assert memory_low.frequency_count == 5
|
||||
assert memory_medium.frequency_count == 5
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user