Files
veye-lalwa/tests/test_tableaux_sec10.py
T
Cyber MawonajandClaude Opus 5 c36b8dc631 Phase 2 : parseur de l'annexe sec10 — 52 textes, 12 affaires CC
Briques réutilisables :
- dates_fr : lit les formes réelles du corpus (« 23 avr. 2026 (JO 24 avr.) »,
  « 1er juill. », « 24.07.2026 ») et distingue une date exacte d'une échéance
  prévisionnelle (« Fin août 2026 » → situé au 25, libellé conservé, jamais
  présenté comme un fait)
- markdown : découpage des tableaux (barres échappées, lignes courtes),
  nettoyage, extraction des marqueurs [^dim07-24^] / [^phase5-3.1^]

Parseur sec10 :
- 27 lois promulguées + 21 lignes simples + 2 lignes éclatées en 4 textes = 52
- éclatement justifié : les lignes « accords Kazakhstan et Colombie » et
  « MACF » décrivent chacune deux textes de statuts différents
- migration 002 : regime_libertes stocké à part, faute de savoir quel public
  la cotation globale du rapport vise — dérouler « droits+ » sur les trois
  publics aurait inventé une ventilation absente de la source
- migration 003 : la facette « devant le CC » couvre aussi les saisines sans
  numéro d'affaire (programmation militaire), sinon 6 textes au lieu de 7

Trois pièges du corpus, corrigés et couverts par des tests :
- « eau » matchait comme sous-chaîne dans réseaux / drapeau / Retailleau
- une décision citée sans saisine créait une fausse saisine (loi Philippine)
- la date de saisine était pêchée dans la colonne « échéance », datant la
  saisine de l'aide à mourir du mois suivant

72 tests, 90 % de couverture sur pipeline/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 18:22:18 -04:00

306 lines
14 KiB
Python

"""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