forked from cardosofelipe/fast-next-template
Implements Sub-Issue #87 of Issue #62 (Agent Memory System). Core infrastructure: - memory/types.py: Type definitions for all memory types (Working, Episodic, Semantic, Procedural) with enums for MemoryType, ScopeLevel, Outcome - memory/config.py: MemorySettings with MEM_ env prefix, thread-safe singleton - memory/exceptions.py: Comprehensive exception hierarchy for memory operations - memory/manager.py: MemoryManager facade with placeholder methods Directory structure: - working/: Working memory (Redis/in-memory) - to be implemented in #89 - episodic/: Episodic memory (experiences) - to be implemented in #90 - semantic/: Semantic memory (facts) - to be implemented in #91 - procedural/: Procedural memory (skills) - to be implemented in #92 - scoping/: Scope management - to be implemented in #93 - indexing/: Vector indexing - to be implemented in #94 - consolidation/: Memory consolidation - to be implemented in #95 Tests: 71 unit tests for config, types, and exceptions Docs: Comprehensive implementation plan at docs/architecture/memory-system-plan.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
244 lines
8.5 KiB
Python
244 lines
8.5 KiB
Python
"""
|
|
Tests for Memory System Configuration.
|
|
"""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.services.memory.config import (
|
|
MemorySettings,
|
|
get_default_settings,
|
|
get_memory_settings,
|
|
reset_memory_settings,
|
|
)
|
|
|
|
|
|
class TestMemorySettings:
|
|
"""Tests for MemorySettings class."""
|
|
|
|
def test_default_settings(self) -> None:
|
|
"""Test that default settings are valid."""
|
|
settings = MemorySettings()
|
|
|
|
# Working memory defaults
|
|
assert settings.working_memory_backend == "redis"
|
|
assert settings.working_memory_default_ttl_seconds == 3600
|
|
assert settings.working_memory_max_items_per_session == 1000
|
|
|
|
# Redis defaults
|
|
assert settings.redis_url == "redis://localhost:6379/0"
|
|
assert settings.redis_prefix == "mem"
|
|
|
|
# Episodic defaults
|
|
assert settings.episodic_max_episodes_per_project == 10000
|
|
assert settings.episodic_default_importance == 0.5
|
|
|
|
# Semantic defaults
|
|
assert settings.semantic_max_facts_per_project == 50000
|
|
assert settings.semantic_min_confidence == 0.1
|
|
|
|
# Procedural defaults
|
|
assert settings.procedural_max_procedures_per_project == 1000
|
|
assert settings.procedural_min_success_rate == 0.3
|
|
|
|
# Embedding defaults
|
|
assert settings.embedding_model == "text-embedding-3-small"
|
|
assert settings.embedding_dimensions == 1536
|
|
|
|
# Retrieval defaults
|
|
assert settings.retrieval_default_limit == 10
|
|
assert settings.retrieval_max_limit == 100
|
|
|
|
def test_invalid_backend(self) -> None:
|
|
"""Test that invalid backend raises error."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MemorySettings(working_memory_backend="invalid")
|
|
|
|
assert "backend must be one of" in str(exc_info.value)
|
|
|
|
def test_valid_backends(self) -> None:
|
|
"""Test valid backend values."""
|
|
redis_settings = MemorySettings(working_memory_backend="redis")
|
|
assert redis_settings.working_memory_backend == "redis"
|
|
|
|
memory_settings = MemorySettings(working_memory_backend="memory")
|
|
assert memory_settings.working_memory_backend == "memory"
|
|
|
|
def test_invalid_embedding_model(self) -> None:
|
|
"""Test that invalid embedding model raises error."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MemorySettings(embedding_model="invalid-model")
|
|
|
|
assert "embedding_model must be one of" in str(exc_info.value)
|
|
|
|
def test_valid_embedding_models(self) -> None:
|
|
"""Test valid embedding model values."""
|
|
for model in [
|
|
"text-embedding-3-small",
|
|
"text-embedding-3-large",
|
|
"text-embedding-ada-002",
|
|
]:
|
|
settings = MemorySettings(embedding_model=model)
|
|
assert settings.embedding_model == model
|
|
|
|
def test_retrieval_limit_validation(self) -> None:
|
|
"""Test that default limit cannot exceed max limit."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MemorySettings(
|
|
retrieval_default_limit=50,
|
|
retrieval_max_limit=25,
|
|
)
|
|
|
|
assert "cannot exceed retrieval_max_limit" in str(exc_info.value)
|
|
|
|
def test_valid_retrieval_limits(self) -> None:
|
|
"""Test valid retrieval limit combinations."""
|
|
settings = MemorySettings(
|
|
retrieval_default_limit=10,
|
|
retrieval_max_limit=50,
|
|
)
|
|
assert settings.retrieval_default_limit == 10
|
|
assert settings.retrieval_max_limit == 50
|
|
|
|
# Equal limits should be valid
|
|
settings = MemorySettings(
|
|
retrieval_default_limit=25,
|
|
retrieval_max_limit=25,
|
|
)
|
|
assert settings.retrieval_default_limit == 25
|
|
assert settings.retrieval_max_limit == 25
|
|
|
|
def test_ttl_bounds(self) -> None:
|
|
"""Test TTL setting bounds."""
|
|
# Valid TTL
|
|
settings = MemorySettings(working_memory_default_ttl_seconds=1800)
|
|
assert settings.working_memory_default_ttl_seconds == 1800
|
|
|
|
# Too low
|
|
with pytest.raises(ValidationError):
|
|
MemorySettings(working_memory_default_ttl_seconds=30)
|
|
|
|
# Too high
|
|
with pytest.raises(ValidationError):
|
|
MemorySettings(working_memory_default_ttl_seconds=100000)
|
|
|
|
def test_confidence_bounds(self) -> None:
|
|
"""Test confidence score bounds."""
|
|
# Valid confidence
|
|
settings = MemorySettings(semantic_min_confidence=0.5)
|
|
assert settings.semantic_min_confidence == 0.5
|
|
|
|
# Bounds
|
|
settings = MemorySettings(semantic_min_confidence=0.0)
|
|
assert settings.semantic_min_confidence == 0.0
|
|
|
|
settings = MemorySettings(semantic_min_confidence=1.0)
|
|
assert settings.semantic_min_confidence == 1.0
|
|
|
|
# Out of bounds
|
|
with pytest.raises(ValidationError):
|
|
MemorySettings(semantic_min_confidence=-0.1)
|
|
|
|
with pytest.raises(ValidationError):
|
|
MemorySettings(semantic_min_confidence=1.1)
|
|
|
|
def test_get_working_memory_config(self) -> None:
|
|
"""Test working memory config dictionary."""
|
|
settings = MemorySettings()
|
|
config = settings.get_working_memory_config()
|
|
|
|
assert config["backend"] == "redis"
|
|
assert config["default_ttl_seconds"] == 3600
|
|
assert config["max_items_per_session"] == 1000
|
|
assert config["max_value_size_bytes"] == 1048576
|
|
assert config["checkpoint_enabled"] is True
|
|
|
|
def test_get_redis_config(self) -> None:
|
|
"""Test Redis config dictionary."""
|
|
settings = MemorySettings()
|
|
config = settings.get_redis_config()
|
|
|
|
assert config["url"] == "redis://localhost:6379/0"
|
|
assert config["prefix"] == "mem"
|
|
assert config["connection_timeout_seconds"] == 5
|
|
|
|
def test_get_embedding_config(self) -> None:
|
|
"""Test embedding config dictionary."""
|
|
settings = MemorySettings()
|
|
config = settings.get_embedding_config()
|
|
|
|
assert config["model"] == "text-embedding-3-small"
|
|
assert config["dimensions"] == 1536
|
|
assert config["batch_size"] == 100
|
|
assert config["cache_enabled"] is True
|
|
|
|
def test_get_consolidation_config(self) -> None:
|
|
"""Test consolidation config dictionary."""
|
|
settings = MemorySettings()
|
|
config = settings.get_consolidation_config()
|
|
|
|
assert config["enabled"] is True
|
|
assert config["batch_size"] == 100
|
|
assert config["schedule_cron"] == "0 3 * * *"
|
|
assert config["working_to_episodic_delay_minutes"] == 30
|
|
|
|
def test_to_dict(self) -> None:
|
|
"""Test full settings to dictionary."""
|
|
settings = MemorySettings()
|
|
config = settings.to_dict()
|
|
|
|
assert "working_memory" in config
|
|
assert "redis" in config
|
|
assert "episodic" in config
|
|
assert "semantic" in config
|
|
assert "procedural" in config
|
|
assert "embedding" in config
|
|
assert "retrieval" in config
|
|
assert "consolidation" in config
|
|
assert "pruning" in config
|
|
assert "cache" in config
|
|
assert "performance" in config
|
|
|
|
|
|
class TestMemorySettingsSingleton:
|
|
"""Tests for MemorySettings singleton functions."""
|
|
|
|
def setup_method(self) -> None:
|
|
"""Reset singleton before each test."""
|
|
reset_memory_settings()
|
|
|
|
def teardown_method(self) -> None:
|
|
"""Reset singleton after each test."""
|
|
reset_memory_settings()
|
|
|
|
def test_get_memory_settings_singleton(self) -> None:
|
|
"""Test that get_memory_settings returns same instance."""
|
|
settings1 = get_memory_settings()
|
|
settings2 = get_memory_settings()
|
|
|
|
assert settings1 is settings2
|
|
|
|
def test_reset_memory_settings(self) -> None:
|
|
"""Test that reset creates new instance."""
|
|
settings1 = get_memory_settings()
|
|
reset_memory_settings()
|
|
settings2 = get_memory_settings()
|
|
|
|
assert settings1 is not settings2
|
|
|
|
def test_get_default_settings_cached(self) -> None:
|
|
"""Test that get_default_settings is cached."""
|
|
# Clear the lru_cache first
|
|
get_default_settings.cache_clear()
|
|
|
|
settings1 = get_default_settings()
|
|
settings2 = get_default_settings()
|
|
|
|
assert settings1 is settings2
|
|
|
|
def test_default_settings_immutable_pattern(self) -> None:
|
|
"""Test that default settings provide consistent values."""
|
|
defaults = get_default_settings()
|
|
assert defaults.working_memory_backend == "redis"
|
|
assert defaults.embedding_model == "text-embedding-3-small"
|