test(programme): Drift-Schutz zwischen WAHLPROGRAMME und PROGRAMME

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.
This commit is contained in:
Dotty Dotter 2026-05-08 14:18:41 +02:00
parent 27fd92c15f
commit bf5400ae33
2 changed files with 95 additions and 3 deletions

View File

@ -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": {

View File

@ -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)
# ─────────────────────────────────────────────────────────────────────────────