feat(memory): #87 project setup & core architecture
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>
This commit is contained in:
243
backend/tests/unit/services/memory/test_config.py
Normal file
243
backend/tests/unit/services/memory/test_config.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
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"
|
||||
Reference in New Issue
Block a user