- 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
244 lines
8.2 KiB
Python
244 lines
8.2 KiB
Python
"""
|
|
Tests for ForgettingCurve (M.2)
|
|
|
|
Tests: decay calculation, half-life by importance, archive/deprioritize thresholds.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import UTC, datetime, timedelta
|
|
from unittest.mock import MagicMock
|
|
|
|
from app.services.memory.forgetting_curve import ForgettingCurve
|
|
|
|
|
|
def create_mock_memory(
|
|
last_accessed_at=None,
|
|
last_recalled_at=None,
|
|
importance_level: str = "medium",
|
|
):
|
|
"""Create a mock UserMemory for testing."""
|
|
memory = MagicMock()
|
|
memory.last_accessed_at = last_accessed_at
|
|
memory.last_recalled_at = last_recalled_at
|
|
memory.importance_level = importance_level
|
|
memory.decay_score = 1.0
|
|
memory.is_archived = False
|
|
return memory
|
|
|
|
|
|
class TestForgettingCurveCalculateDecay:
|
|
"""Test decay score calculation"""
|
|
|
|
def test_fresh_memory_full_retention(self):
|
|
"""Never accessed memory returns full retention (1.0)."""
|
|
curve = ForgettingCurve()
|
|
memory = create_mock_memory(last_accessed_at=None, last_recalled_at=None)
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
|
|
assert decay == 1.0
|
|
|
|
def test_just_accessed_high_retention(self):
|
|
"""Recently accessed memory has high retention."""
|
|
curve = ForgettingCurve()
|
|
recent = datetime.now(UTC) - timedelta(hours=1)
|
|
memory = create_mock_memory(last_accessed_at=recent)
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
|
|
assert decay > 0.95
|
|
|
|
def test_30_days_medium_decay(self):
|
|
"""~30 days old memory should have ~0.5 decay for medium importance."""
|
|
curve = ForgettingCurve()
|
|
old = datetime.now(UTC) - timedelta(days=30)
|
|
memory = create_mock_memory(last_accessed_at=old, importance_level="medium")
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
|
|
# exp(-30/30) = exp(-1) ≈ 0.368, but capped at min 0.0 max 1.0
|
|
assert 0.3 < decay < 0.5
|
|
|
|
def test_90_days_high_importance_slower_decay(self):
|
|
"""High importance memory decays slower - 90 days should still be > 0.3."""
|
|
curve = ForgettingCurve()
|
|
old = datetime.now(UTC) - timedelta(days=90)
|
|
memory = create_mock_memory(last_accessed_at=old, importance_level="high")
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
|
|
# exp(-90/90) = exp(-1) ≈ 0.368 for high importance (half_life = 90)
|
|
assert 0.3 < decay < 0.5
|
|
|
|
def test_90_days_low_importance_faster_decay(self):
|
|
"""Low importance memory decays faster - 90 days should be near 0."""
|
|
curve = ForgettingCurve()
|
|
old = datetime.now(UTC) - timedelta(days=90)
|
|
memory = create_mock_memory(last_accessed_at=old, importance_level="low")
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
|
|
# exp(-90/15) = exp(-6) ≈ 0.0025
|
|
assert decay < 0.1
|
|
|
|
def test_uses_last_recalled_at_if_last_accessed_missing(self):
|
|
"""Falls back to last_recalled_at when last_accessed_at is None."""
|
|
curve = ForgettingCurve()
|
|
recent = datetime.now(UTC) - timedelta(hours=2)
|
|
memory = create_mock_memory(last_accessed_at=None, last_recalled_at=recent)
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
|
|
assert decay > 0.9
|
|
|
|
def test_naive_datetime_converted_to_utc(self):
|
|
"""Naive datetime (no tzinfo) should be converted to UTC."""
|
|
curve = ForgettingCurve()
|
|
recent = datetime.now() - timedelta(hours=1) # naive
|
|
memory = create_mock_memory(last_accessed_at=recent)
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
|
|
assert decay > 0.9
|
|
|
|
def test_decay_capped_at_one(self):
|
|
"""Decay score should never exceed 1.0."""
|
|
curve = ForgettingCurve()
|
|
very_recent = datetime.now(UTC) + timedelta(hours=1) # future
|
|
memory = create_mock_memory(last_accessed_at=very_recent)
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
|
|
assert decay <= 1.0
|
|
|
|
def test_decay_never_negative(self):
|
|
"""Decay score should never go below 0.0."""
|
|
curve = ForgettingCurve()
|
|
very_old = datetime.now(UTC) - timedelta(days=1000)
|
|
memory = create_mock_memory(last_accessed_at=very_old)
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
|
|
assert decay >= 0.0
|
|
|
|
|
|
class TestForgettingCurveHalfLife:
|
|
"""Test half-life calculation by importance level."""
|
|
|
|
def test_high_importance_half_life_90_days(self):
|
|
"""High importance: half_life = 30 * 3 = 90 days."""
|
|
curve = ForgettingCurve()
|
|
memory = create_mock_memory(importance_level="high")
|
|
|
|
half_life = curve.get_half_life(memory)
|
|
|
|
assert half_life == 90.0
|
|
|
|
def test_medium_importance_half_life_30_days(self):
|
|
"""Medium importance: half_life = 30 * 1 = 30 days."""
|
|
curve = ForgettingCurve()
|
|
memory = create_mock_memory(importance_level="medium")
|
|
|
|
half_life = curve.get_half_life(memory)
|
|
|
|
assert half_life == 30.0
|
|
|
|
def test_low_importance_half_life_15_days(self):
|
|
"""Low importance: half_life = 30 * 0.5 = 15 days."""
|
|
curve = ForgettingCurve()
|
|
memory = create_mock_memory(importance_level="low")
|
|
|
|
half_life = curve.get_half_life(memory)
|
|
|
|
assert half_life == 15.0
|
|
|
|
def test_unknown_importance_defaults_to_medium(self):
|
|
"""Unknown importance level defaults to medium multiplier (1.0)."""
|
|
curve = ForgettingCurve()
|
|
memory = create_mock_memory(importance_level="unknown")
|
|
|
|
half_life = curve.get_half_life(memory)
|
|
|
|
assert half_life == 30.0
|
|
|
|
|
|
class TestForgettingCurveShouldArchive:
|
|
"""Test archive threshold (decay < 0.2)."""
|
|
|
|
def test_high_decay_not_archived(self):
|
|
"""Memory with high decay score (> 0.2) should NOT be archived."""
|
|
curve = ForgettingCurve()
|
|
recent = datetime.now(UTC) - timedelta(days=5)
|
|
memory = create_mock_memory(last_accessed_at=recent)
|
|
|
|
should = curve.should_archive(memory)
|
|
|
|
assert should is False
|
|
|
|
def test_low_decay_archived(self):
|
|
"""Memory with decay < 0.2 should be archived."""
|
|
curve = ForgettingCurve()
|
|
# ~100 days for medium importance: exp(-100/30) ≈ 0.035 < 0.2
|
|
old = datetime.now(UTC) - timedelta(days=100)
|
|
memory = create_mock_memory(last_accessed_at=old, importance_level="medium")
|
|
|
|
should = curve.should_archive(memory)
|
|
|
|
assert should is True
|
|
|
|
def test_boundary_decay_not_archived(self):
|
|
"""At exactly 0.2 decay, should NOT be archived (strict < 0.2)."""
|
|
curve = ForgettingCurve()
|
|
# Create memory with known decay = 0.2
|
|
memory = create_mock_memory(importance_level="low")
|
|
memory.last_accessed_at = datetime.now(UTC) - timedelta(days=int(15 * 4.605)) # 69 days
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
should = curve.should_archive(memory)
|
|
|
|
# exp(-69/15) ≈ 0.010 < 0.2
|
|
assert decay < 0.2
|
|
assert should is True
|
|
|
|
|
|
class TestForgettingCurveShouldDeprioritize:
|
|
"""Test deprioritize threshold (decay < 0.5)."""
|
|
|
|
def test_high_decay_not_deprioritized(self):
|
|
"""Memory with high decay score (> 0.5) should NOT be deprioritized."""
|
|
curve = ForgettingCurve()
|
|
recent = datetime.now(UTC) - timedelta(days=10)
|
|
memory = create_mock_memory(last_accessed_at=recent)
|
|
|
|
should = curve.should_deprioritize(memory)
|
|
|
|
assert should is False
|
|
|
|
def test_medium_decay_deprioritized(self):
|
|
"""Memory with decay < 0.5 should be deprioritized."""
|
|
curve = ForgettingCurve()
|
|
# ~42 days for medium: exp(-42/30) ≈ 0.25 < 0.5
|
|
old = datetime.now(UTC) - timedelta(days=42)
|
|
memory = create_mock_memory(last_accessed_at=old, importance_level="medium")
|
|
|
|
should = curve.should_deprioritize(memory)
|
|
|
|
assert should is True
|
|
|
|
def test_boundary_deprioritize_strict(self):
|
|
"""At exactly 0.5 decay, should NOT be deprioritized (strict < 0.5)."""
|
|
curve = ForgettingCurve()
|
|
# For high importance: exp(-x/90) = 0.5 → x = 90 * ln(2) ≈ 62.4 days
|
|
memory = create_mock_memory(importance_level="high")
|
|
memory.last_accessed_at = datetime.now(UTC) - timedelta(days=62)
|
|
|
|
decay = curve.calculate_decay(memory)
|
|
should = curve.should_deprioritize(memory)
|
|
|
|
assert 0.4 < decay < 0.6
|
|
assert should is False
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|