Spaces:
Sleeping
Sleeping
Claude
feat(audit): Phase 3 partielle — câblage des features inachevées (S2, S4, S6)
bd5c812 unverified | """Phase 3.5 audit code-quality — les tests dans | |
| ``tests/integration/live/`` doivent porter le marker | |
| ``@pytest.mark.live`` sur **chacune** de leurs fonctions de test. | |
| Contexte : ``pyproject.toml`` déclare le marker ``live`` comme | |
| « tests d'intégration contre vraie API/binaire (Tesseract, | |
| Anthropic, OpenAI, Mistral) ; exclus par défaut, opt-in via | |
| ``pytest -m live`` ». Le filtre ``addopts = '-m "not live and not | |
| network"'`` les déselectionne au runner par défaut. | |
| Si une fonction dans ``tests/integration/live/`` oublie le marker, | |
| elle s'exécute lors du ``pytest tests/`` standard et : | |
| - échoue sur les runners sans la dep cloud → faux échec CI ; | |
| - consomme du quota API (clé en CI = facture surprise) ; | |
| - introduit une dépendance réseau non documentée. | |
| L'agent d'audit avait flaggé ``test_tesseract_live.py`` comme | |
| « skip top-level inconditionnel ». Vérification : le skip est en | |
| fait **conditionnel** (``if shutil.which("tesseract") is None``), | |
| ce qui est légitime — un test live qui peut s'exécuter seulement | |
| si le binaire est présent. Mais le garde-fou ci-dessous évite | |
| qu'une nouvelle fonction de test oublie le marker. | |
| """ | |
| from __future__ import annotations | |
| import ast | |
| from pathlib import Path | |
| import pytest | |
| LIVE_DIR = Path(__file__).resolve().parents[1] / "integration" / "live" | |
| def _test_functions(path: Path) -> list[tuple[str, ast.FunctionDef | ast.AsyncFunctionDef]]: | |
| """Liste les fonctions ``test_*`` au top-level d'un fichier.""" | |
| tree = ast.parse(path.read_text(encoding="utf-8")) | |
| out: list[tuple[str, ast.FunctionDef | ast.AsyncFunctionDef]] = [] | |
| for node in tree.body: | |
| if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith("test_"): | |
| out.append((node.name, node)) | |
| return out | |
| def _has_live_marker(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: | |
| for deco in fn.decorator_list: | |
| # ``@pytest.mark.live`` ou ``@pytest.mark.live(reason=...)`` | |
| if isinstance(deco, ast.Attribute) and deco.attr == "live": | |
| return True | |
| if isinstance(deco, ast.Call) and isinstance(deco.func, ast.Attribute) and deco.func.attr == "live": | |
| return True | |
| return False | |
| def _live_test_files() -> list[Path]: | |
| if not LIVE_DIR.exists(): | |
| return [] | |
| return [ | |
| p for p in sorted(LIVE_DIR.glob("test_*.py")) | |
| if p.name != "__init__.py" and p.name != "conftest.py" | |
| ] | |
| def test_every_function_in_live_dir_has_live_marker(path: Path) -> None: | |
| """Chaque ``test_*`` dans ``tests/integration/live/`` porte ``@pytest.mark.live``. | |
| Sinon le test peut s'exécuter en CI standard et casser sur | |
| l'absence de clé API / binaire externe. | |
| """ | |
| missing: list[str] = [] | |
| for name, fn in _test_functions(path): | |
| if not _has_live_marker(fn): | |
| missing.append(f" {path.name}:{fn.lineno} :: {name}") | |
| assert not missing, ( | |
| f"Fonctions dans {LIVE_DIR.name}/ sans ``@pytest.mark.live`` :\n" | |
| + "\n".join(missing) | |
| + "\n\nAjouter ``@pytest.mark.live`` au-dessus de chaque test " | |
| "qui hit une API/un binaire externe — sinon le test " | |
| "s'exécute sans opt-in et peut faire échouer le CI standard." | |
| ) | |