gwoe-antragspruefer/tests/test_programme.py

261 lines
12 KiB
Python
Raw Normal View History

"""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"
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
# 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"
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
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"
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
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_2024_returns_btw_2021(self):
# Antrag aus 2024-01-01 liegt im Geltungszeitraum von BTW 2021
# (Scholz-Ampel-Programm, gueltig 2021-09-26 bis 2025-02-23).
p = wahlprogramm_zum_zeitpunkt("BUND", "SPD", "2024-01-01")
assert p is not None
assert p["id"] == "spd-bund-2021"
assert p["wp"] == 20
def test_bund_grenze_btw_2021_btw_2025(self):
# Tag vor BTW 2025: Scholz-Ampel-Programm gilt noch.
p_alt = wahlprogramm_zum_zeitpunkt("BUND", "SPD", "2025-02-22")
assert p_alt["id"] == "spd-bund-2021"
# Wahltag BTW 2025: neues Programm gilt.
p_neu = wahlprogramm_zum_zeitpunkt("BUND", "SPD", "2025-02-23")
assert p_neu["id"] == "spd-bund-2025"
def test_datum_vor_aktueller_wp_nrw_wp17(self):
# Antrag aus 2018 in NRW: WP17-Programm (cdu-nrw-2017) gilt
# ab 2017-05-14 bis 2022-05-15.
p = wahlprogramm_zum_zeitpunkt("NRW", "CDU", "2018-09-01")
assert p is not None
assert p["id"] == "cdu-nrw-2017"
assert p["wp"] == 17
assert p["gueltig_ab"] == "2017-05-14"
assert p["gueltig_bis"] == "2022-05-15"
def test_grenze_zwischen_wp17_und_wp18(self):
# Genau am Wahltag der nächsten WP (2022-05-15) gilt das neue
# Programm. range = [gueltig_ab, gueltig_bis), also gueltig_bis
# selbst ist exklusiv.
p_alt = wahlprogramm_zum_zeitpunkt("NRW", "CDU", "2022-05-14")
assert p_alt["id"] == "cdu-nrw-2017"
p_neu = wahlprogramm_zum_zeitpunkt("NRW", "CDU", "2022-05-15")
assert p_neu["id"] == "cdu-nrw-2022"
# ─────────────────────────────────────────────────────────────────────────────
# 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_drei_versionen(self):
# WP16 (2012) + WP17 (2017) + WP18 (2022) — historisch komplett bis 2012.
versions = alle_versionen("NRW", "CDU")
assert len(versions) >= 3
# sortiert nach gueltig_ab aufsteigend
ids = [v["id"] for v in versions]
assert "cdu-nrw-2012" in ids
assert "cdu-nrw-2017" in ids
assert "cdu-nrw-2022" in ids
# die erste Version muss älter sein als die zweite
assert versions[0]["gueltig_ab"] < versions[1]["gueltig_ab"]
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