""" Tests for the MinimaxTranslationProvider. Validates Bug 1 fix: - default base_url is the public ``api.minimax.io`` host (NOT ``api.minimax.chat``) - default model is ``MiniMax-M3`` - ``is_available()`` / ``health_check()`` tolerate a missing ``/models`` path (Minimax does not document it) and only mark the provider down on 401/network error - translation success, 429 retry, 401 handling """ import pytest from unittest.mock import patch, MagicMock from requests.exceptions import Timeout from services.providers.minimax_provider import ( MinimaxTranslationProvider, MinimaxProviderError, MINIMAX_RATE_LIMITED, MINIMAX_INVALID_KEY, MINIMAX_TIMEOUT, MINIMAX_SERVICE_ERROR, ) from services.providers.schemas import TranslationRequest class TestMinimaxProviderConfig: """Defaults must point at the real public endpoint.""" def test_default_base_url_is_public_host(self): provider = MinimaxTranslationProvider(api_key="k", max_retries=0) assert provider._base_url == "https://api.minimax.io/v1" # Regression guard: the old broken host must never come back. assert "minimax.chat" not in provider._base_url def test_default_model_is_m3(self): provider = MinimaxTranslationProvider(api_key="k", max_retries=0) assert provider._model == "MiniMax-M3" def test_custom_base_url_respected(self): provider = MinimaxTranslationProvider( api_key="k", base_url="https://proxy.example.com/v1", max_retries=0 ) assert provider._base_url == "https://proxy.example.com/v1" def test_get_name(self): provider = MinimaxTranslationProvider(api_key="k", max_retries=0) assert provider.get_name() == "minimax" class TestMinimaxAvailabilityProbe: """is_available/health_check must not fail just because /models 404s.""" @pytest.fixture def provider(self): return MinimaxTranslationProvider(api_key="k", max_retries=0) def _mock_get(self, status_code: int): mock_response = MagicMock() mock_response.status_code = status_code return mock_response @patch("requests.get") def test_available_when_models_returns_200(self, mock_get, provider): mock_get.return_value = self._mock_get(200) assert provider.is_available() is True @patch("requests.get") def test_available_when_models_404(self, mock_get, provider): # Minimax does not document /models; a 404 must NOT mark it unavailable. mock_get.return_value = self._mock_get(404) assert provider.is_available() is True @patch("requests.get") def test_unavailable_on_401(self, mock_get, provider): mock_get.return_value = self._mock_get(401) assert provider.is_available() is False @patch("requests.get") def test_unavailable_on_network_error(self, _mock_get, provider): def _raise(*a, **kw): raise Timeout("boom") with patch("requests.get", side_effect=_raise): assert provider.is_available() is False @patch("requests.get") def test_health_check_tolerates_404(self, mock_get, provider): mock_get.return_value = self._mock_get(404) status = provider.health_check() assert status.available is True assert status.name == "minimax" @patch("requests.get") def test_health_check_marks_down_on_401(self, mock_get, provider): mock_get.return_value = self._mock_get(401) status = provider.health_check() assert status.available is False class TestMinimaxTranslateText: @pytest.fixture def provider(self): return MinimaxTranslationProvider(api_key="k", model="MiniMax-M3", max_retries=0) def _mock_post(self, payload, status_code=200): mock_response = MagicMock() mock_response.status_code = status_code mock_response.json.return_value = payload mock_response.text = "" return mock_response @patch("requests.post") def test_success(self, mock_post, provider): mock_post.return_value = self._mock_post( {"choices": [{"message": {"content": "Bonjour"}}], "usage": {}} ) resp = provider.translate_text(TranslationRequest(text="Hello", target_language="fr")) assert resp.translated_text == "Bonjour" assert resp.provider_name == "minimax" # Verify we hit the public host on the OpenAI-compatible path. called_url = mock_post.call_args[0][0] assert called_url == "https://api.minimax.io/v1/chat/completions" def test_empty_text_short_circuits(self, provider): resp = provider.translate_text(TranslationRequest(text="", target_language="fr")) assert resp.translated_text == "" @patch("requests.post") def test_invalid_key_returns_error(self, mock_post, provider): mock_post.return_value = self._mock_post({"error": "bad key"}, status_code=401) resp = provider.translate_text(TranslationRequest(text="Hello", target_language="fr")) assert resp.error_code == MINIMAX_INVALID_KEY # Original text returned on failure. assert resp.translated_text == "Hello" @patch("time.sleep") @patch("requests.post") def test_rate_limit_then_success(self, mock_post, mock_sleep): provider = MinimaxTranslationProvider(api_key="k", max_retries=2, retry_delay=0.01) mock_post.side_effect = [ self._mock_post({"error": "slow down"}, status_code=429), self._mock_post({"choices": [{"message": {"content": "Hola"}}], "usage": {}}), ] resp = provider.translate_text(TranslationRequest(text="Hello", target_language="es")) assert resp.translated_text == "Hola" assert mock_sleep.called # backoff happened @patch("requests.post") def test_service_error_when_empty_choices(self, mock_post, provider): mock_post.return_value = self._mock_post({"choices": []}) resp = provider.translate_text(TranslationRequest(text="Hello", target_language="fr")) assert resp.error_code == MINIMAX_SERVICE_ERROR class TestMinimaxProviderError: def test_error_carries_code_and_message(self): err = MinimaxProviderError(MINIMAX_TIMEOUT, "timed out", details={"wait": 1}) assert err.code == MINIMAX_TIMEOUT assert err.message == "timed out" assert err.details == {"wait": 1}