From 9ea0cddf3c9a061bd0cf520b2d4b5e22cd24678e Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 9 Oct 2026 17:55:44 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20CI=20verte=20=E2=80=94=20fuite=20du=20cl?= =?UTF-8?q?ient=20HTTP=20mock=C3=A9=20+=20rate=20limiter=20actif=20dans=20?= =?UTF-8?q?test=5Fagent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux causes indépendantes faisaient échouer le job test de la CI (et seulement en CI : ni .env ni les mêmes ordres de chargement en local) : - test_agent_web_tools.mock_http patche http_client.shared_client pendant sa fenêtre d'exécution. Le premier import d'importers/url_fetch dans cette fenêtre fige la factory moquée dans le namespace du module → tous les appels fetch_url suivants du processus passaient par le handler mocké de l'autre test (« assert '…/post' == '…/page' » dans test_v56_import). url_fetch résout désormais le client à l'appel, et le mock_http restaure la vraie factory (référence figée au chargement du module) y compris sur url_fetch ; - test_agent.py posait RATE_LIMIT_ENABLED=false en env var, sans effet sur le singleton Settings déjà instancié : sous pytest -n auto, le worker dépassait le quota de 60 req/min et 8 tests recevaient des 429 (KeyError 'id' au passage). Le fixture désactive maintenant le limiter sur le singleton, comme les autres fichiers de tests. --- app/services/importers/url_fetch.py | 7 +++++-- tests/test_agent.py | 6 ++++++ tests/test_agent_web_tools.py | 14 +++++++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/services/importers/url_fetch.py b/app/services/importers/url_fetch.py index 5d4982e..876f4b7 100644 --- a/app/services/importers/url_fetch.py +++ b/app/services/importers/url_fetch.py @@ -11,8 +11,8 @@ from urllib.parse import urlparse import httpx +from app.services import http_client from app.services.export import markdown_to_blocks -from app.services.http_client import shared_client from app.services.importers.base import ImportPage, ImportResult from app.services.importers.html_notes import _html_to_markdown @@ -65,7 +65,10 @@ async def fetch_url_result(url: str, *, transport: httpx.BaseTransport | None = safe_url = _validate_url(url) result = ImportResult(source="url") try: - async with shared_client( + # Résolu à l'APPEL (et non capturé à l'import) : un import tardif de + # ce module pendant qu'un test patche `http_client.shared_client` + # figerait la factory moquée pour tout le reste du processus. + async with http_client.shared_client( timeout=15, follow_redirects=True, transport=transport, headers={"User-Agent": "FlowDeck-Importer/1.0"}, ) as client: diff --git a/tests/test_agent.py b/tests/test_agent.py index 3b4fd28..14c2a3d 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -36,6 +36,12 @@ def client(): settings.agent_max_iterations = 12 settings.agent_max_tokens_budget = 500_000 settings.agent_run_timeout_seconds = 30 + # Le rate limiter est DÉJÀ instancié (singleton importé avant ce fixture) : + # `os.environ["RATE_LIMIT_ENABLED"]="false"]` ci-dessus ne le touche pas. + # Sans cette mutation, les tests suivants d'un même worker dépassent le + # quota (60 req/min) et reçoivent des 429 — visible sous `pytest -n auto` + # en CI, invisible en local (.env de dev désactivé le limiter). + settings.rate_limit_enabled = False init_db() with get_conn() as conn: diff --git a/tests/test_agent_web_tools.py b/tests/test_agent_web_tools.py index 8a66944..99b5f41 100644 --- a/tests/test_agent_web_tools.py +++ b/tests/test_agent_web_tools.py @@ -70,6 +70,13 @@ def registry() -> ToolRegistry: return ToolRegistry() +#: Vraie factory du client partagé, figée AU CHARGEMENT du module (donc avant +#: tout patch). La restauration repart de CETTE référence : si un import tardif +#: d'un module consommateur a déjà figé une factory moquée, le `finally` la +#: répare au lieu de perpétuer la fuite. +_REAL_SHARED_CLIENT = http_client.shared_client + + @contextlib.contextmanager def mock_http(handler): """Injecte un transport httpx dans le client partagé des services. @@ -95,7 +102,12 @@ def mock_http(handler): finally: for module in targets: if getattr(module, "shared_client", None) is build: - module.shared_client = original + module.shared_client = _REAL_SHARED_CLIENT + # Un consommateur importé PENDANT la fenêtre patchée a figé `build` + # dans son propre namespace : on le remet sur la vraie factory. + from app.services.importers import url_fetch + if getattr(url_fetch, "shared_client", None) is build: + url_fetch.shared_client = _REAL_SHARED_CLIENT def run(coro):