"""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_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_zwei_versionen(self): # WP17 (2017) + WP18 (2022) sind indiziert. versions = alle_versionen("NRW", "CDU") assert len(versions) == 2 # sortiert nach gueltig_ab aufsteigend assert versions[0]["id"] == "cdu-nrw-2017" assert versions[1]["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