forked from cardosofelipe/fast-next-template
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:
1
backend/tests/unit/services/memory/__init__.py
Normal file
1
backend/tests/unit/services/memory/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the Agent Memory System."""
|
||||
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"
|
||||
325
backend/tests/unit/services/memory/test_exceptions.py
Normal file
325
backend/tests/unit/services/memory/test_exceptions.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
Tests for Memory System Exceptions.
|
||||
"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.memory.exceptions import (
|
||||
CheckpointError,
|
||||
EmbeddingError,
|
||||
MemoryCapacityError,
|
||||
MemoryConflictError,
|
||||
MemoryConsolidationError,
|
||||
MemoryError,
|
||||
MemoryExpiredError,
|
||||
MemoryNotFoundError,
|
||||
MemoryRetrievalError,
|
||||
MemoryScopeError,
|
||||
MemorySerializationError,
|
||||
MemoryStorageError,
|
||||
)
|
||||
|
||||
|
||||
class TestMemoryError:
|
||||
"""Tests for base MemoryError class."""
|
||||
|
||||
def test_basic_error(self) -> None:
|
||||
"""Test creating a basic memory error."""
|
||||
error = MemoryError("Something went wrong")
|
||||
|
||||
assert str(error) == "Something went wrong"
|
||||
assert error.message == "Something went wrong"
|
||||
assert error.memory_type is None
|
||||
assert error.scope_type is None
|
||||
assert error.scope_id is None
|
||||
assert error.details == {}
|
||||
|
||||
def test_error_with_context(self) -> None:
|
||||
"""Test creating an error with context."""
|
||||
error = MemoryError(
|
||||
"Operation failed",
|
||||
memory_type="episodic",
|
||||
scope_type="project",
|
||||
scope_id="proj-123",
|
||||
details={"operation": "search"},
|
||||
)
|
||||
|
||||
assert error.memory_type == "episodic"
|
||||
assert error.scope_type == "project"
|
||||
assert error.scope_id == "proj-123"
|
||||
assert error.details == {"operation": "search"}
|
||||
|
||||
def test_error_inheritance(self) -> None:
|
||||
"""Test that MemoryError inherits from Exception."""
|
||||
error = MemoryError("test")
|
||||
assert isinstance(error, Exception)
|
||||
|
||||
|
||||
class TestMemoryNotFoundError:
|
||||
"""Tests for MemoryNotFoundError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = MemoryNotFoundError()
|
||||
assert error.message == "Memory not found"
|
||||
|
||||
def test_with_memory_id(self) -> None:
|
||||
"""Test error with memory ID."""
|
||||
memory_id = uuid4()
|
||||
error = MemoryNotFoundError(
|
||||
f"Memory {memory_id} not found",
|
||||
memory_id=memory_id,
|
||||
)
|
||||
|
||||
assert error.memory_id == memory_id
|
||||
|
||||
def test_with_key(self) -> None:
|
||||
"""Test error with key."""
|
||||
error = MemoryNotFoundError(
|
||||
"Key not found",
|
||||
key="my_key",
|
||||
)
|
||||
|
||||
assert error.key == "my_key"
|
||||
|
||||
|
||||
class TestMemoryCapacityError:
|
||||
"""Tests for MemoryCapacityError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = MemoryCapacityError()
|
||||
assert error.message == "Memory capacity exceeded"
|
||||
|
||||
def test_with_sizes(self) -> None:
|
||||
"""Test error with size information."""
|
||||
error = MemoryCapacityError(
|
||||
"Working memory full",
|
||||
current_size=1048576,
|
||||
max_size=1000000,
|
||||
item_count=500,
|
||||
)
|
||||
|
||||
assert error.current_size == 1048576
|
||||
assert error.max_size == 1000000
|
||||
assert error.item_count == 500
|
||||
|
||||
|
||||
class TestMemoryExpiredError:
|
||||
"""Tests for MemoryExpiredError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = MemoryExpiredError()
|
||||
assert error.message == "Memory has expired"
|
||||
|
||||
def test_with_expiry_info(self) -> None:
|
||||
"""Test error with expiry information."""
|
||||
error = MemoryExpiredError(
|
||||
"Key expired",
|
||||
key="session_data",
|
||||
expired_at="2025-01-05T00:00:00Z",
|
||||
)
|
||||
|
||||
assert error.key == "session_data"
|
||||
assert error.expired_at == "2025-01-05T00:00:00Z"
|
||||
|
||||
|
||||
class TestMemoryStorageError:
|
||||
"""Tests for MemoryStorageError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = MemoryStorageError()
|
||||
assert error.message == "Memory storage operation failed"
|
||||
|
||||
def test_with_operation_info(self) -> None:
|
||||
"""Test error with operation information."""
|
||||
error = MemoryStorageError(
|
||||
"Redis write failed",
|
||||
operation="set",
|
||||
backend="redis",
|
||||
)
|
||||
|
||||
assert error.operation == "set"
|
||||
assert error.backend == "redis"
|
||||
|
||||
|
||||
class TestMemorySerializationError:
|
||||
"""Tests for MemorySerializationError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = MemorySerializationError()
|
||||
assert error.message == "Memory serialization failed"
|
||||
|
||||
def test_with_content_type(self) -> None:
|
||||
"""Test error with content type."""
|
||||
error = MemorySerializationError(
|
||||
"Cannot serialize function",
|
||||
content_type="function",
|
||||
)
|
||||
|
||||
assert error.content_type == "function"
|
||||
|
||||
|
||||
class TestMemoryScopeError:
|
||||
"""Tests for MemoryScopeError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = MemoryScopeError()
|
||||
assert error.message == "Memory scope error"
|
||||
|
||||
def test_with_scope_info(self) -> None:
|
||||
"""Test error with scope information."""
|
||||
error = MemoryScopeError(
|
||||
"Scope access denied",
|
||||
requested_scope="global",
|
||||
allowed_scopes=["project", "session"],
|
||||
)
|
||||
|
||||
assert error.requested_scope == "global"
|
||||
assert error.allowed_scopes == ["project", "session"]
|
||||
|
||||
|
||||
class TestMemoryConsolidationError:
|
||||
"""Tests for MemoryConsolidationError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = MemoryConsolidationError()
|
||||
assert error.message == "Memory consolidation failed"
|
||||
|
||||
def test_with_consolidation_info(self) -> None:
|
||||
"""Test error with consolidation information."""
|
||||
error = MemoryConsolidationError(
|
||||
"Transfer failed",
|
||||
source_type="working",
|
||||
target_type="episodic",
|
||||
items_processed=50,
|
||||
)
|
||||
|
||||
assert error.source_type == "working"
|
||||
assert error.target_type == "episodic"
|
||||
assert error.items_processed == 50
|
||||
|
||||
|
||||
class TestMemoryRetrievalError:
|
||||
"""Tests for MemoryRetrievalError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = MemoryRetrievalError()
|
||||
assert error.message == "Memory retrieval failed"
|
||||
|
||||
def test_with_query_info(self) -> None:
|
||||
"""Test error with query information."""
|
||||
error = MemoryRetrievalError(
|
||||
"Search timeout",
|
||||
query="complex search query",
|
||||
retrieval_type="semantic",
|
||||
)
|
||||
|
||||
assert error.query == "complex search query"
|
||||
assert error.retrieval_type == "semantic"
|
||||
|
||||
|
||||
class TestEmbeddingError:
|
||||
"""Tests for EmbeddingError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = EmbeddingError()
|
||||
assert error.message == "Embedding generation failed"
|
||||
|
||||
def test_with_embedding_info(self) -> None:
|
||||
"""Test error with embedding information."""
|
||||
error = EmbeddingError(
|
||||
"Content too long",
|
||||
content_length=100000,
|
||||
model="text-embedding-3-small",
|
||||
)
|
||||
|
||||
assert error.content_length == 100000
|
||||
assert error.model == "text-embedding-3-small"
|
||||
|
||||
|
||||
class TestCheckpointError:
|
||||
"""Tests for CheckpointError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = CheckpointError()
|
||||
assert error.message == "Checkpoint operation failed"
|
||||
|
||||
def test_with_checkpoint_info(self) -> None:
|
||||
"""Test error with checkpoint information."""
|
||||
error = CheckpointError(
|
||||
"Restore failed",
|
||||
checkpoint_id="chk-123",
|
||||
operation="restore",
|
||||
)
|
||||
|
||||
assert error.checkpoint_id == "chk-123"
|
||||
assert error.operation == "restore"
|
||||
|
||||
|
||||
class TestMemoryConflictError:
|
||||
"""Tests for MemoryConflictError class."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = MemoryConflictError()
|
||||
assert error.message == "Memory conflict detected"
|
||||
|
||||
def test_with_conflict_info(self) -> None:
|
||||
"""Test error with conflict information."""
|
||||
id1 = uuid4()
|
||||
id2 = uuid4()
|
||||
error = MemoryConflictError(
|
||||
"Contradictory facts detected",
|
||||
conflicting_ids=[id1, id2],
|
||||
conflict_type="semantic",
|
||||
)
|
||||
|
||||
assert len(error.conflicting_ids) == 2
|
||||
assert error.conflict_type == "semantic"
|
||||
|
||||
|
||||
class TestExceptionHierarchy:
|
||||
"""Tests for exception inheritance hierarchy."""
|
||||
|
||||
def test_all_exceptions_inherit_from_memory_error(self) -> None:
|
||||
"""Test that all exceptions inherit from MemoryError."""
|
||||
exceptions = [
|
||||
MemoryNotFoundError(),
|
||||
MemoryCapacityError(),
|
||||
MemoryExpiredError(),
|
||||
MemoryStorageError(),
|
||||
MemorySerializationError(),
|
||||
MemoryScopeError(),
|
||||
MemoryConsolidationError(),
|
||||
MemoryRetrievalError(),
|
||||
EmbeddingError(),
|
||||
CheckpointError(),
|
||||
MemoryConflictError(),
|
||||
]
|
||||
|
||||
for exc in exceptions:
|
||||
assert isinstance(exc, MemoryError)
|
||||
assert isinstance(exc, Exception)
|
||||
|
||||
def test_can_catch_base_error(self) -> None:
|
||||
"""Test that catching MemoryError catches all subclasses."""
|
||||
exceptions = [
|
||||
MemoryNotFoundError("not found"),
|
||||
MemoryCapacityError("capacity"),
|
||||
MemoryStorageError("storage"),
|
||||
]
|
||||
|
||||
for exc in exceptions:
|
||||
with pytest.raises(MemoryError):
|
||||
raise exc
|
||||
411
backend/tests/unit/services/memory/test_types.py
Normal file
411
backend/tests/unit/services/memory/test_types.py
Normal file
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
Tests for Memory System Types.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from app.services.memory.types import (
|
||||
ConsolidationStatus,
|
||||
ConsolidationType,
|
||||
EpisodeCreate,
|
||||
Fact,
|
||||
FactCreate,
|
||||
MemoryItem,
|
||||
MemoryStats,
|
||||
MemoryType,
|
||||
Outcome,
|
||||
Procedure,
|
||||
ProcedureCreate,
|
||||
RetrievalResult,
|
||||
ScopeContext,
|
||||
ScopeLevel,
|
||||
Step,
|
||||
TaskState,
|
||||
WorkingMemoryItem,
|
||||
)
|
||||
|
||||
|
||||
class TestEnums:
|
||||
"""Tests for memory enums."""
|
||||
|
||||
def test_memory_type_values(self) -> None:
|
||||
"""Test MemoryType enum values."""
|
||||
assert MemoryType.WORKING == "working"
|
||||
assert MemoryType.EPISODIC == "episodic"
|
||||
assert MemoryType.SEMANTIC == "semantic"
|
||||
assert MemoryType.PROCEDURAL == "procedural"
|
||||
|
||||
def test_scope_level_values(self) -> None:
|
||||
"""Test ScopeLevel enum values."""
|
||||
assert ScopeLevel.GLOBAL == "global"
|
||||
assert ScopeLevel.PROJECT == "project"
|
||||
assert ScopeLevel.AGENT_TYPE == "agent_type"
|
||||
assert ScopeLevel.AGENT_INSTANCE == "agent_instance"
|
||||
assert ScopeLevel.SESSION == "session"
|
||||
|
||||
def test_outcome_values(self) -> None:
|
||||
"""Test Outcome enum values."""
|
||||
assert Outcome.SUCCESS == "success"
|
||||
assert Outcome.FAILURE == "failure"
|
||||
assert Outcome.PARTIAL == "partial"
|
||||
|
||||
def test_consolidation_status_values(self) -> None:
|
||||
"""Test ConsolidationStatus enum values."""
|
||||
assert ConsolidationStatus.PENDING == "pending"
|
||||
assert ConsolidationStatus.RUNNING == "running"
|
||||
assert ConsolidationStatus.COMPLETED == "completed"
|
||||
assert ConsolidationStatus.FAILED == "failed"
|
||||
|
||||
def test_consolidation_type_values(self) -> None:
|
||||
"""Test ConsolidationType enum values."""
|
||||
assert ConsolidationType.WORKING_TO_EPISODIC == "working_to_episodic"
|
||||
assert ConsolidationType.EPISODIC_TO_SEMANTIC == "episodic_to_semantic"
|
||||
assert ConsolidationType.EPISODIC_TO_PROCEDURAL == "episodic_to_procedural"
|
||||
assert ConsolidationType.PRUNING == "pruning"
|
||||
|
||||
|
||||
class TestScopeContext:
|
||||
"""Tests for ScopeContext dataclass."""
|
||||
|
||||
def test_create_scope_context(self) -> None:
|
||||
"""Test creating a scope context."""
|
||||
scope = ScopeContext(
|
||||
scope_type=ScopeLevel.SESSION,
|
||||
scope_id="sess-123",
|
||||
)
|
||||
|
||||
assert scope.scope_type == ScopeLevel.SESSION
|
||||
assert scope.scope_id == "sess-123"
|
||||
assert scope.parent is None
|
||||
|
||||
def test_scope_with_parent(self) -> None:
|
||||
"""Test creating a scope with parent."""
|
||||
parent = ScopeContext(
|
||||
scope_type=ScopeLevel.PROJECT,
|
||||
scope_id="proj-123",
|
||||
)
|
||||
child = ScopeContext(
|
||||
scope_type=ScopeLevel.SESSION,
|
||||
scope_id="sess-456",
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
assert child.parent is parent
|
||||
assert child.parent.scope_type == ScopeLevel.PROJECT
|
||||
|
||||
def test_get_hierarchy(self) -> None:
|
||||
"""Test getting scope hierarchy."""
|
||||
global_scope = ScopeContext(
|
||||
scope_type=ScopeLevel.GLOBAL,
|
||||
scope_id="global",
|
||||
)
|
||||
project_scope = ScopeContext(
|
||||
scope_type=ScopeLevel.PROJECT,
|
||||
scope_id="proj-123",
|
||||
parent=global_scope,
|
||||
)
|
||||
session_scope = ScopeContext(
|
||||
scope_type=ScopeLevel.SESSION,
|
||||
scope_id="sess-456",
|
||||
parent=project_scope,
|
||||
)
|
||||
|
||||
hierarchy = session_scope.get_hierarchy()
|
||||
|
||||
assert len(hierarchy) == 3
|
||||
assert hierarchy[0].scope_type == ScopeLevel.GLOBAL
|
||||
assert hierarchy[1].scope_type == ScopeLevel.PROJECT
|
||||
assert hierarchy[2].scope_type == ScopeLevel.SESSION
|
||||
|
||||
def test_to_key_prefix(self) -> None:
|
||||
"""Test converting scope to key prefix."""
|
||||
scope = ScopeContext(
|
||||
scope_type=ScopeLevel.SESSION,
|
||||
scope_id="sess-123",
|
||||
)
|
||||
|
||||
prefix = scope.to_key_prefix()
|
||||
assert prefix == "session:sess-123"
|
||||
|
||||
|
||||
class TestMemoryItem:
|
||||
"""Tests for MemoryItem dataclass."""
|
||||
|
||||
def test_create_memory_item(self) -> None:
|
||||
"""Test creating a memory item."""
|
||||
now = datetime.now()
|
||||
item = MemoryItem(
|
||||
id=uuid4(),
|
||||
memory_type=MemoryType.EPISODIC,
|
||||
scope_type=ScopeLevel.PROJECT,
|
||||
scope_id="proj-123",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
assert item.memory_type == MemoryType.EPISODIC
|
||||
assert item.scope_type == ScopeLevel.PROJECT
|
||||
assert item.metadata == {}
|
||||
|
||||
def test_get_age_seconds(self) -> None:
|
||||
"""Test getting item age."""
|
||||
past = datetime.now() - timedelta(seconds=100)
|
||||
item = MemoryItem(
|
||||
id=uuid4(),
|
||||
memory_type=MemoryType.SEMANTIC,
|
||||
scope_type=ScopeLevel.GLOBAL,
|
||||
scope_id="global",
|
||||
created_at=past,
|
||||
updated_at=past,
|
||||
)
|
||||
|
||||
age = item.get_age_seconds()
|
||||
assert age >= 100
|
||||
assert age < 105 # Allow small margin
|
||||
|
||||
|
||||
class TestWorkingMemoryItem:
|
||||
"""Tests for WorkingMemoryItem dataclass."""
|
||||
|
||||
def test_create_working_memory_item(self) -> None:
|
||||
"""Test creating a working memory item."""
|
||||
item = WorkingMemoryItem(
|
||||
id=uuid4(),
|
||||
scope_type=ScopeLevel.SESSION,
|
||||
scope_id="sess-123",
|
||||
key="my_key",
|
||||
value={"data": "value"},
|
||||
)
|
||||
|
||||
assert item.key == "my_key"
|
||||
assert item.value == {"data": "value"}
|
||||
assert item.expires_at is None
|
||||
|
||||
def test_is_expired_no_expiry(self) -> None:
|
||||
"""Test is_expired with no expiry set."""
|
||||
item = WorkingMemoryItem(
|
||||
id=uuid4(),
|
||||
scope_type=ScopeLevel.SESSION,
|
||||
scope_id="sess-123",
|
||||
key="my_key",
|
||||
value="value",
|
||||
)
|
||||
|
||||
assert item.is_expired() is False
|
||||
|
||||
def test_is_expired_future(self) -> None:
|
||||
"""Test is_expired with future expiry."""
|
||||
item = WorkingMemoryItem(
|
||||
id=uuid4(),
|
||||
scope_type=ScopeLevel.SESSION,
|
||||
scope_id="sess-123",
|
||||
key="my_key",
|
||||
value="value",
|
||||
expires_at=datetime.now() + timedelta(hours=1),
|
||||
)
|
||||
|
||||
assert item.is_expired() is False
|
||||
|
||||
def test_is_expired_past(self) -> None:
|
||||
"""Test is_expired with past expiry."""
|
||||
item = WorkingMemoryItem(
|
||||
id=uuid4(),
|
||||
scope_type=ScopeLevel.SESSION,
|
||||
scope_id="sess-123",
|
||||
key="my_key",
|
||||
value="value",
|
||||
expires_at=datetime.now() - timedelta(hours=1),
|
||||
)
|
||||
|
||||
assert item.is_expired() is True
|
||||
|
||||
|
||||
class TestTaskState:
|
||||
"""Tests for TaskState dataclass."""
|
||||
|
||||
def test_create_task_state(self) -> None:
|
||||
"""Test creating a task state."""
|
||||
state = TaskState(
|
||||
task_id="task-123",
|
||||
task_type="code_review",
|
||||
description="Review PR #42",
|
||||
)
|
||||
|
||||
assert state.task_id == "task-123"
|
||||
assert state.task_type == "code_review"
|
||||
assert state.status == "in_progress"
|
||||
assert state.current_step == 0
|
||||
assert state.progress_percent == 0.0
|
||||
|
||||
def test_task_state_with_progress(self) -> None:
|
||||
"""Test task state with progress."""
|
||||
state = TaskState(
|
||||
task_id="task-123",
|
||||
task_type="implementation",
|
||||
description="Implement feature X",
|
||||
current_step=3,
|
||||
total_steps=5,
|
||||
progress_percent=60.0,
|
||||
)
|
||||
|
||||
assert state.current_step == 3
|
||||
assert state.total_steps == 5
|
||||
assert state.progress_percent == 60.0
|
||||
|
||||
|
||||
class TestEpisode:
|
||||
"""Tests for Episode and EpisodeCreate dataclasses."""
|
||||
|
||||
def test_create_episode_data(self) -> None:
|
||||
"""Test creating episode create data."""
|
||||
data = EpisodeCreate(
|
||||
project_id=uuid4(),
|
||||
session_id="sess-123",
|
||||
task_type="bug_fix",
|
||||
task_description="Fix login bug",
|
||||
actions=[{"action": "read_file", "file": "auth.py"}],
|
||||
context_summary="User reported login issues",
|
||||
outcome=Outcome.SUCCESS,
|
||||
outcome_details="Fixed by updating validation",
|
||||
duration_seconds=120.5,
|
||||
tokens_used=5000,
|
||||
)
|
||||
|
||||
assert data.task_type == "bug_fix"
|
||||
assert data.outcome == Outcome.SUCCESS
|
||||
assert len(data.actions) == 1
|
||||
assert data.importance_score == 0.5 # Default
|
||||
|
||||
|
||||
class TestFact:
|
||||
"""Tests for Fact and FactCreate dataclasses."""
|
||||
|
||||
def test_create_fact_data(self) -> None:
|
||||
"""Test creating fact create data."""
|
||||
data = FactCreate(
|
||||
subject="FastAPI",
|
||||
predicate="uses",
|
||||
object="Starlette framework",
|
||||
)
|
||||
|
||||
assert data.subject == "FastAPI"
|
||||
assert data.predicate == "uses"
|
||||
assert data.object == "Starlette framework"
|
||||
assert data.confidence == 0.8 # Default
|
||||
assert data.project_id is None # Global fact
|
||||
|
||||
|
||||
class TestProcedure:
|
||||
"""Tests for Procedure and ProcedureCreate dataclasses."""
|
||||
|
||||
def test_create_procedure_data(self) -> None:
|
||||
"""Test creating procedure create data."""
|
||||
data = ProcedureCreate(
|
||||
name="review_pr",
|
||||
trigger_pattern="review pull request",
|
||||
steps=[
|
||||
{"action": "checkout_branch"},
|
||||
{"action": "run_tests"},
|
||||
{"action": "review_changes"},
|
||||
],
|
||||
)
|
||||
|
||||
assert data.name == "review_pr"
|
||||
assert len(data.steps) == 3
|
||||
|
||||
def test_procedure_success_rate(self) -> None:
|
||||
"""Test procedure success rate calculation."""
|
||||
now = datetime.now()
|
||||
procedure = Procedure(
|
||||
id=uuid4(),
|
||||
project_id=None,
|
||||
agent_type_id=None,
|
||||
name="test_proc",
|
||||
trigger_pattern="test",
|
||||
steps=[],
|
||||
success_count=8,
|
||||
failure_count=2,
|
||||
last_used=now,
|
||||
embedding=None,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
assert procedure.success_rate == 0.8
|
||||
|
||||
def test_procedure_success_rate_zero_uses(self) -> None:
|
||||
"""Test procedure success rate with zero uses."""
|
||||
now = datetime.now()
|
||||
procedure = Procedure(
|
||||
id=uuid4(),
|
||||
project_id=None,
|
||||
agent_type_id=None,
|
||||
name="test_proc",
|
||||
trigger_pattern="test",
|
||||
steps=[],
|
||||
success_count=0,
|
||||
failure_count=0,
|
||||
last_used=None,
|
||||
embedding=None,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
assert procedure.success_rate == 0.0
|
||||
|
||||
|
||||
class TestStep:
|
||||
"""Tests for Step dataclass."""
|
||||
|
||||
def test_create_step(self) -> None:
|
||||
"""Test creating a step."""
|
||||
step = Step(
|
||||
order=1,
|
||||
action="run_tests",
|
||||
parameters={"verbose": True},
|
||||
expected_outcome="All tests pass",
|
||||
)
|
||||
|
||||
assert step.order == 1
|
||||
assert step.action == "run_tests"
|
||||
assert step.parameters == {"verbose": True}
|
||||
|
||||
|
||||
class TestRetrievalResult:
|
||||
"""Tests for RetrievalResult dataclass."""
|
||||
|
||||
def test_create_retrieval_result(self) -> None:
|
||||
"""Test creating a retrieval result."""
|
||||
result: RetrievalResult[Fact] = RetrievalResult(
|
||||
items=[],
|
||||
total_count=0,
|
||||
query="test query",
|
||||
retrieval_type="semantic",
|
||||
latency_ms=15.5,
|
||||
)
|
||||
|
||||
assert result.query == "test query"
|
||||
assert result.latency_ms == 15.5
|
||||
assert result.metadata == {}
|
||||
|
||||
|
||||
class TestMemoryStats:
|
||||
"""Tests for MemoryStats dataclass."""
|
||||
|
||||
def test_create_memory_stats(self) -> None:
|
||||
"""Test creating memory stats."""
|
||||
stats = MemoryStats(
|
||||
memory_type=MemoryType.EPISODIC,
|
||||
scope_type=ScopeLevel.PROJECT,
|
||||
scope_id="proj-123",
|
||||
item_count=150,
|
||||
total_size_bytes=1048576,
|
||||
oldest_item_age_seconds=86400,
|
||||
newest_item_age_seconds=60,
|
||||
avg_item_size_bytes=6990.5,
|
||||
)
|
||||
|
||||
assert stats.memory_type == MemoryType.EPISODIC
|
||||
assert stats.item_count == 150
|
||||
assert stats.total_size_bytes == 1048576
|
||||
Reference in New Issue
Block a user