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:
220
backend/tests/services/test_memory_decay.py
Normal file
220
backend/tests/services/test_memory_decay.py
Normal file
@@ -0,0 +1,220 @@
|
||||
"""
|
||||
Tests for MemoryDecay (M.2)
|
||||
|
||||
Tests: evaluate(), archive_memory(), deprioritize_memory(), restore_from_archive().
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from app.services.memory.memory_decay import MemoryDecay
|
||||
|
||||
|
||||
def create_mock_memory(
|
||||
last_accessed_at=None,
|
||||
importance_level: str = "medium",
|
||||
decay_score: float = 1.0,
|
||||
is_archived: bool = False,
|
||||
archive_at=None,
|
||||
):
|
||||
"""Create a mock UserMemory for testing."""
|
||||
memory = MagicMock()
|
||||
memory.last_accessed_at = last_accessed_at
|
||||
memory.importance_level = importance_level
|
||||
memory.decay_score = decay_score
|
||||
memory.is_archived = is_archived
|
||||
memory.archive_at = archive_at
|
||||
return memory
|
||||
|
||||
|
||||
class TestMemoryDecayEvaluate:
|
||||
"""Test evaluate() method."""
|
||||
|
||||
def test_evaluate_fresh_memory_keeps_active(self):
|
||||
"""Fresh memory should be kept active."""
|
||||
decay = MemoryDecay()
|
||||
recent = datetime.now(UTC) - timedelta(hours=1)
|
||||
memory = create_mock_memory(last_accessed_at=recent)
|
||||
|
||||
result = decay.evaluate(memory)
|
||||
|
||||
assert result["action"] == "keep_active"
|
||||
assert result["should_archive"] is False
|
||||
assert result["should_deprioritize"] is False
|
||||
assert result["decay_score"] > 0.5
|
||||
|
||||
def test_evaluate_old_low_importance_archives(self):
|
||||
"""Old low-importance memory should be archived."""
|
||||
decay = MemoryDecay()
|
||||
old = datetime.now(UTC) - timedelta(days=100)
|
||||
memory = create_mock_memory(last_accessed_at=old, importance_level="low")
|
||||
|
||||
result = decay.evaluate(memory)
|
||||
|
||||
assert result["action"] == "archive"
|
||||
assert result["should_archive"] is True
|
||||
assert result["should_deprioritize"] is True
|
||||
assert result["decay_score"] < 0.2
|
||||
|
||||
def test_evaluate_old_high_importance_deprioritizes(self):
|
||||
"""Old high-importance memory may be deprioritized but not archived."""
|
||||
decay = MemoryDecay()
|
||||
# ~45 days for high: exp(-45/90) ≈ 0.6, still > 0.5
|
||||
old = datetime.now(UTC) - timedelta(days=45)
|
||||
memory = create_mock_memory(last_accessed_at=old, importance_level="high")
|
||||
|
||||
result = decay.evaluate(memory)
|
||||
|
||||
assert result["should_archive"] is False
|
||||
assert result["should_deprioritize"] is False
|
||||
assert 0.5 < result["decay_score"] < 0.7
|
||||
|
||||
def test_evaluate_boundary_deprioritize(self):
|
||||
"""Memory at ~42 days medium importance should be deprioritized but not archived."""
|
||||
decay = MemoryDecay()
|
||||
# ~42 days for medium: exp(-42/30) ≈ 0.25 < 0.5, > 0.2
|
||||
old = datetime.now(UTC) - timedelta(days=42)
|
||||
memory = create_mock_memory(last_accessed_at=old, importance_level="medium")
|
||||
|
||||
result = decay.evaluate(memory)
|
||||
|
||||
assert result["action"] == "deprioritize"
|
||||
assert result["should_deprioritize"] is True
|
||||
assert result["should_archive"] is False
|
||||
|
||||
def test_evaluate_returns_all_keys(self):
|
||||
"""evaluate() returns decay_score, should_archive, should_deprioritize, action."""
|
||||
decay = MemoryDecay()
|
||||
memory = create_mock_memory(last_accessed_at=datetime.now(UTC))
|
||||
|
||||
result = decay.evaluate(memory)
|
||||
|
||||
assert "decay_score" in result
|
||||
assert "should_archive" in result
|
||||
assert "should_deprioritize" in result
|
||||
assert "action" in result
|
||||
assert result["action"] in ("keep_active", "deprioritize", "archive")
|
||||
|
||||
|
||||
class TestMemoryDecayArchiveMemory:
|
||||
"""Test archive_memory() method."""
|
||||
|
||||
def test_archive_sets_is_archived_true(self):
|
||||
"""archive_memory() sets is_archived = True."""
|
||||
decay = MemoryDecay()
|
||||
memory = create_mock_memory(is_archived=False)
|
||||
|
||||
result = decay.archive_memory(memory)
|
||||
|
||||
assert result.is_archived is True
|
||||
|
||||
def test_archive_sets_low_decay_score(self):
|
||||
"""archive_memory() resets decay_score to 0.1."""
|
||||
decay = MemoryDecay()
|
||||
memory = create_mock_memory(decay_score=0.8)
|
||||
|
||||
result = decay.archive_memory(memory)
|
||||
|
||||
assert result.decay_score == 0.1
|
||||
|
||||
def test_archive_sets_archive_at_timestamp(self):
|
||||
"""archive_memory() sets archive_at to current time."""
|
||||
decay = MemoryDecay()
|
||||
memory = create_mock_memory(archive_at=None)
|
||||
|
||||
before = datetime.now(UTC)
|
||||
result = decay.archive_memory(memory)
|
||||
after = datetime.now(UTC)
|
||||
|
||||
assert result.archive_at is not None
|
||||
assert before <= result.archive_at <= after
|
||||
|
||||
def test_archive_preserves_other_fields(self):
|
||||
"""archive_memory() does not modify other fields."""
|
||||
decay = MemoryDecay()
|
||||
memory = create_mock_memory(
|
||||
last_accessed_at=datetime.now(UTC),
|
||||
importance_level="high",
|
||||
decay_score=0.5,
|
||||
)
|
||||
|
||||
result = decay.archive_memory(memory)
|
||||
|
||||
assert result.last_accessed_at == memory.last_accessed_at
|
||||
assert result.importance_level == "high"
|
||||
|
||||
|
||||
class TestMemoryDecayDeprioritizeMemory:
|
||||
"""Test deprioritize_memory() method."""
|
||||
|
||||
def test_deprioritize_updates_decay_score(self):
|
||||
"""deprioritize_memory() recalculates decay_score."""
|
||||
decay = MemoryDecay()
|
||||
# Old memory will have low decay score
|
||||
old = datetime.now(UTC) - timedelta(days=60)
|
||||
memory = create_mock_memory(
|
||||
last_accessed_at=old, importance_level="medium", decay_score=0.9
|
||||
)
|
||||
|
||||
result = decay.deprioritize_memory(memory)
|
||||
|
||||
assert result.decay_score < 0.5 # Should be recalculated low
|
||||
|
||||
def test_deprioritize_does_not_archive(self):
|
||||
"""deprioritize_memory() does NOT set is_archived."""
|
||||
decay = MemoryDecay()
|
||||
memory = create_mock_memory(is_archived=False)
|
||||
|
||||
result = decay.deprioritize_memory(memory)
|
||||
|
||||
assert result.is_archived is False
|
||||
|
||||
|
||||
class TestMemoryDecayRestoreFromArchive:
|
||||
"""Test restore_from_archive() method."""
|
||||
|
||||
def test_restore_clears_is_archived(self):
|
||||
"""restore_from_archive() sets is_archived = False."""
|
||||
decay = MemoryDecay()
|
||||
memory = create_mock_memory(is_archived=True)
|
||||
|
||||
result = decay.restore_from_archive(memory)
|
||||
|
||||
assert result.is_archived is False
|
||||
|
||||
def test_restore_sets_decay_score_high(self):
|
||||
"""restore_from_archive() sets decay_score to 0.8."""
|
||||
decay = MemoryDecay()
|
||||
memory = create_mock_memory(decay_score=0.1)
|
||||
|
||||
result = decay.restore_from_archive(memory)
|
||||
|
||||
assert result.decay_score == 0.8
|
||||
|
||||
def test_restore_updates_last_accessed(self):
|
||||
"""restore_from_archive() updates last_accessed_at to now."""
|
||||
decay = MemoryDecay()
|
||||
old_time = datetime.now(UTC) - timedelta(days=30)
|
||||
memory = create_mock_memory(
|
||||
last_accessed_at=old_time, is_archived=True, archive_at=old_time
|
||||
)
|
||||
|
||||
before = datetime.now(UTC)
|
||||
result = decay.restore_from_archive(memory)
|
||||
after = datetime.now(UTC)
|
||||
|
||||
assert before <= result.last_accessed_at <= after
|
||||
|
||||
def test_restore_clears_archive_at(self):
|
||||
"""restore_from_archive() sets archive_at to None."""
|
||||
decay = MemoryDecay()
|
||||
memory = create_mock_memory(is_archived=True, archive_at=datetime.now(UTC))
|
||||
|
||||
result = decay.restore_from_archive(memory)
|
||||
|
||||
assert result.archive_at is None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user