gwoe-antragspruefer/tests/test_programme.py
Dotty Dotter b003cc1d6d feat(programme): Wahlprogramm-Geltung beginnt am Wahltag (B1+B2)
Antwort auf B1 + B2 aus der Roadmap:
- B1: Antrag VOR Regierungsbildung (z.B. NRW WP18-Antrag im Mai 2022,
  vor Vereidigung Wuest II am 29.06.2022) bekommt jetzt das passende
  Wahlprogramm zurueck — der Geltungsbeginn ist der Wahltag, nicht die
  Vereidigung.
- B2: Opposition vs. Regierung wird einheitlich behandelt. Die fruehere
  Logik "Geltung ab Regierungsbildung" war fuer Regierungsfraktionen
  intuitiv (Koalitionsvertrag wird zu Politik), fuer Opposition aber
  willkuerlich. Programme werden zur Wahl beschlossen und sind
  Wahlversprechen ab dem Tag der Wahl.

Implementation in programme._migrate_from_legacy:
- gueltig_ab = aktuelle_legislatur(bl)["wahltermin"] (Fallback auf
  altes "regierungsbildung" fuer rueckwaerts-kompatible Eintraege)
- ``wahl``-Feld auf Wahltag gesetzt
- ``wp``-Feld aus aktuelle_legislatur ergaenzt

Das ``regierungsbildung``-Feld in WAHLPROGRAMME bleibt erhalten und
versorgt den Bewertungs-Kontext-Block weiterhin mit dem Anzeige-Wert
"Regierung zur Antragszeit" (per legislaturen.regierung_zum_zeitpunkt
laeuft das primaer ueber legislaturen.REGIERUNGEN).

UI-Effekt: im Antrag-Detail liest sich z.B. ein BUND-Eintrag jetzt
"gueltig seit 2025-02-23, 60 S." (BTW-Wahltag) statt "2025-05-06"
(Vereidigung Merz I).

Tests: 115 gruen (test_programme + test_legislaturen + test_wahlprogramme
+ test_embeddings). Tests test_bund_btw_2025_in_uebergangsphase und
test_bund_btw_2025_vor_wahl neu, decken die geaenderte Geltungs-Logik
explizit ab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:17:08 +02:00

232 lines
11 KiB
Python

