Collecteurs, chacun isolé et tolérant à l'échec : - legifrance : OAuth PISTE validé en production. Deux pièges vérifiés sur le compte réel — seul le couple « Client ID / Client secret » ouvre un jeton (la clé d'API donne invalid_client), et les identifiants de production ne passent pas en bac à sable. - senat : liste chronologique des lois promulguées. L'URL du §5.1 (senat.fr/lois/index.html) est morte ; l'équivalent fonctionnel est /dossiers-legislatifs/lois-promulguees.html — 31 lois 2026 relevées. - conseil_constitutionnel : sans le paramètre ?id=32246 la page des affaires en instance ne rend que des QPC et aucune affaire DC. Les colonnes DC et QPC diffèrent : lecture par en-tête, jamais par position. Rapprochement à trois issues — numéro officiel, similarité de titre, et une zone de signalement où le pipeline s'abstient plutôt que de fusionner. make update-dry contre les sources réelles : aucun faux positif sur les 52 textes du corpus, et trois apports que le rapport n'avait pas — le numéro d'affaire 2026-907 DC de la programmation militaire, la date de saisine du 16 juillet pour l'aide à mourir, les décisions du 23 juillet sur 908 et 909. Une asymétrie de sources est documentée par un test plutôt que corrigée en douce : le Sénat ne qualifie pas la loi 2026-650 d'organique, Légifrance si. Le collecteur rapporte ce que sa source dit ; le champ n'est pas comparé. Fixtures HTML figées : aucun test ne touche au réseau. 156 tests, 91 % de couverture (chaque module au-dessus du seuil de 70 %). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
241 lines
9.8 KiB
Python
241 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."""
|
|
lignes = base_complete.execute(
|
|
"SELECT id, motif_verification FROM textes WHERE a_verifier = 1"
|
|
)
|
|
for ligne in lignes:
|
|
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
|