feat(context): Phase 1 - Foundation types, config and exceptions (#79)
Implements the foundation for Context Management Engine: Types (backend/app/services/context/types/): - BaseContext: Abstract base with ID, content, priority, scoring - SystemContext: System prompts, personas, instructions - KnowledgeContext: RAG results from Knowledge Base MCP - ConversationContext: Chat history with role support - TaskContext: Task/issue context with acceptance criteria - ToolContext: Tool definitions and execution results - AssembledContext: Final assembled context result Configuration (config.py): - Token budget allocation (system 5%, task 10%, knowledge 40%, etc.) - Scoring weights (relevance 50%, recency 30%, priority 20%) - Cache settings (TTL, prefix) - Performance settings (max assembly time, parallel scoring) - Environment variable overrides with CTX_ prefix Exceptions (exceptions.py): - ContextError: Base exception - BudgetExceededError: Token budget violations - TokenCountError: Token counting failures - CompressionError: Compression failures - AssemblyTimeoutError: Assembly timeout - ScoringError, FormattingError, CacheError - ContextNotFoundError, InvalidContextError All 86 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
252
backend/tests/services/context/test_exceptions.py
Normal file
252
backend/tests/services/context/test_exceptions.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""Tests for context management exceptions."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.context.exceptions import (
|
||||
AssemblyTimeoutError,
|
||||
BudgetExceededError,
|
||||
CacheError,
|
||||
CompressionError,
|
||||
ContextError,
|
||||
ContextNotFoundError,
|
||||
FormattingError,
|
||||
InvalidContextError,
|
||||
ScoringError,
|
||||
TokenCountError,
|
||||
)
|
||||
|
||||
|
||||
class TestContextError:
|
||||
"""Tests for base ContextError."""
|
||||
|
||||
def test_basic_initialization(self) -> None:
|
||||
"""Test basic error initialization."""
|
||||
error = ContextError("Test error")
|
||||
assert error.message == "Test error"
|
||||
assert error.details == {}
|
||||
assert str(error) == "Test error"
|
||||
|
||||
def test_with_details(self) -> None:
|
||||
"""Test error with details."""
|
||||
error = ContextError("Test error", {"key": "value", "count": 42})
|
||||
assert error.details == {"key": "value", "count": 42}
|
||||
|
||||
def test_to_dict(self) -> None:
|
||||
"""Test conversion to dictionary."""
|
||||
error = ContextError("Test error", {"key": "value"})
|
||||
result = error.to_dict()
|
||||
|
||||
assert result["error_type"] == "ContextError"
|
||||
assert result["message"] == "Test error"
|
||||
assert result["details"] == {"key": "value"}
|
||||
|
||||
def test_inheritance(self) -> None:
|
||||
"""Test that ContextError inherits from Exception."""
|
||||
error = ContextError("Test")
|
||||
assert isinstance(error, Exception)
|
||||
|
||||
|
||||
class TestBudgetExceededError:
|
||||
"""Tests for BudgetExceededError."""
|
||||
|
||||
def test_default_message(self) -> None:
|
||||
"""Test default error message."""
|
||||
error = BudgetExceededError()
|
||||
assert "exceeded" in error.message.lower()
|
||||
|
||||
def test_with_budget_info(self) -> None:
|
||||
"""Test with budget information."""
|
||||
error = BudgetExceededError(
|
||||
allocated=1000,
|
||||
requested=1500,
|
||||
context_type="knowledge",
|
||||
)
|
||||
|
||||
assert error.allocated == 1000
|
||||
assert error.requested == 1500
|
||||
assert error.context_type == "knowledge"
|
||||
assert error.details["overage"] == 500
|
||||
|
||||
def test_to_dict_includes_budget_info(self) -> None:
|
||||
"""Test that to_dict includes budget info."""
|
||||
error = BudgetExceededError(
|
||||
allocated=1000,
|
||||
requested=1500,
|
||||
)
|
||||
result = error.to_dict()
|
||||
|
||||
assert result["details"]["allocated"] == 1000
|
||||
assert result["details"]["requested"] == 1500
|
||||
assert result["details"]["overage"] == 500
|
||||
|
||||
|
||||
class TestTokenCountError:
|
||||
"""Tests for TokenCountError."""
|
||||
|
||||
def test_basic_error(self) -> None:
|
||||
"""Test basic token count error."""
|
||||
error = TokenCountError()
|
||||
assert "token" in error.message.lower()
|
||||
|
||||
def test_with_model_info(self) -> None:
|
||||
"""Test with model information."""
|
||||
error = TokenCountError(
|
||||
message="Failed to count",
|
||||
model="claude-3-sonnet",
|
||||
text_length=5000,
|
||||
)
|
||||
|
||||
assert error.model == "claude-3-sonnet"
|
||||
assert error.text_length == 5000
|
||||
assert error.details["model"] == "claude-3-sonnet"
|
||||
|
||||
|
||||
class TestCompressionError:
|
||||
"""Tests for CompressionError."""
|
||||
|
||||
def test_basic_error(self) -> None:
|
||||
"""Test basic compression error."""
|
||||
error = CompressionError()
|
||||
assert "compress" in error.message.lower()
|
||||
|
||||
def test_with_token_info(self) -> None:
|
||||
"""Test with token information."""
|
||||
error = CompressionError(
|
||||
original_tokens=2000,
|
||||
target_tokens=1000,
|
||||
achieved_tokens=1500,
|
||||
)
|
||||
|
||||
assert error.original_tokens == 2000
|
||||
assert error.target_tokens == 1000
|
||||
assert error.achieved_tokens == 1500
|
||||
|
||||
|
||||
class TestAssemblyTimeoutError:
|
||||
"""Tests for AssemblyTimeoutError."""
|
||||
|
||||
def test_basic_error(self) -> None:
|
||||
"""Test basic timeout error."""
|
||||
error = AssemblyTimeoutError()
|
||||
assert "timed out" in error.message.lower()
|
||||
|
||||
def test_with_timing_info(self) -> None:
|
||||
"""Test with timing information."""
|
||||
error = AssemblyTimeoutError(
|
||||
timeout_ms=100,
|
||||
elapsed_ms=150.5,
|
||||
stage="scoring",
|
||||
)
|
||||
|
||||
assert error.timeout_ms == 100
|
||||
assert error.elapsed_ms == 150.5
|
||||
assert error.stage == "scoring"
|
||||
assert error.details["stage"] == "scoring"
|
||||
|
||||
|
||||
class TestScoringError:
|
||||
"""Tests for ScoringError."""
|
||||
|
||||
def test_basic_error(self) -> None:
|
||||
"""Test basic scoring error."""
|
||||
error = ScoringError()
|
||||
assert "score" in error.message.lower()
|
||||
|
||||
def test_with_scorer_info(self) -> None:
|
||||
"""Test with scorer information."""
|
||||
error = ScoringError(
|
||||
scorer_type="relevance",
|
||||
context_id="ctx-123",
|
||||
)
|
||||
|
||||
assert error.scorer_type == "relevance"
|
||||
assert error.context_id == "ctx-123"
|
||||
|
||||
|
||||
class TestFormattingError:
|
||||
"""Tests for FormattingError."""
|
||||
|
||||
def test_basic_error(self) -> None:
|
||||
"""Test basic formatting error."""
|
||||
error = FormattingError()
|
||||
assert "format" in error.message.lower()
|
||||
|
||||
def test_with_model_info(self) -> None:
|
||||
"""Test with model information."""
|
||||
error = FormattingError(
|
||||
model="claude-3-opus",
|
||||
adapter="ClaudeAdapter",
|
||||
)
|
||||
|
||||
assert error.model == "claude-3-opus"
|
||||
assert error.adapter == "ClaudeAdapter"
|
||||
|
||||
|
||||
class TestCacheError:
|
||||
"""Tests for CacheError."""
|
||||
|
||||
def test_basic_error(self) -> None:
|
||||
"""Test basic cache error."""
|
||||
error = CacheError()
|
||||
assert "cache" in error.message.lower()
|
||||
|
||||
def test_with_operation_info(self) -> None:
|
||||
"""Test with operation information."""
|
||||
error = CacheError(
|
||||
operation="get",
|
||||
cache_key="ctx:abc123",
|
||||
)
|
||||
|
||||
assert error.operation == "get"
|
||||
assert error.cache_key == "ctx:abc123"
|
||||
|
||||
|
||||
class TestContextNotFoundError:
|
||||
"""Tests for ContextNotFoundError."""
|
||||
|
||||
def test_basic_error(self) -> None:
|
||||
"""Test basic not found error."""
|
||||
error = ContextNotFoundError()
|
||||
assert "not found" in error.message.lower()
|
||||
|
||||
def test_with_source_info(self) -> None:
|
||||
"""Test with source information."""
|
||||
error = ContextNotFoundError(
|
||||
source="knowledge-base",
|
||||
query="authentication flow",
|
||||
)
|
||||
|
||||
assert error.source == "knowledge-base"
|
||||
assert error.query == "authentication flow"
|
||||
|
||||
|
||||
class TestInvalidContextError:
|
||||
"""Tests for InvalidContextError."""
|
||||
|
||||
def test_basic_error(self) -> None:
|
||||
"""Test basic invalid error."""
|
||||
error = InvalidContextError()
|
||||
assert "invalid" in error.message.lower()
|
||||
|
||||
def test_with_field_info(self) -> None:
|
||||
"""Test with field information."""
|
||||
error = InvalidContextError(
|
||||
field="content",
|
||||
value="",
|
||||
reason="Content cannot be empty",
|
||||
)
|
||||
|
||||
assert error.field == "content"
|
||||
assert error.value == ""
|
||||
assert error.reason == "Content cannot be empty"
|
||||
|
||||
def test_value_type_only_in_details(self) -> None:
|
||||
"""Test that only value type is included in details (not actual value)."""
|
||||
error = InvalidContextError(
|
||||
field="api_key",
|
||||
value="secret-key-here",
|
||||
)
|
||||
|
||||
# Actual value should not be in details
|
||||
assert "secret-key-here" not in str(error.details)
|
||||
assert error.details["value_type"] == "str"
|
||||
Reference in New Issue
Block a user