""" Tests for Track C1.1 — NewProviderAsLegacyAdapter and LegacyProviderAsNewAdapter. Covers: - NewProviderAsLegacyAdapter: * translate() delegates to translate_text() * translate_batch() delegates to translate_batch() * get_name() and is_available() pass through * errors propagate as exceptions (legacy code expects this) - LegacyProviderAsNewAdapter: * translate_text() wraps legacy.translate() * translate_batch() groups by language pair * get_name() and is_available() pass through - coerce_to_legacy / coerce_to_new: * no-op when already in target shape * wraps when in the other shape """ import pytest from typing import List from unittest.mock import MagicMock from services.providers.base import TranslationProvider as NewTranslationProvider from services.providers.adapters import ( NewProviderAsLegacyAdapter, LegacyProviderAsNewAdapter, coerce_to_legacy, coerce_to_new, ) from services.providers.schemas import ( TranslationRequest, TranslationResponse, ) # ============================================================================ # Test helpers — new-style mock # ============================================================================ class MockNewProvider(NewTranslationProvider): """Minimal new-style provider for testing the adapter.""" def __init__(self, translations: dict = None, name: str = "mock-new"): self._t = translations or {} self._name = name self.translate_text_calls = [] self.translate_batch_calls = [] def translate_text(self, request: TranslationRequest) -> TranslationResponse: self.translate_text_calls.append(request) translated = self._t.get(request.text, f"NEW_{request.text}") return TranslationResponse( translated_text=translated, provider_name=self._name, source_language=request.source_language or "auto", target_language=request.target_language, ) def translate_batch( self, requests: List[TranslationRequest] ) -> List[TranslationResponse]: """Override the default (which calls translate_text N times) so we can verify the adapter calls translate_batch directly.""" self.translate_batch_calls.append(requests) return [self.translate_text(req) for req in requests] def get_name(self) -> str: return self._name def is_available(self) -> bool: return True # ============================================================================ # Test helpers — legacy mock # ============================================================================ class MockLegacyProvider: """Minimal legacy provider for testing the adapter.""" def __init__(self, translations: dict = None, name: str = "mock-legacy"): self._t = translations or {} self._name = name self.calls = [] def translate(self, text, target_language, source_language="auto"): self.calls.append((text, target_language, source_language)) return self._t.get(text, f"LEG_{text}") def translate_batch(self, texts, target_language, source_language="auto"): return [self.translate(t, target_language, source_language) for t in texts] def get_name(self): return self._name def is_available(self): return True # ============================================================================ # NewProviderAsLegacyAdapter # ============================================================================ class TestNewProviderAsLegacyAdapter: def test_translate_delegates(self): new = MockNewProvider({"Hello": "Bonjour"}) legacy = NewProviderAsLegacyAdapter(new) result = legacy.translate("Hello", "fr", "en") assert result == "Bonjour" assert len(new.translate_text_calls) == 1 assert new.translate_text_calls[0].text == "Hello" assert new.translate_text_calls[0].target_language == "fr" def test_translate_default_source(self): new = MockNewProvider() legacy = NewProviderAsLegacyAdapter(new) legacy.translate("Hello", "fr") assert new.translate_text_calls[0].source_language == "auto" def test_translate_batch_delegates(self): new = MockNewProvider({"A": "TR_A", "B": "TR_B"}) legacy = NewProviderAsLegacyAdapter(new) result = legacy.translate_batch(["A", "B"], "fr") assert result == ["TR_A", "TR_B"] assert len(new.translate_batch_calls) == 1 def test_translate_batch_empty(self): new = MockNewProvider() legacy = NewProviderAsLegacyAdapter(new) result = legacy.translate_batch([], "fr") assert result == [] def test_get_name_passthrough(self): new = MockNewProvider(name="my-new") legacy = NewProviderAsLegacyAdapter(new) assert legacy.get_name() == "my-new" def test_is_available_passthrough(self): new = MockNewProvider() legacy = NewProviderAsLegacyAdapter(new) assert legacy.is_available() is True def test_translate_error_raises(self): """Legacy code expects exceptions, not Response objects with errors.""" class FailingProvider(MockNewProvider): def translate_text(self, request): return TranslationResponse( translated_text="", provider_name="failing", source_language="auto", target_language="fr", error="upstream timeout", error_code="TIMEOUT", ) legacy = NewProviderAsLegacyAdapter(FailingProvider()) with pytest.raises(Exception, match="TIMEOUT"): legacy.translate("Hello", "fr") def test_health_check(self): new = MockNewProvider() legacy = NewProviderAsLegacyAdapter(new) status = legacy.health_check() assert status["name"] == "mock-new" assert status["available"] is True # ============================================================================ # LegacyProviderAsNewAdapter # ============================================================================ class TestLegacyProviderAsNewAdapter: def test_translate_text_wraps(self): legacy = MockLegacyProvider({"Hello": "Bonjour"}) adapter = LegacyProviderAsNewAdapter(legacy) result = adapter.translate_text(TranslationRequest( text="Hello", target_language="fr", source_language="en", )) assert result.translated_text == "Bonjour" assert result.provider_name == "mock-legacy" assert not result.error assert len(legacy.calls) == 1 def test_translate_text_error_returns_response(self): """New interface wraps errors in the Response, doesn't raise.""" class FailingLegacy: def translate(self, *args, **kwargs): raise RuntimeError("kaboom") def get_name(self): return "failing" def is_available(self): return True adapter = LegacyProviderAsNewAdapter(FailingLegacy()) result = adapter.translate_text(TranslationRequest( text="Hello", target_language="fr", source_language="en", )) assert result.translated_text == "" assert "kaboom" in result.error assert result.error_code == "LEGACY_PROVIDER_ERROR" def test_translate_batch_single_group(self): legacy = MockLegacyProvider({"A": "TR_A", "B": "TR_B"}) adapter = LegacyProviderAsNewAdapter(legacy) results = adapter.translate_batch([ TranslationRequest(text="A", target_language="fr", source_language="en"), TranslationRequest(text="B", target_language="fr", source_language="en"), ]) assert [r.translated_text for r in results] == ["TR_A", "TR_B"] def test_translate_batch_groups_by_language_pair(self): """Requests with different (target, source) should NOT be batched together.""" legacy = MockLegacyProvider() adapter = LegacyProviderAsNewAdapter(legacy) adapter.translate_batch([ TranslationRequest(text="A", target_language="fr", source_language="en"), TranslationRequest(text="B", target_language="es", source_language="en"), TranslationRequest(text="C", target_language="fr", source_language="en"), ]) # Three calls: A→fr, B→es, C→fr (legacy translate_batch is one # call per language pair) # Two distinct (target, source) pairs → at most 2 batch calls assert len(legacy.calls) >= 2 def test_translate_batch_empty(self): legacy = MockLegacyProvider() adapter = LegacyProviderAsNewAdapter(legacy) results = adapter.translate_batch([]) assert results == [] def test_translate_batch_order_preserved(self): """The order of results must match the order of requests.""" legacy = MockLegacyProvider({"X": "TR_X", "Y": "TR_Y", "Z": "TR_Z"}) adapter = LegacyProviderAsNewAdapter(legacy) results = adapter.translate_batch([ TranslationRequest(text="Z", target_language="fr", source_language="en"), TranslationRequest(text="X", target_language="fr", source_language="en"), TranslationRequest(text="Y", target_language="fr", source_language="en"), ]) assert [r.translated_text for r in results] == ["TR_Z", "TR_X", "TR_Y"] def test_get_name_passthrough(self): legacy = MockLegacyProvider(name="my-legacy") adapter = LegacyProviderAsNewAdapter(legacy) assert adapter.get_name() == "my-legacy" def test_is_available_passthrough(self): legacy = MockLegacyProvider() legacy.is_available = lambda: False adapter = LegacyProviderAsNewAdapter(legacy) assert adapter.is_available() is False def test_custom_provider_name_overrides(self): legacy = MockLegacyProvider(name="original-name") adapter = LegacyProviderAsNewAdapter(legacy, provider_name="override-name") assert adapter.get_name() == "override-name" # ============================================================================ # Coercion helpers # ============================================================================ class TestCoercion: def test_new_already_legacy_noop(self): new = MockNewProvider() adapter = NewProviderAsLegacyAdapter(new) result = coerce_to_legacy(adapter) # Should return the existing adapter, not wrap it again assert result is adapter def test_new_provider_to_legacy_wraps(self): new = MockNewProvider() result = coerce_to_legacy(new) assert isinstance(result, NewProviderAsLegacyAdapter) assert result._new is new def test_legacy_provider_unchanged(self): legacy = MockLegacyProvider() result = coerce_to_legacy(legacy) assert result is legacy def test_legacy_already_new_noop(self): legacy = MockLegacyProvider() adapter = LegacyProviderAsNewAdapter(legacy) result = coerce_to_new(adapter) assert result is adapter def test_legacy_provider_to_new_wraps(self): legacy = MockLegacyProvider() result = coerce_to_new(legacy) assert isinstance(result, NewTranslationProvider) assert isinstance(result, LegacyProviderAsNewAdapter) def test_new_provider_unchanged(self): new = MockNewProvider() result = coerce_to_new(new) assert result is new # ============================================================================ # Round-trip # ============================================================================ class TestRoundTrip: """An adapter should be reversible: a string that comes out of one side should match what the other side would have returned.""" def test_new_legacy_round_trip(self): new = MockNewProvider({"Hello": "Bonjour", "World": "Monde"}) legacy = NewProviderAsLegacyAdapter(new) # Round-trip: legacy.translate → new.translate_text via_legacy = legacy.translate("Hello", "fr", "en") via_new = new.translate_text(TranslationRequest( text="Hello", target_language="fr", source_language="en", )).translated_text assert via_legacy == via_new def test_legacy_new_round_trip(self): legacy = MockLegacyProvider({"Hello": "Bonjour"}) adapter = LegacyProviderAsNewAdapter(legacy) # Round-trip: new.translate_text → legacy.translate via_new = adapter.translate_text(TranslationRequest( text="Hello", target_language="fr", source_language="en", )).translated_text via_legacy = legacy.translate("Hello", "fr", "en") assert via_new == via_legacy