Erste Tests für die Codebase. 77 Tests, 0.08s Laufzeit, decken die
drei Bug-Klassen aus der April-2026-Adapter-Session ab plus haben
schon zwei weitere Bugs in Production-Code aufgedeckt.
## Setup
- requirements-dev.txt mit pytest + pytest-asyncio
- pytest.ini mit asyncio_mode=auto
- tests/conftest.py stubbt fitz/bs4/openai/pydantic_settings, damit
die Suite ohne den vollen prod-requirements-Satz läuft (pure unit
tests, kein PDF-Parsing, kein HTTP)
## Tests
- tests/test_parlamente.py (33 Tests)
* PortalaAdapter._parse_hit_list_cards: doctype/doctype_full
NameError-Regression aus 1cb030a, plus Title/Drucksache/Fraktion-
/Datum/PDF-Extraktion gegen ein BE-Card-Fixture
* PortalaAdapter._parse_hit_list_dump: gegen ein LSA-Perl-Dump-
Fixture inkl. Hex-Escape-Decoding (\x{fc} → ü)
* PortalaAdapter._parse_hit_list_html: Auto-Detection zwischen
Card- und Dump-Format
* PortalaAdapter._normalize_fraktion: kanonische Fraktion-Codes
inkl. F.D.P.-mit-Punkten, BÜNDNIS 90, DIE LINKE, BSW
* ParLDokAdapter._hit_to_drucksache: JSON-Hit → Drucksache
Mapping inkl. /navpanes-Stripping, MdL-mit-Partei-in-Klammern,
Landesregierung-Detection
* ParLDokAdapter._fulltext_id: bundle.js-mirroring (deferred,
aber dokumentiert)
* ADAPTERS-Registry-Sanity
- tests/test_embeddings.py (11 Tests)
* _chunk_source_label: Programm-Name + Seite (Halluzinations-
Bug-Regression aus 1b5fd96)
* format_quotes_for_prompt: jeder Chunk muss Programm-Name
enthalten, strict-citation-Hinweis muss im Output sein,
keine NRW-Halluzinationen für MV/BE-Chunk-Sets
- tests/test_wahlprogramme.py (14 Tests)
* Registry-Struktur (jahr int, seiten int, .pdf-Endung)
* File-Existenz: jede registrierte PDF muss in
static/referenzen/ liegen — würde Tippfehler in den 22
indexierten Programmen sofort fangen
* embeddings.PROGRAMME-Konsistenz-Cross-Check
- tests/test_bundeslaender.py (15 Tests)
* Sanity über 16-State-Registry
* #48-Klassifikations-Regression: TH=ParlDok, HB=StarWeb,
SN=Eigensystem
* Wahltermine plausibel (zwischen 2026 und 2035)
- tests/test_analyzer.py (4 Tests)
* Markdown-Codeblock-Stripping aus dem JSON-Retry-Loop
## Bug-Funde während der Test-Schreibphase
Zwei Production-Bugs in den _normalize_fraktion-Helfern wurden
durch die neuen Tests sofort aufgedeckt und im selben Commit gefixt:
1. PortalaAdapter._normalize_fraktion matched "F.D.P." (mit Punkten,
wie historische SH/HB-Drucksachen) nicht — Regex \bFDP\b ist zu
strikt. Fix: \bF\.?\s*D\.?\s*P\.?\b analog zu ParLDokAdapter.
2. ParLDokAdapter._normalize_fraktion (auch PortalaAdapter) matched
"Ministerium der Finanzen" nicht als Landesregierung, weil
\bMINISTER\b die Wortgrenze auch nach MINISTER verlangt — bei
MINISTERIUM steht aber IUM danach, keine Wortgrenze. Fix:
\bMINISTER ohne abschließendes \b.
Beide Bugs hätten Fraktion-Felder bei Drucksachen der Bremischen
Bürgerschaft (FDP-Listen) und bei Landesregierungs-Drucksachen
in MV/LSA fälschlich leer gelassen — exakt der "fraktionen=[]"-
Befund aus dem MV-Smoke-Test in #4.
Phase 0 aus Roadmap-Issue #49.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
5.8 KiB
Python
119 lines
5.8 KiB
Python
"""Tests for wahlprogramme.py — registry consistency + file existence."""
|
|
from app.wahlprogramme import (
|
|
WAHLPROGRAMME,
|
|
REFERENZEN_PATH,
|
|
get_wahlprogramm,
|
|
parteien_mit_wahlprogramm,
|
|
)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Registry consistency
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class TestRegistryStructure:
|
|
def test_active_bundeslaender_present(self):
|
|
for code in ["NRW", "LSA", "MV", "BE"]:
|
|
assert code in WAHLPROGRAMME, f"missing wahlprogramme entry for {code}"
|
|
|
|
def test_each_entry_has_required_keys(self):
|
|
required = {"file", "titel", "partei", "jahr", "seiten"}
|
|
for bl, parteien in WAHLPROGRAMME.items():
|
|
for partei, info in parteien.items():
|
|
missing = required - set(info.keys())
|
|
assert not missing, f"{bl}/{partei} missing keys: {missing}"
|
|
|
|
def test_jahr_is_integer(self):
|
|
for bl, parteien in WAHLPROGRAMME.items():
|
|
for partei, info in parteien.items():
|
|
assert isinstance(info["jahr"], int), f"{bl}/{partei} jahr not int"
|
|
|
|
def test_seiten_is_positive_integer(self):
|
|
for bl, parteien in WAHLPROGRAMME.items():
|
|
for partei, info in parteien.items():
|
|
assert isinstance(info["seiten"], int)
|
|
assert info["seiten"] > 0
|
|
|
|
def test_file_extension_is_pdf(self):
|
|
for bl, parteien in WAHLPROGRAMME.items():
|
|
for partei, info in parteien.items():
|
|
assert info["file"].endswith(".pdf")
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# File existence — every registered file must exist on disk
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class TestFileExistence:
|
|
"""Catches typos in the file field that would silently break embedding
|
|
indexing or PDF download links."""
|
|
|
|
def test_every_registered_pdf_exists(self):
|
|
missing = []
|
|
for bl, parteien in WAHLPROGRAMME.items():
|
|
for partei, info in parteien.items():
|
|
path = REFERENZEN_PATH / info["file"]
|
|
if not path.exists():
|
|
missing.append(f"{bl}/{partei}: {info['file']}")
|
|
assert not missing, "missing PDFs:\n " + "\n ".join(missing)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Lookup helpers
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class TestGetWahlprogramm:
|
|
def test_returns_dict_for_known_combination(self):
|
|
info = get_wahlprogramm("MV", "CDU")
|
|
assert info is not None
|
|
assert info["partei"] == "CDU Mecklenburg-Vorpommern"
|
|
|
|
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
|
|
|
|
|
|
class TestParteienMitWahlprogramm:
|
|
def test_nrw_has_five_parteien(self):
|
|
parteien = parteien_mit_wahlprogramm("NRW")
|
|
assert len(parteien) == 5
|
|
assert set(parteien) == {"CDU", "SPD", "GRÜNE", "FDP", "AfD"}
|
|
|
|
def test_mv_has_six_parteien(self):
|
|
parteien = parteien_mit_wahlprogramm("MV")
|
|
assert set(parteien) == {"CDU", "SPD", "GRÜNE", "FDP", "AfD", "LINKE"}
|
|
|
|
def test_be_has_five_parteien(self):
|
|
parteien = parteien_mit_wahlprogramm("BE")
|
|
assert set(parteien) == {"CDU", "SPD", "GRÜNE", "LINKE", "AfD"}
|
|
|
|
def test_unknown_bundesland_empty_list(self):
|
|
assert parteien_mit_wahlprogramm("XX") == []
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# embeddings.PROGRAMME consistency cross-check
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class TestEmbeddingsRegistryConsistency:
|
|
"""Every entry in WAHLPROGRAMME must also exist in embeddings.PROGRAMME
|
|
so the indexer can find it. Mismatch is the kind of bug a manual smoke
|
|
misses but would show up during indexing."""
|
|
|
|
def test_every_wahlprogramm_has_embeddings_entry(self):
|
|
from app.embeddings import PROGRAMME
|
|
|
|
# Build expected programm_id from filename: "cdu-mv-2021.pdf" → "cdu-mv-2021"
|
|
missing = []
|
|
for bl, parteien in WAHLPROGRAMME.items():
|
|
for partei, info in parteien.items():
|
|
pid = info["file"].rsplit(".", 1)[0]
|
|
if pid not in PROGRAMME:
|
|
missing.append(f"{bl}/{partei} → {pid}")
|
|
assert not missing, (
|
|
"WAHLPROGRAMME entries missing in embeddings.PROGRAMME:\n "
|
|
+ "\n ".join(missing)
|
|
)
|