"""OpenAICompatibleProvider : requete HTTP construite correctement, et gestion des erreurs (401, 429, timeout, reseau, reponse invalide) - urllib.request.urlopen est entierement mocke, aucun appel reseau reel.""" import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) import io import json import urllib.error from unittest.mock import MagicMock, patch from app.ai.provider import AIError, AIProvider, OpenAICompatibleProvider def fake_response(payload: dict): m = MagicMock() m.read.return_value = json.dumps(payload).encode("utf-8") m.__enter__.return_value = m m.__exit__.return_value = False return m def make_http_error(code: int, body: bytes = b"") -> urllib.error.HTTPError: return urllib.error.HTTPError(url="http://x/chat/completions", code=code, msg="err", hdrs=None, fp=io.BytesIO(body)) provider = OpenAICompatibleProvider( base_url="https://api.example.com/v1", api_key="sk-test", model="gpt-test", timeout=5, temperature=0.2, ) print("=== 1. Requete construite correctement (URL, methode, en-tetes, corps) ===") with patch("app.ai.provider.urllib.request.urlopen") as mocked: mocked.return_value = fake_response({"choices": [{"message": {"content": "bonjour"}}]}) result = provider.chat([{"role": "user", "content": "salut"}]) request = mocked.call_args.args[0] print(" URL:", request.full_url) print(" methode:", request.get_method()) print(" Authorization:", request.get_header("Authorization")) body = json.loads(request.data.decode("utf-8")) print(" corps:", body) assert request.full_url == "https://api.example.com/v1/chat/completions" assert request.get_method() == "POST" assert request.get_header("Authorization") == "Bearer sk-test" assert body["model"] == "gpt-test" assert body["temperature"] == 0.2 assert body["messages"] == [{"role": "user", "content": "salut"}] assert result == "bonjour" print(" OK") print() print("=== 2. Erreur 401 -> message clair mentionnant la cle API ===") with patch("app.ai.provider.urllib.request.urlopen", side_effect=make_http_error(401)): try: provider.chat([{"role": "user", "content": "x"}]) ok = False except AIError as exc: ok = True print(" message:", exc) assert "401" in str(exc) and "cle" in str(exc).lower() assert ok print(" OK") print() print("=== 3. Erreur 429 -> message clair mentionnant la limitation ===") with patch("app.ai.provider.urllib.request.urlopen", side_effect=make_http_error(429)): try: provider.chat([{"role": "user", "content": "x"}]) ok = False except AIError as exc: ok = True print(" message:", exc) assert "429" in str(exc) assert ok print(" OK") print() print("=== 4. Timeout -> message clair, pas une exception brute ===") timeout_error = urllib.error.URLError(reason=TimeoutError("timed out")) with patch("app.ai.provider.urllib.request.urlopen", side_effect=timeout_error): try: provider.chat([{"role": "user", "content": "x"}]) ok = False except AIError as exc: ok = True print(" message:", exc) assert "delai" in str(exc).lower() or "temps" in str(exc).lower() assert ok print(" OK") print() print("=== 5. Erreur reseau (DNS, connexion refusee) -> message clair ===") network_error = urllib.error.URLError(reason="nom ou service inconnu") with patch("app.ai.provider.urllib.request.urlopen", side_effect=network_error): try: provider.chat([{"role": "user", "content": "x"}]) ok = False except AIError as exc: ok = True print(" message:", exc) assert ok print(" OK") print() print("=== 6. Reponse invalide (JSON incoherent) -> message clair, pas de KeyError brut ===") with patch("app.ai.provider.urllib.request.urlopen") as mocked: mocked.return_value = fake_response({"unexpected": "shape"}) try: provider.chat([{"role": "user", "content": "x"}]) ok = False except AIError as exc: ok = True print(" message:", exc) assert ok print(" OK") print() print("=== 7. Sans cle API : pas d'en-tete Authorization ===") no_key_provider = OpenAICompatibleProvider(base_url="http://localhost:8080/v1", api_key="", model="local-model") with patch("app.ai.provider.urllib.request.urlopen") as mocked: mocked.return_value = fake_response({"choices": [{"message": {"content": "ok"}}]}) no_key_provider.chat([{"role": "user", "content": "x"}]) request2 = mocked.call_args.args[0] assert request2.get_header("Authorization") is None print(" OK (utile pour un LLM local sans authentification)") print() print("=== 8. Les 4 usages metier construisent les bons messages (via chat()) ===") captured = [] class SpyProvider(AIProvider): def chat(self, messages, *, temperature=None): captured.append(messages) return "reponse" spy = SpyProvider() spy.explain_output("erreur X") spy.diagnose("erreur Y") spy.propose_solution("erreur Z") spy.generate_command("trouver les gros fichiers") spy.ask("pourquoi ?", context="ligne de contexte") print(" nombre d'appels a chat():", len(captured)) assert len(captured) == 5 assert "erreur X" in captured[0][-1]["content"] assert "Cause probable" in captured[1][0]["content"] # structure imposee au system prompt de diagnose assert "erreur Z" in captured[2][-1]["content"] assert captured[3][-1]["content"] == "trouver les gros fichiers" assert "ligne de contexte" in captured[4][1]["content"] assert captured[4][-1]["content"] == "pourquoi ?" print(" OK") print() print("AI PROVIDER : TOUS LES TESTS OK")