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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
31d3935bce
commit
c36b8dc631
@@ -0,0 +1,51 @@
|
||||
"""Fixtures partagées.
|
||||
|
||||
Le corpus de `data/input/` est lu tel quel : c'est la seule façon de garantir
|
||||
que les parseurs suivent le format réel, et non une idée qu'on s'en fait. Les
|
||||
tests ne l'écrivent jamais.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from collections.abc import Iterator
|
||||
|
||||
import pytest
|
||||
|
||||
from pipeline import chemins, db
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def annexe() -> str:
|
||||
"""Contenu de `sec10.md`, l'annexe récapitulative."""
|
||||
return chemins.chapitre(10).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def textes_annexe():
|
||||
"""Textes extraits de l'annexe, analysés une seule fois pour la session."""
|
||||
from pipeline.parseurs.tableaux_sec10 import parser
|
||||
|
||||
return parser(chemins.chapitre(10).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def par_identifiant(textes_annexe) -> dict:
|
||||
return {texte.id: texte for texte in textes_annexe.textes}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base(tmp_path) -> Iterator[sqlite3.Connection]:
|
||||
"""Base SQLite éphémère, migrée, propre à chaque test."""
|
||||
cx = db.initialiser(tmp_path / "test.db")
|
||||
try:
|
||||
yield cx
|
||||
finally:
|
||||
cx.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_ensemencee(base, textes_annexe) -> sqlite3.Connection:
|
||||
"""Base contenant les textes de l'annexe."""
|
||||
db.enregistrer_textes(base, textes_annexe.textes)
|
||||
return base
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Lecture des dates françaises du corpus."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from pipeline.parseurs.dates_fr import lire_date, lire_echeance, sans_accents, toutes_les_dates
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fragment", "attendu"),
|
||||
[
|
||||
# Formes réelles de la colonne « Promulgation / JO » du tableau A.
|
||||
("23 avr. 2026 (JO 24 avr.)", date(2026, 4, 23)),
|
||||
("29 avr. 2026 (JO 30 avr.)", date(2026, 4, 29)),
|
||||
("9 mai 2026 (JO 10 mai)", date(2026, 5, 9)),
|
||||
("12 juin 2026 (JO 13 juin)", date(2026, 6, 12)),
|
||||
("30 juin 2026 (JO 1er juill.)", date(2026, 6, 30)),
|
||||
("13 juill. 2026 (JO 14 juill.)", date(2026, 7, 13)),
|
||||
("1er juill. 2026", date(2026, 7, 1)),
|
||||
# Formes numériques du fichier de validation de phase 5.
|
||||
("24.07.2026", date(2026, 7, 24)),
|
||||
("22/07/2026", date(2026, 7, 22)),
|
||||
("2026-07-25", date(2026, 7, 25)),
|
||||
],
|
||||
)
|
||||
def test_lire_date_formes_du_corpus(fragment: str, attendu: date) -> None:
|
||||
assert lire_date(fragment) == attendu
|
||||
|
||||
|
||||
def test_lire_date_sans_annee_utilise_le_defaut() -> None:
|
||||
assert lire_date("6 juill.", annee_defaut=2026) == date(2026, 7, 6)
|
||||
assert lire_date("6 juill.") is None
|
||||
|
||||
|
||||
def test_lire_date_ignore_les_fragments_sans_jour() -> None:
|
||||
assert lire_date("Fin août 2026") is None
|
||||
assert lire_date("Automne 2026") is None
|
||||
|
||||
|
||||
def test_toutes_les_dates_conserve_l_ordre() -> None:
|
||||
assert toutes_les_dates("23 avr. 2026 (JO 24 avr.)", annee_defaut=2026) == [
|
||||
date(2026, 4, 23),
|
||||
date(2026, 4, 24),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fragment", "attendu"),
|
||||
[
|
||||
("~24 août 2026", date(2026, 8, 24)),
|
||||
("Début août 2026", date(2026, 8, 5)),
|
||||
("Fin août 2026", date(2026, 8, 25)),
|
||||
("Oct. 2026 ; adoption visée fin 2026", date(2026, 10, 15)),
|
||||
("Automne 2026", date(2026, 10, 1)),
|
||||
("Rentrée 2026", date(2026, 9, 1)),
|
||||
],
|
||||
)
|
||||
def test_lire_echeance_situe_les_previsions(fragment: str, attendu: date) -> None:
|
||||
approchee, libelle = lire_echeance(fragment)
|
||||
assert approchee == attendu
|
||||
# Le libellé d'origine est toujours conservé : c'est lui qui est affiché.
|
||||
assert libelle == fragment
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fragment",
|
||||
["Indéterminée", "Sans échéance", "Sans date fixée", "Session ordinaire 2026-2027"],
|
||||
)
|
||||
def test_lire_echeance_refuse_d_inventer(fragment: str) -> None:
|
||||
approchee, libelle = lire_echeance(fragment)
|
||||
assert approchee is None
|
||||
assert libelle == fragment
|
||||
|
||||
|
||||
def test_sans_accents() -> None:
|
||||
assert sans_accents("août") == "aout"
|
||||
assert sans_accents("Échéance probable") == "Echeance probable"
|
||||
assert sans_accents("chlordécone") == "chlordecone"
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Découpage des tableaux, nettoyage et marqueurs de citation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pipeline.parseurs.markdown import (
|
||||
decouper_sections,
|
||||
extraire_marqueurs,
|
||||
extraire_tableaux,
|
||||
nettoyer,
|
||||
)
|
||||
|
||||
TABLEAU = """\
|
||||
## Tableau d'essai
|
||||
|
||||
| Col A | Col B |
|
||||
|---|---|
|
||||
| a1 | b1 |
|
||||
| a2 | b2 |
|
||||
|
||||
Texte après le tableau.
|
||||
"""
|
||||
|
||||
|
||||
def test_extraire_tableaux_simple() -> None:
|
||||
(tableau,) = extraire_tableaux(TABLEAU)
|
||||
assert tableau.entetes == ["Col A", "Col B"]
|
||||
assert tableau.lignes == [{"Col A": "a1", "Col B": "b1"}, {"Col A": "a2", "Col B": "b2"}]
|
||||
assert tableau.section == "Tableau d'essai"
|
||||
assert len(tableau) == 2
|
||||
|
||||
|
||||
def test_extraire_tableaux_tolere_une_ligne_courte() -> None:
|
||||
source = "| A | B | C |\n|---|---|---|\n| 1 | 2 |\n"
|
||||
(tableau,) = extraire_tableaux(source)
|
||||
assert tableau.lignes == [{"A": "1", "B": "2", "C": ""}]
|
||||
|
||||
|
||||
def test_extraire_tableaux_respecte_les_barres_echappees() -> None:
|
||||
source = "| A | B |\n|---|---|\n| x \\| y | z |\n"
|
||||
(tableau,) = extraire_tableaux(source)
|
||||
assert tableau.lignes[0]["A"] == "x | y"
|
||||
|
||||
|
||||
def test_extraire_tableaux_sur_le_corpus_reel(annexe: str) -> None:
|
||||
tableaux = extraire_tableaux(annexe)
|
||||
assert len(tableaux) == 2
|
||||
tableau_a, tableau_b = tableaux
|
||||
assert tableau_a.entetes[0] == "N° de loi"
|
||||
assert len(tableau_a) == 27, "27 lois promulguées attendues au tableau A"
|
||||
assert tableau_b.entetes[0] == "Texte"
|
||||
assert len(tableau_b) == 23
|
||||
|
||||
|
||||
def test_nettoyer_retire_emphase_et_marqueurs() -> None:
|
||||
brut = "**Adoptée, non promulguée** (21 juill.) ; saisine [^dim04-487^][^phase5-3.1^]"
|
||||
assert nettoyer(brut) == "Adoptée, non promulguée (21 juill.) ; saisine"
|
||||
|
||||
|
||||
def test_nettoyer_peut_garder_les_marqueurs() -> None:
|
||||
brut = "Chlordécone [^dim07-24^]"
|
||||
assert nettoyer(brut, garder_marqueurs=True) == "Chlordécone [^dim07-24^]"
|
||||
|
||||
|
||||
def test_extraire_marqueurs_distingue_les_dimensions() -> None:
|
||||
marqueurs = extraire_marqueurs("[^dim07-24^] et [^phase5-3.1^] et [^insight-5^] et [^42^]")
|
||||
assert [(m.dimension, m.numero) for m in marqueurs] == [
|
||||
("dim07", "24"),
|
||||
("phase5", "3.1"),
|
||||
("insight", "5"),
|
||||
("", "42"),
|
||||
]
|
||||
|
||||
|
||||
def test_extraire_marqueurs_dedoublonne() -> None:
|
||||
assert len(extraire_marqueurs("[^dim01-3^] [^dim01-3^] [^dim01-4^]")) == 2
|
||||
|
||||
|
||||
def test_decouper_sections() -> None:
|
||||
source = "# Titre\ncorps un\n\n## Sous\ncorps deux\n"
|
||||
sections = decouper_sections(source)
|
||||
assert [s.titre for s in sections] == ["Titre", "Sous"]
|
||||
assert sections[0].corps == "corps un"
|
||||
assert sections[1].niveau == 2
|
||||
@@ -0,0 +1,305 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user