"""Parseur de l'annexe récapitulative. Ces tests portent sur le corpus réel : ils échouent si le format d'entrée change, ce qui est le comportement voulu — un seed silencieusement incomplet serait pire qu'une erreur bruyante. """ from __future__ import annotations from datetime import date import pytest from pipeline.modeles import ( Pertinence, RegimeLibertes, ResultatCC, Statut, Theme, TypeTexte, ) from pipeline.parseurs.tableaux_sec10 import ( _deduire_themes, _lire_pertinence, _lire_regime, _lire_statut, extraire_marqueurs_annexe, ) # ───────────────────────────────────────────────────────────────────────────── # Volumétrie # ───────────────────────────────────────────────────────────────────────────── def test_aucun_avertissement(textes_annexe) -> None: assert textes_annexe.avertissements == [] def test_volumetrie_minimale(textes_annexe) -> None: # 27 lois promulguées + 23 lignes du tableau B, dont 2 éclatées en deux # textes chacune : 27 + 21 + 4 = 52. assert len(textes_annexe) == 52 assert len(textes_annexe) >= 49, "le cahier des charges exige au moins 49 textes" def test_identifiants_uniques(textes_annexe) -> None: identifiants = [t.id for t in textes_annexe.textes] assert len(identifiants) == len(set(identifiants)) def test_toutes_les_lois_promulguees_ont_numero_et_date(textes_annexe) -> None: promulguees = [t for t in textes_annexe.textes if t.statut is Statut.PROMULGUEE] assert len(promulguees) == 27 for texte in promulguees: assert texte.numero_officiel, texte.id assert texte.date_promulgation, texte.id assert texte.regime_libertes is not None, texte.id assert texte.guadeloupe_pertinence is not None, texte.id def test_aucun_texte_non_promulgue_ne_porte_de_numero(textes_annexe) -> None: for texte in textes_annexe.textes: if texte.statut is not Statut.PROMULGUEE: assert texte.numero_officiel is None, texte.id # ───────────────────────────────────────────────────────────────────────────── # Contrôles d'acceptation du §7 du cahier des charges # ───────────────────────────────────────────────────────────────────────────── def test_loi_chlordecone(par_identifiant) -> None: texte = par_identifiant["loi-2026-491"] assert texte.numero_officiel == "2026-491" assert texte.statut is Statut.PROMULGUEE assert texte.date_promulgation == date(2026, 6, 12) assert texte.guadeloupe_pertinence is Pertinence.FORTE assert {Theme.SANTE, Theme.OUTRE_MER, Theme.MEMOIRE_PATRIMOINE} <= set(texte.themes) def test_loi_fraudes_et_sa_decision_cc(par_identifiant) -> None: texte = par_identifiant["loi-2026-534"] assert texte.statut is Statut.PROMULGUEE assert texte.date_promulgation == date(2026, 6, 25) assert [d.numero_affaire for d in texte.decisions_cc] == ["2026-904 DC"] assert texte.guadeloupe_pertinence is Pertinence.FORTE def test_loi_riposte_statut_et_evenement_de_saisine(par_identifiant) -> None: """Le §7 impose le statut `adoptee_non_promulguee` ET l'événement de saisine.""" texte = par_identifiant["loi-riposte"] assert texte.statut is Statut.ADOPTEE_NON_PROMULGUEE assert texte.numero_officiel is None saisines = [e for e in texte.evenements if e.type_etape == "saisine_cc"] assert len(saisines) == 1 assert "2026-915 DC" in saisines[0].description assert "24 juillet 2026" in saisines[0].description assert saisines[0].date_evenement == date(2026, 7, 24) (affaire,) = texte.decisions_cc assert affaire.numero_affaire == "2026-915 DC" assert affaire.resultat is ResultatCC.EN_INSTANCE assert affaire.date_saisine == date(2026, 7, 24) def test_ppl_aide_a_mourir(par_identifiant) -> None: texte = par_identifiant["ppl-aide-a-mourir"] assert texte.type is TypeTexte.PPL assert texte.statut is Statut.ADOPTEE_NON_PROMULGUEE assert texte.date_adoption == date(2026, 7, 15) (affaire,) = texte.decisions_cc assert affaire.numero_affaire == "2026-910 DC" # Le corpus ne date pas cette saisine : rien ne doit être inventé. assert affaire.date_saisine is None assert affaire.date_decision_attendue == date(2026, 8, 20) def test_pjl_pacte_migration(par_identifiant) -> None: texte = par_identifiant["pjl-pacte-migration"] assert texte.type is TypeTexte.PJL assert texte.statut is Statut.NAVETTE assert Theme.MIGRATION in texte.themes def test_loi_simplification_economique(par_identifiant) -> None: texte = par_identifiant["loi-2026-403"] assert texte.statut is Statut.PROMULGUEE (affaire,) = texte.decisions_cc assert affaire.numero_affaire == "2026-903 DC" assert affaire.resultat is ResultatCC.NON_CONFORMITE_PARTIELLE assert "25 articles censurés" in (affaire.resume or "") assert texte.guadeloupe_pertinence is Pertinence.FORTE def test_loi_philippine_est_validee_sans_fausse_saisine(par_identifiant) -> None: """Sa cellule cite une décision, pas une saisine : aucun événement de saisine.""" texte = par_identifiant["loi-philippine"] assert texte.statut is Statut.VALIDEE_CC assert [e.type_etape for e in texte.evenements if e.type_etape == "saisine_cc"] == [] decisions = [e for e in texte.evenements if e.type_etape == "decision_cc"] assert decisions and decisions[0].date_evenement == date(2026, 7, 23) def test_programmation_militaire_saisie_sans_numero_d_affaire(par_identifiant) -> None: """« Conseil constitutionnel saisi le 6 juill. » : événement, pas d'affaire.""" texte = par_identifiant["loi-programmation-militaire"] saisines = [e for e in texte.evenements if e.type_etape == "saisine_cc"] assert len(saisines) == 1 assert saisines[0].date_evenement == date(2026, 7, 6) assert texte.decisions_cc == [] # ───────────────────────────────────────────────────────────────────────────── # Éclatement des lignes doubles # ───────────────────────────────────────────────────────────────────────────── def test_accords_bilateraux_eclates(par_identifiant) -> None: kazakhstan = par_identifiant["accord-kazakhstan-readmission"] colombie = par_identifiant["accord-colombie-extradition"] assert kazakhstan.type is TypeTexte.ACCORD_INTERNATIONAL assert kazakhstan.date_adoption == date(2026, 1, 28) assert colombie.date_adoption == date(2026, 2, 18) def test_macf_eclate_en_deux_statuts(par_identifiant) -> None: """La ligne MACF décrit deux textes de statuts différents.""" ratification = par_identifiant["pjl-ratification-ordonnance-macf"] accord = par_identifiant["accord-macf-plateforme-centrale"] assert ratification.statut is Statut.DEPOSEE_NON_EXAMINEE assert accord.statut is Statut.ANNONCEE # ───────────────────────────────────────────────────────────────────────────── # Lecture de cellules # ───────────────────────────────────────────────────────────────────────────── @pytest.mark.parametrize( ("cellule", "attendu"), [ ("**Droits+** : contrôle externe de la détention", RegimeLibertes.DROITS_PLUS), ("**Contrôle+** : durées réduites", RegimeLibertes.CONTROLE_PLUS), ("Mixte : anti-impayés ; vigilance débiteur", RegimeLibertes.MIXTE), ("Neutre", RegimeLibertes.NEUTRE), ("Neutre à **droits+** : équité territoriale", RegimeLibertes.DROITS_PLUS), ], ) def test_lire_regime(cellule: str, attendu: RegimeLibertes) -> None: regime, note = _lire_regime(cellule) assert regime is attendu assert note @pytest.mark.parametrize( ("cellule", "attendu", "note"), [ ("Forte — maison d'arrêt de Baie-Mahault", Pertinence.FORTE, "maison d'arrêt"), ("Moyenne — CPCE applicable aux DROM", Pertinence.MOYENNE, "CPCE"), ("Faible — sans objet", Pertinence.FAIBLE, "sans objet"), ], ) def test_lire_pertinence(cellule: str, attendu: Pertinence, note: str) -> None: pertinence, motif = _lire_pertinence(cellule) assert pertinence is attendu assert note in (motif or "") @pytest.mark.parametrize( ("cellule", "attendu"), [ ("**Adoptée, non promulguée** (21 juill., CMP)", Statut.ADOPTEE_NON_PROMULGUEE), ("**En navette** : Sénat 1re lecture 20 mai", Statut.NAVETTE), ("**Déposé, non examiné** : Sénat n° 890", Statut.DEPOSEE_NON_EXAMINEE), ("**Annoncé** : transmis au Conseil d'État", Statut.ANNONCEE), ("**Adoptée par une chambre** : Sénat 5-6 mai", Statut.NAVETTE), ("**Adoptée en 1re lecture** : AN 28 mai", Statut.NAVETTE), ("**Adoptée** (15-16 juin) ; conforme avec réserves ; **promulgation pendante**", Statut.VALIDEE_CC), ], ) def test_lire_statut(cellule: str, attendu: Statut) -> None: assert _lire_statut(cellule) is attendu def test_lire_statut_refuse_l_inconnu() -> None: with pytest.raises(ValueError, match="statut non reconnu"): _lire_statut("Texte au statut fantaisiste") def test_themes_ne_confondent_pas_les_sous_chaines() -> None: """« eau » ne doit pas se déclencher sur « réseaux », « drapeau », « Retailleau ».""" assert Theme.ENVIRONNEMENT not in _deduire_themes("PPL réseaux sociaux et mineurs") assert Theme.ENVIRONNEMENT not in _deduire_themes("outrage à l'hymne et au drapeau") assert Theme.ENVIRONNEMENT not in _deduire_themes("PPL entrisme Retailleau") # Contrôle positif : le mot isolé compte bien. assert Theme.ENVIRONNEMENT in _deduire_themes("habilitation Martinique énergie et eau") def test_themes_plafonnes_a_quatre() -> None: long_texte = " ".join( ["justice", "sécurité", "numérique", "social", "fiscal", "agriculture", "santé"] ) assert len(_deduire_themes(long_texte)) <= 4 # ───────────────────────────────────────────────────────────────────────────── # Marqueurs de citation # ───────────────────────────────────────────────────────────────────────────── def test_marqueurs_de_l_annexe(annexe: str) -> None: associations = extraire_marqueurs_annexe(annexe) assert "2026-491" in associations assert "dim07-24" in associations["2026-491"] riposte = next(cle for cle in associations if cle.startswith("Loi « Riposte »")) assert "phase5-3.1" in associations[riposte] # ───────────────────────────────────────────────────────────────────────────── # Écriture en base # ───────────────────────────────────────────────────────────────────────────── def test_seed_en_base(base_ensemencee) -> None: cx = base_ensemencee assert cx.execute("SELECT COUNT(*) FROM textes").fetchone()[0] == 52 assert cx.execute("SELECT COUNT(*) FROM decisions_cc").fetchone()[0] == 12 def test_facette_devant_cc(base_ensemencee) -> None: """Le §8.7 attend au moins 7 textes suspendus au Conseil constitutionnel.""" total = base_ensemencee.execute( "SELECT COUNT(*) FROM v_textes WHERE devant_cc = 1" ).fetchone()[0] assert total >= 7 def test_les_lois_promulguees_ne_sont_pas_devant_le_cc(base_ensemencee) -> None: """Une décision rendue mais non datée ne doit pas passer pour une instance.""" restantes = base_ensemencee.execute( "SELECT id FROM v_textes WHERE devant_cc = 1 AND statut = 'promulguee'" ).fetchall() assert restantes == [] def test_recherche_plein_texte(base_ensemencee) -> None: """La recherche doit trouver « chlordécone » sans les accents.""" lignes = base_ensemencee.execute( "SELECT t.id FROM textes_fts f JOIN textes t ON t.rowid = f.rowid " "WHERE textes_fts MATCH 'chlordecone'" ).fetchall() assert [ligne["id"] for ligne in lignes] == ["loi-2026-491"] def test_ecriture_idempotente(base, textes_annexe) -> None: """Rejouer le seed ne duplique ni textes, ni sources, ni événements.""" from pipeline import db db.enregistrer_textes(base, textes_annexe.textes) premier = db.statistiques(base) db.enregistrer_textes(base, textes_annexe.textes) second = db.statistiques(base) assert premier == second