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:
2026-01-05 01:27:36 +01:00
parent 4b149b8a52
commit 085a748929
17 changed files with 3242 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Tests for the Agent Memory System."""

View 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"

View 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

View 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