Files
syndarix/mcp-servers/llm-gateway/tests/test_exceptions.py
Felipe Cardoso f482559e15 fix(llm-gateway): improve type safety and datetime consistency
- Add type annotations for mypy compliance
- Use UTC-aware datetimes consistently (datetime.now(UTC))
- Add type: ignore comments for LiteLLM incomplete stubs
- Fix import ordering and formatting
- Update pyproject.toml mypy configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 20:56:05 +01:00

377 lines
12 KiB
Python

"""
Tests for exceptions module.
"""
from exceptions import (
AllProvidersFailedError,
CircuitOpenError,
ConfigurationError,
ContextTooLongError,
CostLimitExceededError,
ErrorCode,
InvalidModelError,
InvalidModelGroupError,
LLMGatewayError,
ModelNotAvailableError,
ProviderError,
RateLimitError,
StreamError,
TokenLimitExceededError,
)
class TestErrorCode:
"""Tests for ErrorCode enum."""
def test_error_code_values(self) -> None:
"""Test error code values."""
assert ErrorCode.UNKNOWN_ERROR.value == "LLM_UNKNOWN_ERROR"
assert ErrorCode.PROVIDER_ERROR.value == "LLM_PROVIDER_ERROR"
assert ErrorCode.CIRCUIT_OPEN.value == "LLM_CIRCUIT_OPEN"
assert ErrorCode.COST_LIMIT_EXCEEDED.value == "LLM_COST_LIMIT_EXCEEDED"
class TestLLMGatewayError:
"""Tests for LLMGatewayError base class."""
def test_basic_error(self) -> None:
"""Test basic error creation."""
error = LLMGatewayError("Something went wrong")
assert str(error) == "[LLM_UNKNOWN_ERROR] Something went wrong"
assert error.message == "Something went wrong"
assert error.code == ErrorCode.UNKNOWN_ERROR
assert error.details == {}
assert error.cause is None
def test_error_with_code(self) -> None:
"""Test error with custom code."""
error = LLMGatewayError(
"Provider failed",
code=ErrorCode.PROVIDER_ERROR,
)
assert error.code == ErrorCode.PROVIDER_ERROR
def test_error_with_details(self) -> None:
"""Test error with details."""
error = LLMGatewayError(
"Error",
details={"key": "value"},
)
assert error.details == {"key": "value"}
def test_error_with_cause(self) -> None:
"""Test error with cause exception."""
cause = ValueError("Original error")
error = LLMGatewayError("Wrapped error", cause=cause)
assert error.cause is cause
def test_to_dict(self) -> None:
"""Test converting error to dict."""
error = LLMGatewayError(
"Test error",
code=ErrorCode.INVALID_REQUEST,
details={"field": "value"},
)
result = error.to_dict()
assert result["error"] == "LLM_INVALID_REQUEST"
assert result["message"] == "Test error"
assert result["details"] == {"field": "value"}
def test_to_dict_no_details(self) -> None:
"""Test to_dict without details."""
error = LLMGatewayError("Test error")
result = error.to_dict()
assert "details" not in result
def test_repr(self) -> None:
"""Test error repr."""
error = LLMGatewayError("Test", details={"key": "val"})
repr_str = repr(error)
assert "LLMGatewayError" in repr_str
assert "Test" in repr_str
class TestProviderError:
"""Tests for ProviderError."""
def test_basic_provider_error(self) -> None:
"""Test basic provider error."""
error = ProviderError(
message="API call failed",
provider="anthropic",
)
assert error.provider == "anthropic"
assert error.model is None
assert error.status_code is None
assert error.code == ErrorCode.PROVIDER_ERROR
assert "provider" in error.details
def test_provider_error_with_model(self) -> None:
"""Test provider error with model info."""
error = ProviderError(
message="Model not found",
provider="openai",
model="gpt-5",
status_code=404,
)
assert error.model == "gpt-5"
assert error.status_code == 404
assert error.details["model"] == "gpt-5"
assert error.details["status_code"] == 404
def test_provider_error_with_cause(self) -> None:
"""Test provider error with cause."""
cause = ConnectionError("Network down")
error = ProviderError(
message="Connection failed",
provider="google",
cause=cause,
)
assert error.cause is cause
class TestRateLimitError:
"""Tests for RateLimitError."""
def test_internal_rate_limit(self) -> None:
"""Test internal rate limit error."""
error = RateLimitError(
message="Too many requests",
retry_after=60,
)
assert error.code == ErrorCode.RATE_LIMIT_EXCEEDED
assert error.provider is None
assert error.retry_after == 60
assert error.details["retry_after_seconds"] == 60
def test_provider_rate_limit(self) -> None:
"""Test provider rate limit error."""
error = RateLimitError(
message="OpenAI rate limit",
provider="openai",
retry_after=30,
)
assert error.code == ErrorCode.PROVIDER_RATE_LIMIT
assert error.provider == "openai"
assert error.details["provider"] == "openai"
class TestCircuitOpenError:
"""Tests for CircuitOpenError."""
def test_circuit_open_error(self) -> None:
"""Test circuit open error."""
error = CircuitOpenError(
provider="anthropic",
recovery_time=45,
)
assert error.provider == "anthropic"
assert error.recovery_time == 45
assert error.code == ErrorCode.CIRCUIT_OPEN
assert "Circuit breaker open" in error.message
assert error.details["recovery_time_seconds"] == 45
def test_circuit_open_no_recovery_time(self) -> None:
"""Test circuit open without recovery time."""
error = CircuitOpenError(provider="openai")
assert error.recovery_time is None
assert "recovery_time_seconds" not in error.details
class TestCostLimitExceededError:
"""Tests for CostLimitExceededError."""
def test_project_cost_limit(self) -> None:
"""Test project cost limit error."""
error = CostLimitExceededError(
entity_type="project",
entity_id="proj-123",
current_cost=150.0,
limit=100.0,
)
assert error.entity_type == "project"
assert error.entity_id == "proj-123"
assert error.current_cost == 150.0
assert error.limit == 100.0
assert error.code == ErrorCode.COST_LIMIT_EXCEEDED
assert "$150.00" in error.message
assert "$100.00" in error.message
def test_agent_cost_limit(self) -> None:
"""Test agent cost limit error."""
error = CostLimitExceededError(
entity_type="agent",
entity_id="agent-456",
current_cost=50.0,
limit=25.0,
)
assert error.entity_type == "agent"
assert error.details["entity_type"] == "agent"
class TestInvalidModelGroupError:
"""Tests for InvalidModelGroupError."""
def test_invalid_group_error(self) -> None:
"""Test invalid model group error."""
error = InvalidModelGroupError(
model_group="invalid_group",
available_groups=["reasoning", "code", "fast"],
)
assert error.model_group == "invalid_group"
assert error.available_groups == ["reasoning", "code", "fast"]
assert error.code == ErrorCode.INVALID_MODEL_GROUP
assert "invalid_group" in error.message
def test_invalid_group_no_available(self) -> None:
"""Test invalid group without available list."""
error = InvalidModelGroupError(model_group="unknown")
assert error.available_groups is None
assert "available_groups" not in error.details
class TestInvalidModelError:
"""Tests for InvalidModelError."""
def test_invalid_model_error(self) -> None:
"""Test invalid model error."""
error = InvalidModelError(
model="gpt-99",
reason="Model does not exist",
)
assert error.model == "gpt-99"
assert error.code == ErrorCode.INVALID_MODEL
assert "gpt-99" in error.message
assert "Model does not exist" in error.message
def test_invalid_model_no_reason(self) -> None:
"""Test invalid model without reason."""
error = InvalidModelError(model="unknown-model")
assert "reason" not in error.details
class TestModelNotAvailableError:
"""Tests for ModelNotAvailableError."""
def test_model_not_available(self) -> None:
"""Test model not available error."""
error = ModelNotAvailableError(
model="claude-opus-4",
provider="anthropic",
)
assert error.model == "claude-opus-4"
assert error.provider == "anthropic"
assert error.code == ErrorCode.MODEL_NOT_AVAILABLE
assert "not configured" in error.message
class TestAllProvidersFailedError:
"""Tests for AllProvidersFailedError."""
def test_all_providers_failed(self) -> None:
"""Test all providers failed error."""
errors = [
{"model": "claude-opus-4", "error": "Rate limited"},
{"model": "gpt-4.1", "error": "Timeout"},
]
error = AllProvidersFailedError(
model_group="reasoning",
attempted_models=["claude-opus-4", "gpt-4.1"],
errors=errors,
)
assert error.model_group == "reasoning"
assert error.attempted_models == ["claude-opus-4", "gpt-4.1"]
assert error.errors == errors
assert error.code == ErrorCode.ALL_PROVIDERS_FAILED
class TestStreamError:
"""Tests for StreamError."""
def test_stream_error(self) -> None:
"""Test stream error."""
cause = OSError("Connection reset")
error = StreamError(
message="Stream interrupted",
chunks_received=10,
cause=cause,
)
assert error.chunks_received == 10
assert error.cause is cause
assert error.code == ErrorCode.STREAM_ERROR
class TestTokenLimitExceededError:
"""Tests for TokenLimitExceededError."""
def test_token_limit_exceeded(self) -> None:
"""Test token limit exceeded error."""
error = TokenLimitExceededError(
model="claude-haiku",
token_count=10000,
limit=8192,
)
assert error.model == "claude-haiku"
assert error.token_count == 10000
assert error.limit == 8192
assert error.code == ErrorCode.TOKEN_LIMIT_EXCEEDED
class TestContextTooLongError:
"""Tests for ContextTooLongError."""
def test_context_too_long(self) -> None:
"""Test context too long error."""
error = ContextTooLongError(
model="gpt-4.1-mini",
context_length=150000,
max_context=100000,
)
assert error.model == "gpt-4.1-mini"
assert error.context_length == 150000
assert error.max_context == 100000
assert error.code == ErrorCode.CONTEXT_TOO_LONG
class TestConfigurationError:
"""Tests for ConfigurationError."""
def test_configuration_error(self) -> None:
"""Test configuration error."""
error = ConfigurationError(
message="Missing API key",
config_key="ANTHROPIC_API_KEY",
)
assert error.config_key == "ANTHROPIC_API_KEY"
assert error.code == ErrorCode.CONFIGURATION_ERROR
assert error.details["config_key"] == "ANTHROPIC_API_KEY"
def test_configuration_error_no_key(self) -> None:
"""Test configuration error without key."""
error = ConfigurationError(message="Invalid configuration")
assert error.config_key is None
assert "config_key" not in error.details