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