| """
|
| Tests for MnemoCore Error Handling
|
| ===================================
|
| Tests the exception hierarchy, error codes, and FastAPI integration.
|
| """
|
|
|
| import pytest
|
| import os
|
| import sys
|
|
|
|
|
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
| from mnemocore.core.exceptions import (
|
|
|
| MnemoCoreError,
|
| RecoverableError,
|
| IrrecoverableError,
|
| ErrorCategory,
|
|
|
| StorageError,
|
| StorageConnectionError,
|
| StorageTimeoutError,
|
| DataCorruptionError,
|
|
|
| VectorError,
|
| DimensionMismatchError,
|
| VectorOperationError,
|
|
|
| ConfigurationError,
|
|
|
| CircuitOpenError,
|
|
|
| MemoryOperationError,
|
|
|
| ValidationError,
|
| MetadataValidationError,
|
| AttributeValidationError,
|
|
|
| NotFoundError,
|
| AgentNotFoundError,
|
| MemoryNotFoundError,
|
|
|
| ProviderError,
|
| UnsupportedProviderError,
|
| UnsupportedTransportError,
|
| DependencyMissingError,
|
|
|
| wrap_storage_exception,
|
| is_debug_mode,
|
| )
|
|
|
|
|
| class TestExceptionHierarchy:
|
| """Test the exception inheritance hierarchy."""
|
|
|
| def test_base_exception(self):
|
| """Test MnemoCoreError base class."""
|
| exc = MnemoCoreError("Test error")
|
| assert str(exc) == "Test error"
|
| assert exc.message == "Test error"
|
| assert exc.context == {}
|
| assert exc.recoverable is True
|
| assert exc.error_code == "MNEMO_CORE_ERROR"
|
|
|
| def test_exception_with_context(self):
|
| """Test exception with context."""
|
| exc = MnemoCoreError("Test error", context={"key": "value"})
|
| assert exc.context == {"key": "value"}
|
| assert "context=" in str(exc)
|
|
|
| def test_exception_to_dict(self):
|
| """Test to_dict conversion."""
|
| exc = ValidationError(
|
| field="test_field",
|
| reason="Invalid value",
|
| value="bad_data"
|
| )
|
| d = exc.to_dict()
|
| assert d["error"] == "Validation error for 'test_field': Invalid value"
|
| assert d["code"] == "VALIDATION_ERROR"
|
| assert d["recoverable"] is False
|
| assert "traceback" not in d
|
|
|
| def test_exception_to_dict_with_traceback(self):
|
| """Test to_dict with traceback in debug mode."""
|
| exc = ValidationError(field="test", reason="test")
|
| d = exc.to_dict(include_traceback=True)
|
| assert "traceback" in d
|
|
|
|
|
| class TestRecoverableErrors:
|
| """Test recoverable error classes."""
|
|
|
| def test_storage_connection_error_is_recoverable(self):
|
| """Storage connection errors should be recoverable."""
|
| exc = StorageConnectionError("redis", "Connection refused")
|
| assert exc.recoverable is True
|
| assert exc.error_code == "STORAGE_CONNECTION_ERROR"
|
| assert exc.backend == "redis"
|
|
|
| def test_storage_timeout_error_is_recoverable(self):
|
| """Storage timeout errors should be recoverable."""
|
| exc = StorageTimeoutError("qdrant", "search", timeout_ms=5000)
|
| assert exc.recoverable is True
|
| assert exc.error_code == "STORAGE_TIMEOUT_ERROR"
|
| assert exc.backend == "qdrant"
|
| assert exc.operation == "search"
|
| assert exc.context["timeout_ms"] == 5000
|
|
|
| def test_circuit_open_error_is_recoverable(self):
|
| """Circuit breaker open errors should be recoverable."""
|
| exc = CircuitOpenError("storage", failures=5)
|
| assert exc.recoverable is True
|
| assert exc.error_code == "CIRCUIT_OPEN_ERROR"
|
| assert exc.breaker_name == "storage"
|
| assert exc.failures == 5
|
|
|
|
|
| class TestIrrecoverableErrors:
|
| """Test irrecoverable error classes."""
|
|
|
| def test_validation_error_is_irrecoverable(self):
|
| """Validation errors should be irrecoverable."""
|
| exc = ValidationError(field="content", reason="Cannot be empty")
|
| assert exc.recoverable is False
|
| assert exc.error_code == "VALIDATION_ERROR"
|
| assert exc.field == "content"
|
|
|
| def test_configuration_error_is_irrecoverable(self):
|
| """Configuration errors should be irrecoverable."""
|
| exc = ConfigurationError("api_key", "Missing required key")
|
| assert exc.recoverable is False
|
| assert exc.error_code == "CONFIGURATION_ERROR"
|
| assert exc.config_key == "api_key"
|
|
|
| def test_data_corruption_error_is_irrecoverable(self):
|
| """Data corruption errors should be irrecoverable."""
|
| exc = DataCorruptionError("mem_123", "Invalid checksum")
|
| assert exc.recoverable is False
|
| assert exc.error_code == "DATA_CORRUPTION_ERROR"
|
| assert exc.resource_id == "mem_123"
|
|
|
| def test_not_found_errors_are_irrecoverable(self):
|
| """Not found errors should be irrecoverable."""
|
| exc = MemoryNotFoundError("mem_123")
|
| assert exc.recoverable is False
|
| assert exc.error_code == "MEMORY_NOT_FOUND_ERROR"
|
|
|
| exc2 = AgentNotFoundError("agent_456")
|
| assert exc2.recoverable is False
|
| assert exc2.error_code == "AGENT_NOT_FOUND_ERROR"
|
|
|
| def test_unsupported_provider_error_is_irrecoverable(self):
|
| """Unsupported provider errors should be irrecoverable."""
|
| exc = UnsupportedProviderError("unknown", supported_providers=["openai", "anthropic"])
|
| assert exc.recoverable is False
|
| assert exc.error_code == "UNSUPPORTED_PROVIDER_ERROR"
|
| assert exc.provider == "unknown"
|
| assert "openai" in str(exc)
|
|
|
|
|
| class TestVectorErrors:
|
| """Test vector-related errors."""
|
|
|
| def test_dimension_mismatch_error(self):
|
| """Test dimension mismatch error."""
|
| exc = DimensionMismatchError(expected=16384, actual=10000, operation="encode")
|
| assert exc.recoverable is False
|
| assert exc.error_code == "DIMENSION_MISMATCH_ERROR"
|
| assert exc.expected == 16384
|
| assert exc.actual == 10000
|
| assert "16384" in str(exc)
|
| assert "10000" in str(exc)
|
|
|
| def test_vector_operation_error(self):
|
| """Test vector operation error."""
|
| exc = VectorOperationError("bundle", "NaN detected")
|
| assert exc.recoverable is False
|
| assert exc.error_code == "VECTOR_OPERATION_ERROR"
|
| assert exc.operation == "bundle"
|
|
|
|
|
| class TestStorageErrorWrapper:
|
| """Test wrap_storage_exception utility."""
|
|
|
| def test_wrap_timeout_exception(self):
|
| """Timeout exceptions should be wrapped as StorageTimeoutError."""
|
| exc = Exception("Connection timeout after 5000ms")
|
| wrapped = wrap_storage_exception("redis", "get", exc)
|
| assert isinstance(wrapped, StorageTimeoutError)
|
| assert wrapped.backend == "redis"
|
| assert wrapped.operation == "get"
|
|
|
| def test_wrap_connection_exception(self):
|
| """Connection exceptions should be wrapped as StorageConnectionError."""
|
|
|
| class ConnectionRefusedError(Exception):
|
| pass
|
| exc = ConnectionRefusedError("Connection refused")
|
| wrapped = wrap_storage_exception("qdrant", "search", exc)
|
| assert isinstance(wrapped, StorageConnectionError)
|
| assert wrapped.backend == "qdrant"
|
|
|
| def test_wrap_generic_exception(self):
|
| """Generic exceptions should be wrapped as StorageError."""
|
| exc = Exception("Unknown error")
|
| wrapped = wrap_storage_exception("redis", "set", exc)
|
| assert isinstance(wrapped, StorageError)
|
| assert "redis" in str(wrapped)
|
| assert "set" in str(wrapped)
|
|
|
|
|
| class TestDebugMode:
|
| """Test debug mode detection."""
|
|
|
| def test_debug_mode_off_by_default(self):
|
| """Debug mode should be off by default."""
|
|
|
| old_val = os.environ.get("MNEMO_DEBUG")
|
| if "MNEMO_DEBUG" in os.environ:
|
| del os.environ["MNEMO_DEBUG"]
|
|
|
| try:
|
| assert is_debug_mode() is False
|
| finally:
|
| if old_val:
|
| os.environ["MNEMO_DEBUG"] = old_val
|
|
|
| def test_debug_mode_on_with_true(self):
|
| """Debug mode should be on when set to 'true'."""
|
| old_val = os.environ.get("MNEMO_DEBUG")
|
| os.environ["MNEMO_DEBUG"] = "true"
|
|
|
| try:
|
| assert is_debug_mode() is True
|
| finally:
|
| if old_val:
|
| os.environ["MNEMO_DEBUG"] = old_val
|
| else:
|
| del os.environ["MNEMO_DEBUG"]
|
|
|
| def test_debug_mode_on_with_1(self):
|
| """Debug mode should be on when set to '1'."""
|
| old_val = os.environ.get("MNEMO_DEBUG")
|
| os.environ["MNEMO_DEBUG"] = "1"
|
|
|
| try:
|
| assert is_debug_mode() is True
|
| finally:
|
| if old_val:
|
| os.environ["MNEMO_DEBUG"] = old_val
|
| else:
|
| del os.environ["MNEMO_DEBUG"]
|
|
|
|
|
| class TestErrorCategories:
|
| """Test error category classification."""
|
|
|
| def test_storage_error_category(self):
|
| """Storage errors should have STORAGE category."""
|
| exc = StorageError("test")
|
| assert exc.category == ErrorCategory.STORAGE
|
|
|
| def test_vector_error_category(self):
|
| """Vector errors should have VECTOR category."""
|
| exc = VectorError("test")
|
| assert exc.category == ErrorCategory.VECTOR
|
|
|
| def test_config_error_category(self):
|
| """Config errors should have CONFIG category."""
|
| exc = ConfigurationError("key", "reason")
|
| assert exc.category == ErrorCategory.CONFIG
|
|
|
| def test_validation_error_category(self):
|
| """Validation errors should have VALIDATION category."""
|
| exc = ValidationError("field", "reason")
|
| assert exc.category == ErrorCategory.VALIDATION
|
|
|
| def test_memory_error_category(self):
|
| """Memory errors should have MEMORY category."""
|
| exc = MemoryOperationError("store", "mem_1", "failed")
|
| assert exc.category == ErrorCategory.MEMORY
|
|
|
| def test_agent_error_category(self):
|
| """Agent errors should have AGENT category."""
|
| exc = AgentNotFoundError("agent_1")
|
| assert exc.category == ErrorCategory.AGENT
|
|
|
| def test_provider_error_category(self):
|
| """Provider errors should have PROVIDER category."""
|
| exc = UnsupportedProviderError("unknown")
|
| assert exc.category == ErrorCategory.PROVIDER
|
|
|
|
|
| class TestMetadataValidationErrors:
|
| """Test specialized validation errors."""
|
|
|
| def test_metadata_validation_error(self):
|
| """Test metadata validation error."""
|
| exc = MetadataValidationError("metadata", "Too many keys")
|
| assert exc.error_code == "METADATA_VALIDATION_ERROR"
|
| assert exc.recoverable is False
|
|
|
| def test_attribute_validation_error(self):
|
| """Test attribute validation error."""
|
| exc = AttributeValidationError("attributes", "Key too long")
|
| assert exc.error_code == "ATTRIBUTE_VALIDATION_ERROR"
|
| assert exc.recoverable is False
|
|
|
|
|
| class TestUnsupportedTransportError:
|
| """Test unsupported transport error."""
|
|
|
| def test_unsupported_transport_error(self):
|
| """Test unsupported transport error."""
|
| exc = UnsupportedTransportError(
|
| transport="websocket",
|
| supported_transports=["stdio", "sse"]
|
| )
|
| assert exc.recoverable is False
|
| assert exc.error_code == "UNSUPPORTED_TRANSPORT_ERROR"
|
| assert exc.transport == "websocket"
|
| assert "stdio" in str(exc)
|
| assert "sse" in str(exc)
|
|
|
|
|
| class TestDependencyMissingError:
|
| """Test dependency missing error."""
|
|
|
| def test_dependency_missing_error(self):
|
| """Test dependency missing error."""
|
| exc = DependencyMissingError(
|
| dependency="mcp",
|
| message="Install with: pip install mcp"
|
| )
|
| assert exc.recoverable is False
|
| assert exc.error_code == "DEPENDENCY_MISSING_ERROR"
|
| assert exc.dependency == "mcp"
|
| assert "pip install mcp" in str(exc)
|
|
|
|
|
| class TestErrorContext:
|
| """Test error context handling."""
|
|
|
| def test_context_preserved_in_subclass(self):
|
| """Context should be preserved in subclasses."""
|
| exc = StorageConnectionError(
|
| backend="redis",
|
| message="Connection failed",
|
| context={"retry_count": 3, "last_error": "ECONNREFUSED"}
|
| )
|
| assert exc.context["retry_count"] == 3
|
| assert exc.context["last_error"] == "ECONNREFUSED"
|
| assert exc.context["backend"] == "redis"
|
|
|
| def test_value_truncation_in_validation_error(self):
|
| """Large values should be truncated in validation error context."""
|
| large_value = "x" * 200
|
| exc = ValidationError("field", "too long", value=large_value)
|
| assert len(exc.context["value"]) == 103
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| pytest.main([__file__, "-v"])
|
|
|