feat(translation): quality pipeline overhaul + new features (audit 2026-08-29)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s
Translation quality & format preservation: - Word: merge adjacent same-format runs into one unit (sentence-level coherence like inline-tag handling); translate comments/balloons; dedupe textbox collection (was translated twice); RTL no longer overrides center/justify alignment; CJK/Arabic font hints (eastAsia/cs) - PPTX: chart translations now actually reach the output file (ChartPart.blob is read-only — rewrite chart XML in the saved ZIP); CJK typeface hints (a:ea) - Excel: sheet renames no longer break references — rewrite cell formulas (3D/quoted), defined names, data validations, cond. formats - PDF: bold/italic honored (hebo/heit/hebi); table cells never merge; unchanged blocks left untouched (typography preserved, fixes duplicate hyperlinks); attempted/changed stats + route gate now cover PDF; CJK font paths; scanned PDFs via Mistral OCR (detection + admin settings) Features: - formality param (formal/informal) + automatic regional-variant prompts - output_mode=bilingual docx (source above translation) - per-user translation memory on Redis (falls back to LRU), context-hashed - QA report + 0-100 confidence score in job status; L0 on by default - OpenAI-compatible providers: whole chunk in ONE numbered-JSON request (~15x fewer calls) with per-item fallback; base prompt always present (custom prompt no longer replaces translation instructions) Infra & marketing alignment: - plan-based engine gating + vision gating (closes paid-engine leak); /providers/available filtered per plan; 107 languages exposed - zh-CN/zh-TW validation fixed; libmagic disabled on Windows (native crash) - admin: Mistral OCR settings + engine status dashboard; httpx<0.28 pin (TestClient breakage); Prometheus test fixture fixed - marketing docs aligned with code (PDF+OCR, retention, engines, pricing) - security: .env.ionos/.env.production/provider_settings.json removed Tests: 1173 passed / 0 failed (6 network tests deselected: free Google endpoint temporarily blocked from this machine)
This commit is contained in:
161
tests/test_providers/test_minimax_provider.py
Normal file
161
tests/test_providers/test_minimax_provider.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
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}
|
||||
@@ -97,11 +97,24 @@ class TestHelperFunctions:
|
||||
assert "translator" in prompt.lower()
|
||||
|
||||
def test_build_system_prompt_custom(self):
|
||||
"""Test custom system prompt."""
|
||||
"""A custom prompt AUGMENTS the base translation instructions.
|
||||
|
||||
The base prompt is always present — a glossary-only custom prompt
|
||||
used to produce a system prompt with no translation instruction
|
||||
at all (fixed 2026-08-29).
|
||||
"""
|
||||
custom = "Translate this text formally for business context."
|
||||
prompt = _build_system_prompt("English", "French", custom)
|
||||
|
||||
assert prompt == custom
|
||||
# Base translation instructions survive
|
||||
assert "English" in prompt
|
||||
assert "French" in prompt
|
||||
assert "translator" in prompt.lower()
|
||||
# Custom content is appended, not replacing
|
||||
assert custom in prompt
|
||||
assert "ADDITIONAL CONTEXT AND INSTRUCTIONS" in prompt
|
||||
# The base part comes first
|
||||
assert prompt.index("French") < prompt.index(custom)
|
||||
|
||||
|
||||
class TestOpenAITranslationProvider:
|
||||
|
||||
Reference in New Issue
Block a user