Files
veye-lalwa/tests/test_acceptation.py
T
Cyber MawonajandClaude Opus 5 f3e546cfc4 Phase 4 : impacts par public, arbitrages, calendrier — les 7 critères §7 passent
Impacts : le rapport ne dresse pas de tableau texte × public, mais il consacre
un chapitre à chacun (sec04 individus, sec05 associations, sec06 entreprises).
C'est ce placement éditorial — un choix explicite de l'auteur, pas une
inférence — qui sert de base à la ventilation. Le sens vient de la cotation
manuelle de l'annexe quand elle existe, sinon du vocabulaire, et le texte est
alors marqué « à vérifier » (17 cas).

Arbitrages de la validation de phase 5 portés en points clés :
loi-riposte affiche l'AFD à 500 €, et le montant de 1 500 € n'apparaît que
présenté comme la version sénatoriale abandonnée.

Migration 004 + calendrier : 9 échéances nationales transcrites du corpus avec
leur extrait source. Celle des sénatoriales porte explicitement
concerne_guadeloupe = 0 — série 1, renouvelée en 2023.

Défaut de qualité corrigé : un titre de section nommant plusieurs textes
(« Cohésion républicaine, logement, GloBE, entrisme ») faisait hériter chaque
phrase de transition aux quatre, polluant leurs résumés. Une section n'est
dédiée que si elle nomme un seul texte.

118 tests, 92 % de couverture. Les 7 garde-fous du §7 et les 5 démos du §8.7
sont automatisés dans tests/test_acceptation.py.

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

238 lines
9.8 KiB
Python

