| """Tests for providers/nvidia_nim/errors.py error mapping.""" |
|
|
| from unittest.mock import MagicMock, patch |
|
|
| import openai |
| import pytest |
| from httpx import ReadTimeout, Request, Response |
|
|
| from providers.common import append_request_id, get_user_facing_error_message, map_error |
| from providers.exceptions import ( |
| APIError, |
| AuthenticationError, |
| InvalidRequestError, |
| OverloadedError, |
| RateLimitError, |
| ) |
|
|
|
|
| def _make_openai_error(cls, message="test error", status_code=None): |
| """Helper to create openai exceptions with required httpx objects.""" |
| response = Response( |
| status_code=status_code or 500, request=Request("POST", "http://test") |
| ) |
| body = {"error": {"message": message}} |
| |
| if cls is openai.APIError: |
| return cls(message, request=Request("POST", "http://test"), body=body) |
| return cls(message, response=response, body=body) |
|
|
|
|
| class TestMapError: |
| """Tests for map_error function.""" |
|
|
| def test_authentication_error(self): |
| """openai.AuthenticationError -> AuthenticationError.""" |
| exc = _make_openai_error(openai.AuthenticationError, status_code=401) |
| result = map_error(exc) |
| assert isinstance(result, AuthenticationError) |
| assert result.status_code == 401 |
|
|
| def test_rate_limit_error(self): |
| """openai.RateLimitError -> RateLimitError and triggers global block.""" |
| exc = _make_openai_error(openai.RateLimitError, status_code=429) |
| with patch("providers.common.error_mapping.GlobalRateLimiter") as mock_rl: |
| mock_instance = MagicMock() |
| mock_rl.get_instance.return_value = mock_instance |
| result = map_error(exc) |
| assert isinstance(result, RateLimitError) |
| assert result.status_code == 429 |
| mock_instance.set_blocked.assert_called_once_with(60) |
|
|
| def test_bad_request_error(self): |
| """openai.BadRequestError -> InvalidRequestError.""" |
| exc = _make_openai_error(openai.BadRequestError, status_code=400) |
| result = map_error(exc) |
| assert isinstance(result, InvalidRequestError) |
| assert result.status_code == 400 |
|
|
| @pytest.mark.parametrize( |
| "message", |
| ["Server overloaded", "No capacity available"], |
| ids=["overloaded", "capacity"], |
| ) |
| def test_internal_server_error_overloaded(self, message): |
| """InternalServerError with overloaded/capacity keywords -> OverloadedError.""" |
| exc = _make_openai_error( |
| openai.InternalServerError, message=message, status_code=500 |
| ) |
| result = map_error(exc) |
| assert isinstance(result, OverloadedError) |
| assert result.status_code == 529 |
|
|
| def test_internal_server_error_generic(self): |
| """InternalServerError without keywords -> APIError(500).""" |
| exc = _make_openai_error( |
| openai.InternalServerError, message="Unknown error", status_code=500 |
| ) |
| result = map_error(exc) |
| assert isinstance(result, APIError) |
| assert result.status_code == 500 |
|
|
| def test_generic_api_error(self): |
| """openai.APIError -> APIError with original status_code.""" |
| exc = _make_openai_error( |
| openai.APIError, message="Bad gateway", status_code=502 |
| ) |
| result = map_error(exc) |
| assert isinstance(result, APIError) |
|
|
| def test_unmapped_exception_passthrough(self): |
| """Non-openai exceptions are returned as-is.""" |
| exc = RuntimeError("unexpected") |
| result = map_error(exc) |
| assert result is exc |
| assert isinstance(result, RuntimeError) |
|
|
| def test_value_error_passthrough(self): |
| """ValueError passes through unchanged.""" |
| exc = ValueError("bad value") |
| result = map_error(exc) |
| assert result is exc |
|
|
| @pytest.mark.parametrize( |
| "exc_cls,expected_cls", |
| [ |
| (openai.AuthenticationError, AuthenticationError), |
| (openai.RateLimitError, RateLimitError), |
| (openai.BadRequestError, InvalidRequestError), |
| ], |
| ids=["auth", "rate_limit", "bad_request"], |
| ) |
| def test_mapping_parametrized(self, exc_cls, expected_cls): |
| """Parametrized check of openai -> provider error mapping.""" |
| status_map = { |
| openai.AuthenticationError: 401, |
| openai.RateLimitError: 429, |
| openai.BadRequestError: 400, |
| } |
| exc = _make_openai_error(exc_cls, status_code=status_map[exc_cls]) |
| with patch("providers.common.error_mapping.GlobalRateLimiter"): |
| result = map_error(exc) |
| assert isinstance(result, expected_cls) |
|
|
|
|
| def test_user_facing_message_read_timeout_empty_string(): |
| """ReadTimeout wrapping TimeoutError should still produce readable text.""" |
| timeout_exc = ReadTimeout("") |
| message = get_user_facing_error_message(timeout_exc, read_timeout_s=60) |
| assert message == "Provider request timed out after 60s." |
|
|
|
|
| def test_append_request_id_suffix(): |
| """Request id suffix should be appended deterministically.""" |
| message = append_request_id("Provider request failed.", "req_abc123") |
| assert message == "Provider request failed. (request_id=req_abc123)" |
|
|