Schließt #222. Entfernt die Doppelung zwischen ``wahlprogramme.WAHLPROGRAMME`` und ``programme.PROGRAMME``. Single source of truth ist jetzt ``programme.PROGRAMME`` als Literal mit allen 287 Programmen (Wahlprogramme + Bundes- + Landes-Grundsatzprogramme, historisch + aktuell). Schema schmaler — Felder ohne Konsumenten entfallen: - ``regierungsbildung`` / ``regierungsende`` → gehören zu ``legislaturen.REGIERUNGEN``. Verbindung Programm→Regierung läuft jetzt über ``legislaturen.regierung_zum_zeitpunkt(bl, datum)``. - ``partei`` (Langform "CDU NRW") → ableitbar aus partei + bundesland. - ``jahr`` → ableitbar aus ``gueltig_ab[:4]``. - ``beschluss`` / ``wahl`` / ``hinweis`` → keine App-Konsumenten. Felder im neuen Schema: id, typ, partei, bundesland, wp, gueltig_ab, gueltig_bis, name, titel (Slogan, optional), pdf, seiten. Daten-Migration einmalig via ``tools/build_programme_literal.py``: - Basis: bisherige embeddings.PROGRAMME (alle 287 IDs + gueltig_ab/bis) - titel aus WAHLPROGRAMME für die ~80 aktuellen Wahlprogramme + Land-Grundsatzprogramm-Slogans (ehem. _ARCHIVED_SKELETONS) - seiten via ``fitz.open(p).page_count`` für alle 287 PDFs Aufrufer migriert: - app/main.py:4055 — ``aktuelles_wahlprogramm(bl, partei).pdf`` - app/wahlprogramm_check.py — ``parteien_mit_wahlprogramm(bl)`` - app/redline_utils.py — Reverse-Lookup über ``all_programme()`` - app/wahlprogramm_fetch.py (3 Stellen) — ``aktuelles_wahlprogramm()`` - tests/test_redline_parser.py — Programm-Lookup statt WAHLPROGRAMME ``wahlprogramme.py`` schrumpft auf den Such-Code: Keyword-Fallback + PDF-Text-Loader + ein dünner ``get_wahlprogramm``-Compat-Adapter zu ``programme.aktuelles_wahlprogramm``. Drei Helper gelöscht (keine App-Konsumenten): ``regierungsbildung_for``, ``regierungsende_for``, ``regierung_aktuell``. Wer das Datum der Regierungsbildung will, fragt ``legislaturen.aktuelle_regierung(bl).get('von')``. Test-Suite: 1217 grün (vorher 1244, Differenz 27 = entfernte regierungs-Helper-Tests + obsolete WAHLPROGRAMME-Strukturtests).
178 lines
8.9 KiB
Python
178 lines
8.9 KiB
Python
"""Tests for wahlprogramme.py.
|
|
|
|
Nach dem #222-Refactor ist dieses Modul nur noch ein dünner Wrapper:
|
|
keyword-basierte Suche + PDF-Text-Loader + ein Compat-Adapter
|
|
``get_wahlprogramm`` der zu ``programme.aktuelles_wahlprogramm``
|
|
delegiert. Die Stamm-Daten (``WAHLPROGRAMME``-Literal) sind nach
|
|
``programme.PROGRAMME`` gewandert. Strukturelle Daten-Tests leben
|
|
deshalb in ``test_programme.py``.
|
|
"""
|
|
import pytest
|
|
|
|
from app.wahlprogramme import (
|
|
REFERENZEN_PATH,
|
|
get_wahlprogramm,
|
|
load_wahlprogramm_text,
|
|
search_wahlprogramm,
|
|
find_relevant_quotes,
|
|
format_quote_for_prompt,
|
|
)
|
|
from app.programme import parteien_mit_wahlprogramm
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Stichproben aktiver Bundesländer + zugeordnete Parteien
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestParteienMitWahlprogramm:
|
|
"""Smoke-Test: in den jeweiligen BL liegen die erwarteten Fraktionen.
|
|
Strikte Schema-Tests gegen ``programme.PROGRAMME`` in test_programme.py."""
|
|
|
|
def test_nrw_has_five_parteien(self):
|
|
parteien = parteien_mit_wahlprogramm("NRW")
|
|
assert set(parteien) == {"CDU", "SPD", "GRÜNE", "FDP", "AfD"}
|
|
|
|
def test_bund_has_eight_parteien(self):
|
|
parteien = parteien_mit_wahlprogramm("BUND")
|
|
assert set(parteien) == {"CDU", "CSU", "SPD", "GRÜNE", "FDP", "AfD", "LINKE", "BSW"}
|
|
|
|
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", "BiW"}
|
|
|
|
def test_unknown_bundesland_empty_list(self):
|
|
assert parteien_mit_wahlprogramm("XX") == []
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# get_wahlprogramm — Compat-Adapter
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestGetWahlprogramm:
|
|
def test_returns_programm_for_known_combination(self):
|
|
prog = get_wahlprogramm("NRW", "CDU")
|
|
assert prog is not None
|
|
assert prog["pdf"] == "cdu-nrw-2022.pdf"
|
|
assert prog["partei"] == "CDU"
|
|
assert prog["bundesland"] == "NRW"
|
|
|
|
def test_returns_none_for_unknown_bundesland(self):
|
|
assert get_wahlprogramm("XX", "CDU") is None
|
|
|
|
def test_returns_none_for_unknown_partei(self):
|
|
assert get_wahlprogramm("NRW", "BSW") is None # BSW nicht im NRW-Landtag
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# File existence — every registered pdf must exist on disk
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestFileExistence:
|
|
"""Catches typos im pdf-Feld der Programm-Registry, die das
|
|
Indexing oder PDF-Download silently brechen würden."""
|
|
|
|
def test_every_registered_pdf_exists(self):
|
|
from app.programme import all_programme
|
|
|
|
missing = []
|
|
for prog in all_programme():
|
|
pdf = prog.get("pdf")
|
|
if not pdf:
|
|
continue
|
|
path = REFERENZEN_PATH / pdf
|
|
if not path.exists():
|
|
missing.append(f"{prog['id']}: {pdf}")
|
|
assert not missing, "missing PDFs:\n " + "\n ".join(missing)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# load_wahlprogramm_text — Fallback-Pfade (#134 Coverage-Backfill)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestLoadWahlprogrammText:
|
|
def test_returns_empty_for_unknown_combination(self):
|
|
assert load_wahlprogramm_text("XX", "XYZ") == {}
|
|
|
|
def test_paged_textfile_used_when_present(self, tmp_path, monkeypatch):
|
|
"""Wenn die paged-Textdatei existiert, wird sie genutzt.
|
|
Format: '--- PAGE N ---'-Marker pro Seitenanfang."""
|
|
from app import wahlprogramme as wp_mod
|
|
# Mock get_wahlprogramm -> bekannte Datei
|
|
monkeypatch.setattr(wp_mod, "get_wahlprogramm",
|
|
lambda bl, p: {"pdf": "test.pdf"})
|
|
paged = tmp_path / "test-paged.txt"
|
|
paged.write_text("--- PAGE 1 ---\nseite eins\n--- PAGE 2 ---\nseite zwei")
|
|
monkeypatch.setattr(wp_mod, "KONTEXT_PATH", tmp_path)
|
|
|
|
result = wp_mod.load_wahlprogramm_text("X", "Y")
|
|
assert 2 in result
|
|
assert "seite zwei" in result[2]
|
|
|
|
def test_falls_back_to_normal_textfile(self, tmp_path, monkeypatch):
|
|
"""Ohne paged-Datei wird auf normale .txt-Datei zurückgefallen,
|
|
komplett unter Seite 1."""
|
|
from app import wahlprogramme as wp_mod
|
|
monkeypatch.setattr(wp_mod, "get_wahlprogramm",
|
|
lambda bl, p: {"pdf": "test.pdf"})
|
|
normal = tmp_path / "test.txt"
|
|
normal.write_text("flacher text ohne seitenmarker")
|
|
monkeypatch.setattr(wp_mod, "KONTEXT_PATH", tmp_path)
|
|
|
|
result = wp_mod.load_wahlprogramm_text("X", "Y")
|
|
assert result == {1: "flacher text ohne seitenmarker"}
|
|
|
|
def test_returns_empty_when_no_textfile(self, tmp_path, monkeypatch):
|
|
"""Weder paged- noch flat-Textdatei vorhanden → leeres Dict."""
|
|
from app import wahlprogramme as wp_mod
|
|
monkeypatch.setattr(wp_mod, "get_wahlprogramm",
|
|
lambda bl, p: {"pdf": "test.pdf"})
|
|
monkeypatch.setattr(wp_mod, "KONTEXT_PATH", tmp_path)
|
|
|
|
result = wp_mod.load_wahlprogramm_text("X", "Y")
|
|
assert result == {}
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# search_wahlprogramm — Edge cases
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestSearchWahlprogramm:
|
|
def test_returns_empty_for_unknown_combination(self):
|
|
result = search_wahlprogramm("XX", "XYZ", ["foo"])
|
|
assert result == []
|
|
|
|
def test_returns_empty_when_text_missing(self, monkeypatch):
|
|
"""get_wahlprogramm liefert ein Programm, aber kein paged-Text:
|
|
search_wahlprogramm muss [] liefern, nicht crashen."""
|
|
from app import wahlprogramme as wp_mod
|
|
monkeypatch.setattr(wp_mod, "get_wahlprogramm",
|
|
lambda bl, p: {"pdf": "missing.pdf",
|
|
"name": "X Wahlprogramm 2024",
|
|
"gueltig_ab": "2024-01-01"})
|
|
monkeypatch.setattr(wp_mod, "load_wahlprogramm_text",
|
|
lambda bl, p: {})
|
|
assert search_wahlprogramm("X", "Y", ["foo"]) == []
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# find_relevant_quotes — Bundesland-Validierung
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestFindRelevantQuotes:
|
|
def test_unknown_bundesland_raises(self):
|
|
with pytest.raises(ValueError, match="Unbekanntes Bundesland"):
|
|
find_relevant_quotes("text", ["CDU"], "XX")
|
|
|
|
|
|
class TestFormatQuoteForPrompt:
|
|
def test_empty_quotes_returns_empty_string(self):
|
|
assert format_quote_for_prompt({}) == ""
|