"""Critères d'acceptation du cahier des charges (§7 et §8.7).
Ces tests exécutent le seed complet sur le corpus réel, puis vérifient en base
les sept garde-fous du §7 et les scénarios de démonstration du §8.7. Ils
échouent si une régression altère un fait — c'est leur seule raison d'être.
"""
from __future__ import annotations
import json
import sqlite3
import pytest
from pipeline import db
from pipeline.seed_from_research import ensemencer
@pytest.fixture(scope="module")
def base_complete(tmp_path_factory) -> sqlite3.Connection:
"""Base issue d'un seed complet du corpus, partagée par le module."""
chemin = tmp_path_factory.mktemp("acceptation") / "veille.db"
cx = db.initialiser(chemin)
ensemencer(cx)
return cx
def _texte(cx: sqlite3.Connection, identifiant: str) -> sqlite3.Row:
ligne = cx.execute("SELECT * FROM v_textes WHERE id = ?", (identifiant,)).fetchone()
assert ligne is not None, f"{identifiant} absent de la base"
return ligne
def _affaires(cx: sqlite3.Connection, identifiant: str) -> list[str]:
return [
r["numero_affaire"]
for r in cx.execute(
"SELECT numero_affaire FROM decisions_cc WHERE texte_id = ? ORDER BY numero_affaire",
(identifiant,),
)
]
# ─────────────────────────────────────────────────────────────────────────────
# §2 — volumétrie
# ─────────────────────────────────────────────────────────────────────────────
def test_au_moins_49_textes(base_complete) -> None:
total = base_complete.execute("SELECT COUNT(*) FROM textes").fetchone()[0]
assert total >= 49
def test_chaque_texte_a_au_moins_une_source_url(base_complete) -> None:
"""§8.2 : 100 % des entrées avec au moins une source URL."""
orphelins = [
r["id"]
for r in base_complete.execute(
"SELECT id FROM textes t "
"WHERE NOT EXISTS (SELECT 1 FROM sources s WHERE s.texte_id = t.id)"
)
]
assert orphelins == []
def test_toutes_les_sources_ont_une_url_absolue(base_complete) -> None:
mauvaises = [
r["url"]
for r in base_complete.execute(
"SELECT url FROM sources WHERE url NOT LIKE 'http://%' AND url NOT LIKE 'https://%'"
)
]
assert mauvaises == []
# ─────────────────────────────────────────────────────────────────────────────
# §7 — les sept garde-fous
# ─────────────────────────────────────────────────────────────────────────────
def test_1_chlordecone(base_complete) -> None:
texte = _texte(base_complete, "loi-2026-491")
assert texte["numero_officiel"] == "2026-491"
assert texte["statut"] == "promulguee"
assert texte["date_promulgation"] == "2026-06-12"
assert texte["guadeloupe_pertinence"] == "forte"
assert {"sante", "outre_mer", "memoire_patrimoine"} <= set(json.loads(texte["themes"]))
assert texte["nb_sources"] > 0
def test_2_fraudes_sociales_et_fiscales(base_complete) -> None:
texte = _texte(base_complete, "loi-2026-534")
assert texte["statut"] == "promulguee"
assert texte["date_promulgation"] == "2026-06-25"
assert _affaires(base_complete, "loi-2026-534") == ["2026-904 DC"]
def test_3_loi_riposte(base_complete) -> None:
"""Statut, événement de saisine, et amende forfaitaire à 500 € — pas 1 500 €."""
texte = _texte(base_complete, "loi-riposte")
assert texte["statut"] == "adoptee_non_promulguee"
assert texte["numero_officiel"] is None
saisines = base_complete.execute(
"SELECT date_evenement, description FROM evenements "
"WHERE texte_id = 'loi-riposte' AND type_etape = 'saisine_cc'"
).fetchall()
assert len(saisines) == 1
assert saisines[0]["date_evenement"] == "2026-07-24"
assert "2026-915 DC" in saisines[0]["description"]
assert "24 juillet 2026" in saisines[0]["description"]
points = json.loads(texte["points_cles"])
assert any("500 €" in p for p in points), "l'AFD à 500 € doit figurer aux points clés"
# Le montant abandonné ne doit apparaître que présenté comme tel.
for point in points:
if "1 500 €" in point:
assert "abandonn" in point or "Sénat 1" in point
def test_4_ppl_aide_a_mourir(base_complete) -> None:
texte = _texte(base_complete, "ppl-aide-a-mourir")
assert texte["date_adoption"] == "2026-07-15"
assert _affaires(base_complete, "ppl-aide-a-mourir") == ["2026-910 DC"]
assert "291" in (texte["resume"] or ""), "le vote 291/241 doit apparaître au résumé"
def test_5_pjl_pacte_migration(base_complete) -> None:
texte = _texte(base_complete, "pjl-pacte-migration")
assert texte["statut"] == "navette"
assert "migration" in json.loads(texte["themes"])
def test_6_simplification_vie_economique(base_complete) -> None:
texte = _texte(base_complete, "loi-2026-403")
assert texte["statut"] == "promulguee"
decision = base_complete.execute(
"SELECT * FROM decisions_cc WHERE texte_id = 'loi-2026-403'"
).fetchone()
assert decision["numero_affaire"] == "2026-903 DC"
assert decision["resultat"] == "non_conformite_partielle"
assert "25 articles" in decision["resume"]
note = (texte["guadeloupe_note"] or "").lower()
assert "march" in note and "ultramarin" in note
def test_7_senatoriales_la_guadeloupe_n_est_pas_concernee(base_complete) -> None:
"""Le point 4 de la validation de phase 5 tranche : série 1, renouvelée en 2023."""
echeance = base_complete.execute(
"SELECT * FROM echeances WHERE code = 'senatoriales-2026'"
).fetchone()
assert echeance is not None
assert echeance["date_echeance"] == "2026-09-27"
assert echeance["concerne_guadeloupe"] == 0
assert "n'est PAS concernée" in echeance["note_guadeloupe"]
assert "série 1" in echeance["note_guadeloupe"]
# ─────────────────────────────────────────────────────────────────────────────
# §8.7 — scénarios de démonstration
# ─────────────────────────────────────────────────────────────────────────────
def test_recherche_chlordecone_classe_la_bonne_loi_en_tete(base_complete) -> None:
lignes = base_complete.execute(
"""
SELECT t.id
FROM textes_fts f
JOIN textes t ON t.rowid = f.rowid
WHERE textes_fts MATCH 'chlordecone'
ORDER BY bm25(textes_fts, 10.0, 5.0, 3.0, 2.0, 2.0, 1.0)
"""
).fetchall()
assert lignes, "la recherche « chlordécone » ne doit pas être vide"
assert lignes[0]["id"] == "loi-2026-491"
def test_facette_devant_le_conseil_constitutionnel(base_complete) -> None:
total = base_complete.execute(
"SELECT COUNT(*) FROM v_textes WHERE devant_cc = 1"
).fetchone()[0]
assert total >= 7
def test_facette_guadeloupe_forte_non_vide(base_complete) -> None:
total = base_complete.execute(
"SELECT COUNT(*) FROM textes WHERE guadeloupe_pertinence = 'forte'"
).fetchone()[0]
assert total > 0
def test_facettes_combinees(base_complete) -> None:
"""`/recherche?statut=saisie_cc&guadeloupe=forte` doit rendre des résultats."""
total = base_complete.execute(
"SELECT COUNT(*) FROM v_textes WHERE devant_cc = 1 AND guadeloupe_pertinence = 'forte'"
).fetchone()[0]
assert total > 0
def test_une_fiche_a_timeline_et_sources(base_complete) -> None:
texte = _texte(base_complete, "loi-riposte")
assert texte["nb_evenements"] >= 2
assert texte["nb_sources"] >= 3
# ─────────────────────────────────────────────────────────────────────────────
# Honnêteté des données
# ─────────────────────────────────────────────────────────────────────────────
def test_les_deductions_sont_signalees(base_complete) -> None:
"""Toute valeur déduite porte un motif de vérification lisible."""
for ligne in base_complete.execute("SELECT id, motif_verification FROM textes WHERE a_verifier = 1"):
assert ligne["motif_verification"], f"{ligne['id']} marqué à vérifier sans motif"
def test_aucune_pertinence_manuelle_n_a_ete_ecrasee(base_complete) -> None:
"""Les 27 lois promulguées gardent la cotation de l'annexe (§5.3)."""
deduites = base_complete.execute(
"SELECT COUNT(*) FROM textes WHERE statut = 'promulguee' "
"AND motif_verification LIKE '%Pertinence Guadeloupe déduite%'"
).fetchone()[0]
assert deduites == 0
def test_analyses_transversales_importees(base_complete) -> None:
total = base_complete.execute("SELECT COUNT(*) FROM insights").fetchone()[0]
assert total == 7
def test_echeances_du_calendrier(base_complete) -> None:
codes = {
r["code"] for r in base_complete.execute("SELECT code FROM echeances")
}
assert {"senatoriales-2026", "plf-2027", "facturation-electronique-reception"} <= codes