"""Tests Sprint A10 — gouvernance institutionnelle (M-10 + M-11). Garde-fou : les 6 fichiers de gouvernance doivent exister, ne pas être vides, et couvrir les sections clés. Empêche une suppression accidentelle ou un PR qui viderait l'un d'eux par inadvertance. """ from __future__ import annotations from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[2] GOVERNANCE_FILES = [ "CODEOWNERS", # cherché à la racine ET dans .github/ "GOVERNANCE.md", "CODE_OF_CONDUCT.md", "SECURITY.md", # pré-existant (Sprint 24) "ACCESSIBILITY.md", # Sprint A7 "LICENSE", # pré-existant "CONTRIBUTING.md", # pré-existant (Sprint 30) ] def _resolve(name: str) -> Path | None: """Cherche un fichier à la racine, dans ``.github/`` ou ``docs/``.""" candidates = [ REPO_ROOT / name, REPO_ROOT / ".github" / name, REPO_ROOT / "docs" / name, ] for c in candidates: if c.exists(): return c return None @pytest.mark.parametrize("name", GOVERNANCE_FILES) def test_governance_file_exists(name: str) -> None: """Chaque fichier de gouvernance doit exister.""" path = _resolve(name) assert path is not None, ( f"{name} introuvable — recherché à racine/, .github/, docs/." ) @pytest.mark.parametrize("name", GOVERNANCE_FILES) def test_governance_file_not_empty(name: str) -> None: """Chaque fichier de gouvernance doit avoir un contenu non trivial (≥ 200 octets — un README dégradé serait < 100 octets).""" path = _resolve(name) if path is None: pytest.skip(f"{name} absent — couvert par test_governance_file_exists") assert path.stat().st_size >= 200, ( f"{path.name} fait seulement {path.stat().st_size} octets — " "fichier vide ou tronqué." ) def test_codeowners_has_catchall() -> None: """``.github/CODEOWNERS`` doit avoir une ligne catch-all ``* @user`` pour qu'aucun chemin ne soit sans reviewer.""" f = REPO_ROOT / ".github" / "CODEOWNERS" assert f.exists() text = f.read_text(encoding="utf-8") # Ligne qui commence par `*` suivi d'au moins un @ has_catchall = any( line.strip().startswith("*") and "@" in line for line in text.splitlines() if not line.strip().startswith("#") and line.strip() ) assert has_catchall, ( "CODEOWNERS doit contenir une ligne catch-all `* @user` " "pour garantir un reviewer par défaut." ) def test_governance_documents_release_cadence() -> None: """``GOVERNANCE.md`` doit documenter la cadence de release.""" f = REPO_ROOT / "GOVERNANCE.md" text = f.read_text(encoding="utf-8") for keyword in ["Patch", "Mineure", "Majeure", "release", "SLO"]: assert keyword in text, ( f"GOVERNANCE.md doit mentionner la notion `{keyword}`" ) def test_governance_documents_coi() -> None: """``GOVERNANCE.md`` doit contenir une section Conflicts of interest (item M-10 de l'audit).""" f = REPO_ROOT / "GOVERNANCE.md" text = f.read_text(encoding="utf-8") # Cherche l'une des formulations acceptables coi_markers = [ "Conflicts of interest", "Conflits d'intérêt", "indépendance", "Affiliations des mainteneurs", ] found = [m for m in coi_markers if m in text] assert found, ( f"GOVERNANCE.md doit contenir une section COI. " f"Aucun marqueur trouvé parmi {coi_markers}." ) def test_coi_mentions_pricing_independence() -> None: """La section COI doit explicitement traiter l'indépendance des données de pricing vis-à-vis des fournisseurs benchmarkés.""" f = REPO_ROOT / "GOVERNANCE.md" text = f.read_text(encoding="utf-8") assert "pricing" in text.lower() # Doit mentionner au moins un fournisseur cloud benchmarké providers = ["OpenAI", "Anthropic", "Mistral", "Google", "Azure"] found = [p for p in providers if p in text] assert len(found) >= 3, ( f"COI doit citer ≥ 3 fournisseurs cloud benchmarkés ; " f"trouvé : {found}" ) def test_code_of_conduct_uses_contributor_covenant() -> None: """``CODE_OF_CONDUCT.md`` doit s'appuyer sur Contributor Covenant (standard de facto, traduit en 30+ langues).""" f = REPO_ROOT / "CODE_OF_CONDUCT.md" text = f.read_text(encoding="utf-8") assert "Contributor Covenant" in text # Doit mentionner les 4 niveaux d'application standard levels = ["Correction", "Avertissement", "Bannissement"] for level in levels: assert level in text, f"CoC doit décrire le niveau `{level}`" def test_governance_links_other_docs() -> None: """``GOVERNANCE.md`` doit lier les autres docs de gouvernance pour cohérence du système.""" f = REPO_ROOT / "GOVERNANCE.md" text = f.read_text(encoding="utf-8") expected_links = [ "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "SECURITY.md", "ACCESSIBILITY.md", ] missing = [link for link in expected_links if link not in text] assert not missing, ( f"GOVERNANCE.md doit linker : {missing}" )