Files
JARVIS/backend/tests/services/test_memory_decay.py
WIN-JHFT4D3SIVT\caoxiaozhu 11160ec4d2 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
2026-04-05 14:09:51 +08:00

221 lines
7.3 KiB
Python

"""
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"])