gwoe-antragspruefer/tests/integration/test_frontend_xref.py
Dotty Dotter 73a7f76472 Add E2E functional acceptance test suite (#50, #51, #52, #53, #54)
Vier Sub-Issues unter Umbrella #50 — opt-in via 'pytest -m integration',
Default-Suite (77 Unit-Tests) bleibt unberührt.

- Sub-Issue A (#51): test_adapters_live.py — pro aktivem BL Reachability,
  Drucksache-ID-Format, Type-Filter, Datum-/Fraktion-Plausibilität,
  PDF-Link-HEAD-Probe (slow). NI als xfail (Login-Wall).
- Sub-Issue B (#52): test_frontend_xref.py + ground_truth.py — pro BL
  ein manuell kuratiertes Frontend-Sample (Drucksache + Title-Substring +
  Fraktionen + Datum + PDF-URL), gegen das adapter.get_document() gespiegelt
  wird. Fängt Bug-Klasse 14 (Cross-Bundesland-Match).
- Sub-Issue C (#53): test_wahlprogramme_indexed.py — Indexing-Status pro
  aktivem BL aus embeddings.db, PDF-Inhalts-Plausibilität (14 Marker +
  Wahlperioden-Horizont), expliziter Anti-Marker für Bug-Klasse 8
  (CDU-BE 2021 vs 2026 PDF-Tausch durch abgeordnetenwatch).
- Sub-Issue D (#54): test_citations_substring.py — Property-Verification:
  jedes vom LLM zitierte Snippet muss als (whitespace-normalisierter)
  Substring auf der angegebenen PDF-Seite vorhanden sein. Strict-Match
  mit Truncation-Marker-Toleranz, kein Fuzzy. Liest reale Assessments
  aus gwoe-antraege.db. Fängt Bug-Klassen 7/10/17 (Halluzination).

Architektur: separates tests/integration/ Verzeichnis mit eigenem
conftest.py, das die Stubs der Unit-Suite (fitz/bs4/openai/pydantic_settings)
gezielt entfernt und auf echte Module umstellt — mit Fallback-Skip via
pytest.require_module wenn lokale Dev-Maschine die Prod-Deps nicht hat.

206 neue Integration-Tests, 13 Helper-Unit-Tests. 77 Unit-Tests bleiben grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:00:20 +02:00

106 lines
3.9 KiB
Python

"""Sub-Issue B — Adapter ↔ Frontend Cross-Validation.
Pro aktivem BL ist im ``ground_truth.py``-Modul ein einzelnes Drucksachen-
Tupel kuratiert, das aus der echten Frontend-Suche des jeweiligen
Landtags stammt. Dieser Test ruft ``adapter.get_document(...)`` mit der
bekannten ID auf und prüft, dass:
- die Drucksache überhaupt gefunden wird
- der Title (substring) passt
- die erwarteten Fraktionen drin sind
- das Datum (wenn gesetzt im Sample) übereinstimmt
- der PDF-Link das erwartete URL-Fragment enthält
Bug-Klassen aus den letzten Sessions, die diese Datei abdeckt:
- 14 (get_document() liefert Match aus falschem Bundesland)
- Allgemeine Schema-Drift in URL-Strukturen, Hit-Format-Änderungen,
Encoding-Bugs, Pagination-Cut-Offs, Adapter-Reuse-Konfigurations-Fehler
Issue: #52 (Sub-Issue B des Umbrella #50)
Wartung: siehe Doku im ``ground_truth.py``-Header.
"""
import pytest
from app.bundeslaender import aktive_bundeslaender
from app.parlamente import ADAPTERS
from .ground_truth import GROUND_TRUTH
pytestmark = pytest.mark.integration
_ACTIVE_CODES = {bl.code for bl in aktive_bundeslaender()}
# Skip Samples für BL die nicht (mehr) aktiv sind
_GT_PARAMS = [pytest.param(gt, id=gt.bundesland) for gt in GROUND_TRUTH if gt.bundesland in _ACTIVE_CODES]
@pytest.mark.parametrize("gt", _GT_PARAMS)
async def test_adapter_finds_known_drucksache(gt):
"""Cross-Validation gegen die Frontend-Suche des jeweiligen Landtags.
Wenn dieser Test fehlschlägt: erst den Frontend-URL aus
``gt.frontend_search_url`` öffnen und prüfen, ob die Drucksache
überhaupt noch existiert. Wenn ja → Adapter-Bug. Wenn nein → ein
neues Sample im ``ground_truth.py`` aufnehmen.
"""
if gt.bundesland not in ADAPTERS:
pytest.skip(f"{gt.bundesland} hat keinen registrierten Adapter")
if not gt.title_substring:
pytest.skip(
f"{gt.bundesland}: Sample noch nicht kuratiert "
"(title_substring leer in ground_truth.py)"
)
adapter = ADAPTERS[gt.bundesland]
doc = await adapter.get_document(gt.drucksache)
assert doc is not None, (
f"{gt.bundesland} adapter ({type(adapter).__name__}) hat die "
f"bekannte Drucksache {gt.drucksache!r} nicht gefunden. Frontend-"
f"Probe: {gt.frontend_search_url}"
)
# 1. Drucksachen-Nummer roundtrip
assert doc.drucksache == gt.drucksache, (
f"{gt.bundesland}: get_document({gt.drucksache!r}) lieferte "
f"abweichende drucksache={doc.drucksache!r}"
)
# 2. Title-Substring
assert gt.title_substring.lower() in doc.title.lower(), (
f"{gt.bundesland}: title_substring {gt.title_substring!r} nicht "
f"in adapter-title {doc.title!r}"
)
# 3. Erwartete Fraktionen sind alle da (Subset-Match — Adapter darf
# mehr Fraktionen erkennen als das Sample erwartet)
if gt.expected_fraktionen:
adapter_fraktionen = set(doc.fraktionen)
missing = gt.expected_fraktionen - adapter_fraktionen
assert not missing, (
f"{gt.bundesland}: erwartete Fraktionen {gt.expected_fraktionen} "
f"nicht alle im Adapter-Output {adapter_fraktionen}; fehlt: {missing}"
)
# 4. Datum (nur wenn das Sample eines hat)
if gt.datum:
assert doc.datum == gt.datum, (
f"{gt.bundesland}: erwartetes datum={gt.datum!r}, adapter lieferte "
f"{doc.datum!r}"
)
# 5. PDF-Link enthält erwartetes URL-Fragment
if gt.pdf_url_substring:
assert gt.pdf_url_substring.lower() in doc.link.lower(), (
f"{gt.bundesland}: pdf_url_substring {gt.pdf_url_substring!r} "
f"nicht in adapter-link {doc.link!r}"
)
# 6. Bundesland-Konsistenz — fängt Bug-Klasse 14 (Cross-Bundesland-Match)
assert doc.bundesland == gt.bundesland, (
f"adapter[{gt.bundesland}].get_document() lieferte ein Doc mit "
f"bundesland={doc.bundesland!r}"
)