"""Tests for app.programme — zentrale Programm-Registry mit Geltungsdaten.
Diese Tests prüfen die Helper-API und die Migrationspfade aus
``WAHLPROGRAMME`` und ``embeddings.PROGRAMME``. Architektur-Doku in
docs/adr/0013-programme-legislaturen-zeitpunktige-bewertung.md.
"""
import sys
import types
import pytest
# Stub openai für embeddings.py-Import (programme._migrate_from_legacy()
# triggert beim ersten API-Call den embeddings-Import).
if "openai" not in sys.modules:
o = types.ModuleType("openai")
o.OpenAI = lambda **kw: None
sys.modules["openai"] = o
try:
import fitz as _fitz
if not hasattr(_fitz, "open"):
import pymupdf
sys.modules["fitz"] = pymupdf
except ImportError:
try:
import pymupdf
sys.modules["fitz"] = pymupdf
except ImportError:
pass
from app.programme import (
PROGRAMME,
aktuelles_wahlprogramm,
alle_versionen,
all_programme,
get_programm,
grundsatzprogramm_zum_zeitpunkt,
parteien_mit_wahlprogramm,
wahlprogramm_zum_zeitpunkt,
)
# ─────────────────────────────────────────────────────────────────────────────
# Migration: Daten landen aus den Legacy-Quellen in PROGRAMME
# ─────────────────────────────────────────────────────────────────────────────
class TestMigration:
def test_migration_populates_programme(self):
progs = all_programme()
assert len(progs) > 80, \
f"Expected >80 programme nach migration; got {len(progs)}"
def test_alle_typen_vertreten(self):
progs = all_programme()
typs = {p["typ"] for p in progs}
assert "wahlprogramm" in typs
assert "grundsatzprogramm-bund" in typs
assert "grundsatzprogramm-land" in typs
def test_jeder_eintrag_hat_pflichtfelder(self):
for prog in all_programme():
for f in ("id", "titel", "name", "typ", "partei",
"gueltig_ab", "pdf"):
assert f in prog, f"{prog.get('id')}: feld {f} fehlt"
def test_ids_sind_eindeutig(self):
ids = [p["id"] for p in all_programme()]
assert len(ids) == len(set(ids)), "Duplicate Programm-IDs"
# ─────────────────────────────────────────────────────────────────────────────
# aktuelles_wahlprogramm
# ─────────────────────────────────────────────────────────────────────────────
class TestAktuellesWahlprogramm:
def test_nrw_cdu_returns_2022(self):
p = aktuelles_wahlprogramm("NRW", "CDU")
assert p is not None
assert p["id"] == "cdu-nrw-2022"
assert p["bundesland"] == "NRW"
assert p["partei"] == "CDU"
assert p["typ"] == "wahlprogramm"
def test_bund_cdu_returns_btw_2025(self):
p = aktuelles_wahlprogramm("BUND", "CDU")
assert p is not None
assert p["id"] == "cdu-bund-2025"
# Wahltag, nicht Regierungsbildung (B1+B2: Programme gelten ab Wahl)
assert p["gueltig_ab"] == "2025-02-23"
def test_unknown_bl_returns_none(self):
assert aktuelles_wahlprogramm("XX", "CDU") is None
def test_unknown_partei_returns_none(self):
assert aktuelles_wahlprogramm("NRW", "BSW") is None # nicht im Landtag NRW
def test_aktuelles_hat_gueltig_bis_none(self):
for bl in ["NRW", "BUND", "BB", "TH"]:
for partei in parteien_mit_wahlprogramm(bl):
p = aktuelles_wahlprogramm(bl, partei)
if p is not None:
assert p["gueltig_bis"] is None, \
f"{bl}/{partei} aktuell aber gueltig_bis gesetzt"
# ─────────────────────────────────────────────────────────────────────────────
# wahlprogramm_zum_zeitpunkt — historisch korrekte Einordnung
# ─────────────────────────────────────────────────────────────────────────────
class TestWahlprogrammZumZeitpunkt:
def test_nrw_cdu_aktuelles_datum(self):
# 2024-01-01 liegt im Geltungszeitraum von cdu-nrw-2022
p = wahlprogramm_zum_zeitpunkt("NRW", "CDU", "2024-01-01")
assert p is not None
assert p["id"] == "cdu-nrw-2022"
def test_bund_btw_2025_nach_wahl(self):
# Nach Wahltag (2025-02-23) gilt das BTW-2025-Programm.
p = wahlprogramm_zum_zeitpunkt("BUND", "SPD", "2025-09-01")
assert p is not None
assert p["id"] == "spd-bund-2025"
def test_bund_btw_2025_in_uebergangsphase(self):
# Zwischen Wahl (2025-02-23) und Vereidigung Merz I (2025-05-06)
# gilt das BTW-2025-Programm bereits — B1+B2 (Programme gelten ab
# Wahltag).
p = wahlprogramm_zum_zeitpunkt("BUND", "SPD", "2025-04-01")
assert p is not None
assert p["id"] == "spd-bund-2025"
def test_bund_btw_2025_vor_wahl(self):
# Vor BTW 2025 (vor 2025-02-23) kein Bund-Wahlprogramm indiziert.
p = wahlprogramm_zum_zeitpunkt("BUND", "SPD", "2024-01-01")
assert p is None
def test_datum_vor_aktueller_wp(self):
# Antrag aus 2018 in NRW: aktuelle WP18-Programm gilt erst ab
# Wahltag 2022-05-15.
p = wahlprogramm_zum_zeitpunkt("NRW", "CDU", "2018-09-01")
# Heute keine WP17-Programme indiziert → erwarten None.
assert p is None
# ─────────────────────────────────────────────────────────────────────────────
# grundsatzprogramm_zum_zeitpunkt — Bund + Land mit Vorgänger-Logik
# ─────────────────────────────────────────────────────────────────────────────
class TestGrundsatzprogrammZumZeitpunkt:
def test_cdu_bund_2025(self):
p = grundsatzprogramm_zum_zeitpunkt("CDU", "2025-01-01")
assert p is not None
assert p["id"] == "cdu-grundsatz" # 2024er Programm
assert p["typ"] == "grundsatzprogramm-bund"
def test_cdu_bund_vor_2024_keiner(self):
# Hannoveraner Programm 2007 ist nicht im Schema indiziert.
p = grundsatzprogramm_zum_zeitpunkt("CDU", "2010-01-01")
assert p is None
def test_cdu_nrw_landesgrundsatz_bevorzugt(self):
# Mit bundesland=NRW wird das Landesgrundsatzprogramm zurückgegeben,
# nicht das Bundes.
p = grundsatzprogramm_zum_zeitpunkt("CDU", "2024-01-01", bundesland="NRW")
assert p is not None
assert p["id"] == "cdu-grundsatz-nrw"
assert p["typ"] == "grundsatzprogramm-land"
def test_cdu_he_kein_landesgrundsatz_fallback_auf_bund(self):
# Hessen hat kein CDU-Landesgrundsatzprogramm → Fallback auf Bund.
p = grundsatzprogramm_zum_zeitpunkt("CDU", "2025-01-01", bundesland="HE")
assert p is not None
assert p["id"] == "cdu-grundsatz" # Bund
def test_ssw_sh_landesgrundsatz(self):
# SSW existiert nur in SH — Rahmenprogramm 2016.
p = grundsatzprogramm_zum_zeitpunkt("SSW", "2024-01-01", bundesland="SH")
assert p is not None
assert p["id"] == "ssw-grundsatz"
def test_csu_2023_aktuell(self):
p = grundsatzprogramm_zum_zeitpunkt("CSU", "2024-01-01", bundesland="BY")
assert p is not None
assert p["id"] == "csu-grundsatz"
assert p["gueltig_ab"] == "2023-05-06"
# ─────────────────────────────────────────────────────────────────────────────
# parteien_mit_wahlprogramm
# ─────────────────────────────────────────────────────────────────────────────
class TestParteienMitWahlprogramm:
def test_nrw_5_parteien(self):
parteien = set(parteien_mit_wahlprogramm("NRW"))
assert parteien == {"CDU", "SPD", "GRÜNE", "FDP", "AfD"}
def test_bund_8_parteien(self):
parteien = set(parteien_mit_wahlprogramm("BUND"))
assert parteien == {"CDU", "CSU", "SPD", "GRÜNE", "FDP",
"AfD", "LINKE", "BSW"}
def test_unknown_bl_empty(self):
assert parteien_mit_wahlprogramm("XX") == []
# ─────────────────────────────────────────────────────────────────────────────
# alle_versionen — Lieferung sortiert nach gueltig_ab
# ─────────────────────────────────────────────────────────────────────────────
class TestAlleVersionen:
def test_nrw_cdu_eine_version(self):
# Heute nur cdu-nrw-2022 indiziert.
versions = alle_versionen("NRW", "CDU")
assert len(versions) == 1
assert versions[0]["id"] == "cdu-nrw-2022"
def test_unknown_bl_leer(self):
assert alle_versionen("XX", "CDU") == []
# ─────────────────────────────────────────────────────────────────────────────
# get_programm — direct ID-Lookup
# ─────────────────────────────────────────────────────────────────────────────
class TestGetProgramm:
def test_known_id(self):
p = get_programm("cdu-nrw-2022")
assert p is not None
assert p["partei"] == "CDU"
def test_unknown_id_returns_none(self):
assert get_programm("does-not-exist") is None