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