From bf5400ae332b0ca3a9f625a0ccb0c7982f2a4f3f Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Fri, 8 May 2026 14:18:41 +0200 Subject: [PATCH] test(programme): Drift-Schutz zwischen WAHLPROGRAMME und PROGRAMME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0013 hatte als offene Folge "Doppelter Daten-Bestand zwischen WAHLPROGRAMME und embeddings.PROGRAMME ist nicht aufgelöst — Risk: stille Drift". Der invasive Compat-Shim (#222) ist aufwendig; bis dahin fängt eine neue Test-Klasse die Drift bidirektional ab: TestWahlprogrammeProgrammeConsistency (4 Tests): - Jeder WAHLPROGRAMME-Eintrag hat ein passendes aktuelles Programm in PROGRAMME (sonst liefert aktuelles_wahlprogramm None) - pdf-Dateinamen müssen übereinstimmen (file == pdf) - Partei-Kurzform muss übereinstimmen - Jedes aktuelle Wahlprogramm in PROGRAMME muss auch in WAHLPROGRAMME registriert sein (orphan-check andere Richtung) Drift-Funde dabei: - BIW (Bürger in Wut) HB war in PROGRAMME (biw-hb-2023, biw-hb-2019, biw-hb-2015), aber NICHT in WAHLPROGRAMME-HB. Bewertungs-Pipeline hätte BIW-Anträge gegen kein Wahlprogramm geprüft. Eintrag ergänzt: BÜRGER IN WUT — Programm Bürgerschaftswahl 2023 (26 Seiten). - Test test_hb_has_four_parteien → test_hb_has_five_parteien. 92/92 Programme-Tests grün. --- app/wahlprogramme.py | 4 +- tests/test_wahlprogramme.py | 94 ++++++++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/app/wahlprogramme.py b/app/wahlprogramme.py index fe08115..521521e 100644 --- a/app/wahlprogramme.py +++ b/app/wahlprogramme.py @@ -121,12 +121,14 @@ WAHLPROGRAMME: dict[str, dict[str, dict]] = { "AfD": {"file": "afd-by-2023.pdf", "titel": "AfD Bayern Wahlprogramm 2023", "partei": "AfD Bayern", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-11-07", "regierungsende": None}, }, # Bremen — Bürgerschaftswahl 14.05.2023. Senat Bovenschulte II (SPD+GRÜNE+LINKE) vereidigt 04.07.2023. - # AfD war wegen Listenstreit nicht zur Wahl zugelassen. + # AfD war wegen Listenstreit nicht zur Wahl zugelassen — stattdessen ist + # BIW (Bürger in Wut) als 6. Fraktion in der 21. Bürgerschaft. "HB": { "SPD": {"file": "spd-hb-2023.pdf", "titel": "SPD Bremen Wahlprogramm Bürgerschaftswahl 2023", "partei": "SPD Bremen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-07-04", "regierungsende": None}, "CDU": {"file": "cdu-hb-2023.pdf", "titel": "CDU Bremen Wahlprogramm Bürgerschaftswahl 2023", "partei": "CDU Bremen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-07-04", "regierungsende": None}, "GRÜNE": {"file": "gruene-hb-2023.pdf","titel": "BÜNDNIS 90/DIE GRÜNEN Bremen Wahlprogramm 2023", "partei": "BÜNDNIS 90/DIE GRÜNEN Bremen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-07-04", "regierungsende": None}, "LINKE": {"file": "linke-hb-2023.pdf", "titel": "DIE LINKE Bremen Wahlprogramm Bürgerschaftswahl 2023", "partei": "DIE LINKE Bremen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-07-04", "regierungsende": None}, + "BIW": {"file": "biw-hb-2023.pdf", "titel": "BÜRGER IN WUT — Programm für die Bürgerschaftswahl 2023", "partei": "BIW Bremen", "jahr": 2023, "seiten": 26, "regierungsbildung": "2023-07-04", "regierungsende": None}, }, # Hessen — LTW 08.10.2023. Kabinett Rhein II (CDU+SPD) vereidigt 18.01.2024. "HE": { diff --git a/tests/test_wahlprogramme.py b/tests/test_wahlprogramme.py index d0b30bd..b819eef 100644 --- a/tests/test_wahlprogramme.py +++ b/tests/test_wahlprogramme.py @@ -182,10 +182,11 @@ class TestParteienMitWahlprogramm: parteien = parteien_mit_wahlprogramm("BY") assert set(parteien) == {"CSU", "FREIE WÄHLER", "GRÜNE", "SPD", "AfD"} - def test_hb_has_four_parteien(self): + def test_hb_has_five_parteien(self): # AfD war wegen Listenstreit nicht zur Bürgerschaftswahl 2023 zugelassen. + # Stattdessen ist BIW (Bürger in Wut) als 6. Fraktion in der 21. WP. parteien = parteien_mit_wahlprogramm("HB") - assert set(parteien) == {"SPD", "CDU", "GRÜNE", "LINKE"} + assert set(parteien) == {"SPD", "CDU", "GRÜNE", "LINKE", "BIW"} def test_he_has_five_parteien(self): parteien = parteien_mit_wahlprogramm("HE") @@ -240,6 +241,95 @@ class TestEmbeddingsRegistryConsistency: ) +# ───────────────────────────────────────────────────────────────────────────── +# Strikte Cross-Konsistenz mit programme.PROGRAMME (Drift-Schutz, ADR 0013) +# ───────────────────────────────────────────────────────────────────────────── + +class TestWahlprogrammeProgrammeConsistency: + """WAHLPROGRAMME (legacy, mit titel/seiten/regierung) und + programme.PROGRAMME (zentrale Registry mit Geltungsdaten) speichern + überlappende Felder. Diese Tests fangen stille Drift, bis #222 die + Quelle vereinheitlicht (Compat-Shim). + + Invarianten: + - Für jedes (bl, partei) in WAHLPROGRAMME liefert + ``aktuelles_wahlprogramm(bl, partei)`` einen Eintrag (nicht None). + - Der ``pdf``-Wert in PROGRAMME stimmt mit ``file`` in WAHLPROGRAMME + überein. + - Die ``partei``-Kurzform stimmt überein (PROGRAMME["partei"] ist die + Kurzform; WAHLPROGRAMME-Key ist auch Kurzform). + """ + + def test_every_wahlprogramm_has_aktuelles_programm_match(self): + from app.programme import aktuelles_wahlprogramm + + mismatches = [] + for bl, parteien in WAHLPROGRAMME.items(): + for partei, info in parteien.items(): + prog = aktuelles_wahlprogramm(bl, partei) + if prog is None: + mismatches.append( + f"{bl}/{partei}: aktuelles_wahlprogramm liefert None, " + f"obwohl WAHLPROGRAMME-Eintrag {info['file']} existiert" + ) + assert not mismatches, "\n ".join(mismatches) + + def test_pdf_filenames_match_between_registries(self): + from app.programme import aktuelles_wahlprogramm + + drift = [] + for bl, parteien in WAHLPROGRAMME.items(): + for partei, info in parteien.items(): + prog = aktuelles_wahlprogramm(bl, partei) + if prog is None: + continue # vom Vortest abgedeckt + wp_pdf = info["file"] + pr_pdf = prog.get("pdf") + if wp_pdf != pr_pdf: + drift.append(f"{bl}/{partei}: WAHLPROGRAMME.file={wp_pdf!r} ≠ PROGRAMME.pdf={pr_pdf!r}") + assert not drift, "Drift zwischen WAHLPROGRAMME und PROGRAMME:\n " + "\n ".join(drift) + + def test_partei_kurzform_consistency(self): + """PROGRAMME["partei"] ist die Kurzform (z.B. 'CDU'), nicht + die Langform ('CDU NRW'). Test-Sicherheitsnetz, falls jemand + versehentlich die Langform reinträgt.""" + from app.programme import aktuelles_wahlprogramm + + wrong = [] + for bl, parteien in WAHLPROGRAMME.items(): + for partei in parteien.keys(): + prog = aktuelles_wahlprogramm(bl, partei) + if prog is None: + continue + if prog.get("partei") != partei: + wrong.append( + f"{bl}/{partei}: PROGRAMME.partei={prog.get('partei')!r} " + f"≠ WAHLPROGRAMME-Key {partei!r}" + ) + assert not wrong, "\n ".join(wrong) + + def test_no_orphan_aktuelle_programme_in_registry(self): + """Die andere Richtung: jedes aktuelle Wahlprogramm in PROGRAMME + (gueltig_bis IS NULL, typ='wahlprogramm') muss in WAHLPROGRAMME + vorhanden sein. Sonst ist die Bewertungs-Pipeline blind dafür.""" + from app.programme import all_programme + + orphans = [] + for prog in all_programme(): + if prog.get("typ") != "wahlprogramm": + continue + if prog.get("gueltig_bis") is not None: + continue # historisches Programm + bl = prog.get("bundesland") + partei = prog.get("partei") + if bl not in WAHLPROGRAMME: + orphans.append(f"{prog['id']}: BL {bl} nicht in WAHLPROGRAMME") + continue + if partei not in WAHLPROGRAMME[bl]: + orphans.append(f"{prog['id']}: {bl}/{partei} fehlt in WAHLPROGRAMME") + assert not orphans, "Aktuelle Wahlprogramme in PROGRAMME ohne WAHLPROGRAMME-Eintrag:\n " + "\n ".join(orphans) + + # ───────────────────────────────────────────────────────────────────────────── # load_wahlprogramm_text — Fallback-Pfade (#134 Coverage-Backfill) # ─────────────────────────────────────────────────────────────────────────────