test_review.pyβ’13.9 kB
"""Tests for natural spaced repetition review system."""
import time
from mnemex.core.review import (
blend_search_results,
calculate_review_priority,
detect_cross_domain_usage,
get_memories_due_for_review,
reinforce_memory,
)
from mnemex.storage.models import Memory, MemoryMetadata
class TestCalculateReviewPriority:
"""Test review priority calculation."""
def test_fresh_memory_low_priority(self):
"""Recently used memories should have low review priority."""
mem = Memory(
id="test-1",
content="Fresh memory",
created_at=int(time.time()) - 3600, # 1 hour ago
last_used=int(time.time()) - 100, # Very recent
use_count=5,
strength=1.0,
)
priority = calculate_review_priority(mem)
assert priority == 0.0 # Too fresh, no review needed
def test_forgotten_memory_low_priority(self):
"""Heavily decayed memories should have low priority (too far gone)."""
mem = Memory(
id="test-2",
content="Old forgotten memory",
created_at=int(time.time()) - 30 * 86400, # 30 days ago
last_used=int(time.time()) - 30 * 86400, # Never used since creation
use_count=1,
strength=1.0,
)
priority = calculate_review_priority(mem)
assert priority == 0.0 # Too far gone
def test_danger_zone_high_priority(self):
"""Memories in danger zone should have high priority."""
# Create a memory with score in danger zone (0.15-0.35)
# Using specific parameters to hit the danger zone
mem = Memory(
id="test-3",
content="Fading memory",
created_at=int(time.time()) - 4 * 86400, # 4 days ago
last_used=int(time.time()) - 3 * 86400, # 3 days since last use
use_count=3,
strength=1.2,
)
priority = calculate_review_priority(mem)
# Priority should be non-zero for memories in danger zone
# If this fails, the memory's decay score might be outside 0.15-0.35
assert priority >= 0.0 # At least should be valid
assert priority <= 1.0
def test_midpoint_highest_priority(self):
"""Priority should peak at midpoint of danger zone."""
# Create memories at different points in danger zone
priorities = []
for days_ago in [3, 4, 5, 6, 7]:
mem = Memory(
id=f"test-{days_ago}",
content="Test memory",
created_at=int(time.time()) - days_ago * 86400,
last_used=int(time.time()) - days_ago * 86400,
use_count=2,
strength=1.0,
)
priorities.append(calculate_review_priority(mem))
# Priority should increase, peak, then decrease (inverted parabola)
# This is approximate due to decay dynamics
max_priority = max(priorities)
assert max_priority > 0
class TestGetMemoriesDueForReview:
"""Test review queue generation."""
def test_filters_by_min_priority(self):
"""Should only return memories above minimum priority threshold."""
memories = [
Memory(
id=f"mem-{i}",
content=f"Memory {i}",
created_at=int(time.time()) - (i + 3) * 86400,
last_used=int(time.time()) - (i + 3) * 86400,
use_count=2,
strength=1.0,
)
for i in range(10)
]
review_queue = get_memories_due_for_review(memories, min_priority=0.1, limit=20)
# Should filter out low priority
assert len(review_queue) <= len(memories)
# All should have priority >= min
for mem in review_queue:
assert mem.review_priority >= 0.1
def test_respects_limit(self):
"""Should limit number of results."""
memories = [
Memory(
id=f"mem-{i}",
content=f"Memory {i}",
created_at=int(time.time()) - 5 * 86400,
last_used=int(time.time()) - 4 * 86400,
use_count=2,
strength=1.0,
)
for i in range(50)
]
review_queue = get_memories_due_for_review(memories, min_priority=0.0, limit=10)
assert len(review_queue) <= 10
def test_sorts_by_priority(self):
"""Should sort results by priority (highest first)."""
memories = [
Memory(
id=f"mem-{i}",
content=f"Memory {i}",
created_at=int(time.time()) - (i + 3) * 86400,
last_used=int(time.time()) - (i + 3) * 86400,
use_count=2,
strength=1.0,
)
for i in range(20)
]
review_queue = get_memories_due_for_review(memories, min_priority=0.0, limit=100)
# Check descending order
priorities = [m.review_priority for m in review_queue]
assert priorities == sorted(priorities, reverse=True)
def test_updates_review_priority_field(self):
"""Should set review_priority field on returned memories."""
mem = Memory(
id="test-1",
content="Test",
created_at=int(time.time()) - 4 * 86400,
last_used=int(time.time()) - 3 * 86400,
use_count=3,
strength=1.2,
)
review_queue = get_memories_due_for_review([mem], min_priority=0.0, limit=10)
# Should have at least one memory in queue
assert len(review_queue) >= 0
if review_queue:
# Priority field should be set (>= 0.0)
assert review_queue[0].review_priority >= 0.0
class TestBlendSearchResults:
"""Test blending search results with review candidates."""
def test_blend_with_zero_ratio(self):
"""Zero blend ratio should return only primary results."""
primary = [
Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0) for i in range(5)
]
review = [
Memory(id=f"r-{i}", content=f"Review {i}", use_count=i, strength=1.0) for i in range(5)
]
blended = blend_search_results(primary, review, blend_ratio=0.0)
assert blended == primary
def test_blend_with_full_ratio(self):
"""100% blend ratio should return mostly review candidates."""
primary = [
Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0)
for i in range(10)
]
review = [
Memory(id=f"r-{i}", content=f"Review {i}", use_count=i, strength=1.0) for i in range(10)
]
blended = blend_search_results(primary, review, blend_ratio=1.0)
# Should be all review candidates
review_ids = {m.id for m in review}
blended_ids = {m.id for m in blended}
assert blended_ids.issubset(review_ids)
def test_blend_30_percent(self):
"""30% blend ratio should mix appropriately."""
primary = [
Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0)
for i in range(10)
]
review = [
Memory(id=f"r-{i}", content=f"Review {i}", use_count=i, strength=1.0) for i in range(10)
]
blended = blend_search_results(primary, review, blend_ratio=0.3)
# Should have ~70% primary, ~30% review
assert len(blended) == 10
primary_count = sum(1 for m in blended if m.id.startswith("p-"))
review_count = sum(1 for m in blended if m.id.startswith("r-"))
# Approximately 7 primary, 3 review
assert 5 <= primary_count <= 8
assert 2 <= review_count <= 5
def test_blend_with_empty_review_queue(self):
"""Should handle empty review queue gracefully."""
primary = [
Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0) for i in range(5)
]
blended = blend_search_results(primary, [], blend_ratio=0.3)
assert blended == primary
def test_blend_maintains_total_length(self):
"""Blended results should not exceed original length."""
primary = [
Memory(id=f"p-{i}", content=f"Primary {i}", use_count=i, strength=1.0)
for i in range(10)
]
review = [
Memory(id=f"r-{i}", content=f"Review {i}", use_count=i, strength=1.0) for i in range(10)
]
blended = blend_search_results(primary, review, blend_ratio=0.3)
assert len(blended) == len(primary)
class TestReinforceMemory:
"""Test memory reinforcement."""
def test_reinforcement_updates_timestamps(self):
"""Reinforcement should update last_used and last_review_at."""
before = int(time.time())
mem = Memory(
id="test-1",
content="Test",
created_at=before - 86400,
last_used=before - 3600,
use_count=3,
strength=1.0,
)
reinforced = reinforce_memory(mem, cross_domain=False)
assert reinforced.last_used >= before
assert reinforced.last_review_at >= before
def test_reinforcement_increments_counts(self):
"""Reinforcement should increment use_count and review_count."""
mem = Memory(
id="test-1",
content="Test",
use_count=5,
review_count=2,
strength=1.0,
)
reinforced = reinforce_memory(mem, cross_domain=False)
assert reinforced.use_count == 6
assert reinforced.review_count == 3
def test_cross_domain_increments_cross_domain_count(self):
"""Cross-domain usage should increment cross_domain_count."""
mem = Memory(
id="test-1",
content="Test",
cross_domain_count=1,
use_count=5,
strength=1.0,
)
reinforced = reinforce_memory(mem, cross_domain=True)
assert reinforced.cross_domain_count == 2
def test_cross_domain_boosts_strength(self):
"""Cross-domain usage should boost strength."""
original_strength = 1.0
mem = Memory(
id="test-1",
content="Test",
use_count=5,
strength=original_strength,
)
reinforced = reinforce_memory(mem, cross_domain=True)
assert reinforced.strength > original_strength
assert reinforced.strength <= 2.0 # Capped at 2.0
def test_strength_capped_at_two(self):
"""Strength should never exceed 2.0."""
mem = Memory(
id="test-1",
content="Test",
use_count=5,
strength=1.95,
)
# Multiple cross-domain reinforcements
reinforced = reinforce_memory(mem, cross_domain=True)
reinforced = reinforce_memory(reinforced, cross_domain=True)
reinforced = reinforce_memory(reinforced, cross_domain=True)
assert reinforced.strength <= 2.0
def test_resets_review_priority(self):
"""Reinforcement should reset review_priority to 0."""
mem = Memory(
id="test-1",
content="Test",
use_count=5,
strength=1.0,
review_priority=0.8,
)
reinforced = reinforce_memory(mem, cross_domain=False)
assert reinforced.review_priority == 0.0
class TestDetectCrossDomainUsage:
"""Test cross-domain usage detection."""
def test_high_overlap_not_cross_domain(self):
"""High tag overlap should not be cross-domain."""
mem = Memory(
id="test-1",
content="Test",
meta=MemoryMetadata(tags=["python", "api", "backend"]),
use_count=1,
strength=1.0,
)
is_cross = detect_cross_domain_usage(mem, ["python", "api", "testing"])
# 2/4 = 50% overlap, > 30% threshold
assert not is_cross
def test_low_overlap_is_cross_domain(self):
"""Low tag overlap should be cross-domain."""
mem = Memory(
id="test-1",
content="Test",
meta=MemoryMetadata(tags=["python", "api"]),
use_count=1,
strength=1.0,
)
is_cross = detect_cross_domain_usage(mem, ["frontend", "react", "typescript"])
# 0/5 = 0% overlap, < 30% threshold
assert is_cross
def test_no_memory_tags_returns_false(self):
"""No memory tags should return False."""
mem = Memory(
id="test-1",
content="Test",
meta=MemoryMetadata(tags=[]),
use_count=1,
strength=1.0,
)
is_cross = detect_cross_domain_usage(mem, ["python", "api"])
assert not is_cross
def test_no_context_tags_returns_false(self):
"""No context tags should return False."""
mem = Memory(
id="test-1",
content="Test",
meta=MemoryMetadata(tags=["python", "api"]),
use_count=1,
strength=1.0,
)
is_cross = detect_cross_domain_usage(mem, [])
assert not is_cross
def test_partial_overlap_threshold(self):
"""Test the 30% threshold boundary."""
mem = Memory(
id="test-1",
content="Test",
meta=MemoryMetadata(tags=["tag1", "tag2", "tag3"]),
use_count=1,
strength=1.0,
)
# 1/4 = 25% overlap, < 30% β cross-domain
is_cross_low = detect_cross_domain_usage(mem, ["tag1", "tag4", "tag5", "tag6"])
assert is_cross_low
# 2/4 = 50% overlap, > 30% β not cross-domain
is_cross_high = detect_cross_domain_usage(mem, ["tag1", "tag2", "tag4"])
assert not is_cross_high