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