"""Tests fuer das Retry-Verhalten des OpenRouter-TTS (transiente leere/5xx-Antworten).""" import asyncio import httpx import pytest import app.providers.tts.openrouter as orm from app.providers.tts.openrouter import OpenRouterTTSProvider REQ = httpx.Request("POST", "https://openrouter.ai/api/v1/audio/speech") def _run(coro): return asyncio.run(coro) def _patch(monkeypatch, responses): """httpx-POST liefert nacheinander die vorgegebenen Antworten; kein echtes Sleep.""" it = iter(responses) calls = {"n": 0} async def fake_post(self, url, **kw): calls["n"] += 1 return next(it) async def no_sleep(*a, **k): pass monkeypatch.setattr(httpx.AsyncClient, "post", fake_post) monkeypatch.setattr(orm.asyncio, "sleep", no_sleep) return calls def _provider(): return OpenRouterTTSProvider("key", "some/model", "Zephyr") def test_retries_on_empty_then_succeeds(monkeypatch): calls = _patch(monkeypatch, [ httpx.Response(200, content=b"", request=REQ), # transienter Aussetzer httpx.Response(200, content=b"AUDIO", request=REQ), # dann echtes Audio ]) assert _run(_provider().synthesize("Hallo")) == b"AUDIO" assert calls["n"] == 2 def test_4xx_raises_immediately_without_retry(monkeypatch): calls = _patch(monkeypatch, [ httpx.Response(400, text="invalid voice", request=REQ), ]) with pytest.raises(RuntimeError, match="400"): _run(_provider().synthesize("Hallo")) assert calls["n"] == 1 # kein Retry bei 4xx def test_all_empty_raises_after_attempts(monkeypatch): calls = _patch(monkeypatch, [httpx.Response(200, content=b"", request=REQ)] * orm._MAX_ATTEMPTS) with pytest.raises(RuntimeError, match="empty audio content"): _run(_provider().synthesize("Hallo")) assert calls["n"] == orm._MAX_ATTEMPTS def test_5xx_then_success(monkeypatch): calls = _patch(monkeypatch, [ httpx.Response(503, text="upstream", request=REQ), httpx.Response(200, content=b"OK", request=REQ), ]) assert _run(_provider().synthesize("Hallo")) == b"OK" assert calls["n"] == 2