test: 467 -> 574 Tests (+107) — DDD, abgeordnetenwatch, monitoring, v2, Bug-Regressions
Neue Tests in dieser Migration:
- test_database.py (Merkliste-CRUD, Subscriptions, abgeordnetenwatch-Joins)
- test_clustering.py (82% Coverage)
- test_drucksache_typen.py (100%)
- test_mail.py (86%)
- test_monitoring.py (23 Tests)
- test_abgeordnetenwatch.py (23 Tests, inkl. Drucksache-Extraction)
- test_redline_parser.py (20 Tests fuer §INS§/§DEL§-Marker)
- test_bug_regressions.py (PRAGMA, JWT-azp, CDU-PDF, PFLICHT-FRAKTIONEN, NRW-Titel)
- test_embeddings_v3_v4.py (WRITE/READ-Pattern)
- test_wahlprogramm_check.py (#128)
- test_wahlprogramm_fetch.py (#138)
- test_antrag/bewertung/abonnement_repository.py + test_llm_bewerter.py (DDD)
- test_domain_behavior.py (5 Domain-Methoden boundary tests)
- tests/e2e/test_ui.py (Playwright)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
"""Tests für _parse_redline_segments aus app.main.
|
|
|
|
|
|
|
|
|
|
Prüft alle Marker-Formate (§INS§/§DEL§ und **/**+~~), Edge-Cases und
|
|
|
|
|
gemischte Eingaben. Keine DB- oder HTTP-Abhängigkeiten.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from app.redline_utils import parse_redline_segments as _parse_redline_segments
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Hilfsfunktionen
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def types(segments):
|
|
|
|
|
return [s["type"] for s in segments]
|
|
|
|
|
|
|
|
|
|
def texts(segments):
|
|
|
|
|
return [s["text"] for s in segments]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Basis-Parsing
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class TestBasicParsing:
|
|
|
|
|
def test_empty_string_returns_empty(self):
|
|
|
|
|
assert _parse_redline_segments("") == []
|
|
|
|
|
|
|
|
|
|
def test_none_returns_empty(self):
|
|
|
|
|
assert _parse_redline_segments(None) == [] # type: ignore[arg-type]
|
|
|
|
|
|
|
|
|
|
def test_plain_text_is_ctx(self):
|
|
|
|
|
segs = _parse_redline_segments("kein Marker hier")
|
|
|
|
|
assert types(segs) == ["ctx"]
|
|
|
|
|
assert texts(segs) == ["kein Marker hier"]
|
|
|
|
|
|
|
|
|
|
def test_ins_marker_tag_format(self):
|
|
|
|
|
segs = _parse_redline_segments("§INS§neuer Text§INS§")
|
|
|
|
|
assert types(segs) == ["ins"]
|
|
|
|
|
assert texts(segs) == ["neuer Text"]
|
|
|
|
|
|
|
|
|
|
def test_del_marker_tag_format(self):
|
|
|
|
|
segs = _parse_redline_segments("§DEL§alter Text§DEL§")
|
|
|
|
|
assert types(segs) == ["del"]
|
|
|
|
|
assert texts(segs) == ["alter Text"]
|
|
|
|
|
|
|
|
|
|
def test_markdown_bold_becomes_ins(self):
|
|
|
|
|
segs = _parse_redline_segments("**eingefügt**")
|
|
|
|
|
assert types(segs) == ["ins"]
|
|
|
|
|
assert texts(segs) == ["eingefügt"]
|
|
|
|
|
|
|
|
|
|
def test_markdown_strikethrough_becomes_del(self):
|
|
|
|
|
segs = _parse_redline_segments("~~gestrichen~~")
|
|
|
|
|
assert types(segs) == ["del"]
|
|
|
|
|
assert texts(segs) == ["gestrichen"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Kontext + Marker gemischt
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class TestMixedContent:
|
|
|
|
|
def test_ctx_ins_ctx(self):
|
|
|
|
|
segs = _parse_redline_segments("§ 3 Abs. 2 §INS§verpflichtend§INS§ ab 2026")
|
|
|
|
|
assert types(segs) == ["ctx", "ins", "ctx"]
|
|
|
|
|
assert texts(segs)[1] == "verpflichtend"
|
|
|
|
|
|
|
|
|
|
def test_ctx_del_ins(self):
|
|
|
|
|
segs = _parse_redline_segments("Text §DEL§alt§DEL§§INS§neu§INS§ Ende")
|
|
|
|
|
assert types(segs) == ["ctx", "del", "ins", "ctx"]
|
|
|
|
|
|
|
|
|
|
def test_markdown_mixed(self):
|
|
|
|
|
segs = _parse_redline_segments("Vor ~~weg~~ und **rein** nach")
|
|
|
|
|
assert types(segs) == ["ctx", "del", "ctx", "ins", "ctx"]
|
|
|
|
|
|
|
|
|
|
def test_both_formats_in_one_string(self):
|
|
|
|
|
segs = _parse_redline_segments("§DEL§raus§DEL§ und **rein**")
|
|
|
|
|
assert "del" in types(segs)
|
|
|
|
|
assert "ins" in types(segs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Edge-Cases
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class TestEdgeCases:
|
|
|
|
|
def test_empty_ins_marker(self):
|
|
|
|
|
segs = _parse_redline_segments("§INS§§INS§")
|
|
|
|
|
# Leerer ins-Marker bleibt ein ins-Segment mit leerem Text
|
|
|
|
|
assert any(s["type"] == "ins" for s in segs)
|
|
|
|
|
|
|
|
|
|
def test_empty_del_marker(self):
|
|
|
|
|
segs = _parse_redline_segments("§DEL§§DEL§")
|
|
|
|
|
assert any(s["type"] == "del" for s in segs)
|
|
|
|
|
|
|
|
|
|
def test_unbalanced_marker_treated_as_ctx(self):
|
|
|
|
|
# Nur ein §INS§ ohne schließenden Partner → kein ins-Segment
|
|
|
|
|
segs = _parse_redline_segments("§INS§unvollständig")
|
|
|
|
|
assert all(s["type"] == "ctx" for s in segs)
|
|
|
|
|
|
|
|
|
|
def test_marker_with_whitespace_only(self):
|
|
|
|
|
segs = _parse_redline_segments("§INS§ §INS§")
|
|
|
|
|
ins_segs = [s for s in segs if s["type"] == "ins"]
|
|
|
|
|
assert len(ins_segs) == 1
|
|
|
|
|
assert ins_segs[0]["text"].strip() == ""
|
|
|
|
|
|
|
|
|
|
def test_multiple_ins_markers(self):
|
|
|
|
|
segs = _parse_redline_segments("§INS§A§INS§ und §INS§B§INS§")
|
|
|
|
|
ins_texts = [s["text"] for s in segs if s["type"] == "ins"]
|
|
|
|
|
assert ins_texts == ["A", "B"]
|
|
|
|
|
|
|
|
|
|
def test_multiple_del_markers(self):
|
|
|
|
|
segs = _parse_redline_segments("§DEL§X§DEL§ und §DEL§Y§DEL§")
|
|
|
|
|
del_texts = [s["text"] for s in segs if s["type"] == "del"]
|
|
|
|
|
assert del_texts == ["X", "Y"]
|
|
|
|
|
|
|
|
|
|
def test_no_empty_ctx_segments(self):
|
|
|
|
|
# Leere ctx-Segmente sollen nicht in der Ergebnisliste auftauchen
|
|
|
|
|
segs = _parse_redline_segments("§INS§nur Marker§INS§")
|
|
|
|
|
ctx_segs = [s for s in segs if s["type"] == "ctx"]
|
|
|
|
|
# Leere ctx-Strings dürfen nicht enthalten sein
|
|
|
|
|
for seg in ctx_segs:
|
|
|
|
|
assert seg["text"] != ""
|
|
|
|
|
|
|
|
|
|
def test_newline_inside_marker(self):
|
|
|
|
|
segs = _parse_redline_segments("§INS§Zeile 1\nZeile 2§INS§")
|
|
|
|
|
ins_segs = [s for s in segs if s["type"] == "ins"]
|
|
|
|
|
assert len(ins_segs) == 1
|
|
|
|
|
assert "Zeile 1" in ins_segs[0]["text"]
|
|
|
|
|
|
|
|
|
|
def test_long_realistic_redline(self):
|
|
|
|
|
text = (
|
|
|
|
|
"Die Gemeinde §DEL§soll§DEL§ §INS§muss§INS§ bis zum "
|
|
|
|
|
"§DEL§31.12.2026§DEL§ §INS§30.06.2025§INS§ einen Plan vorlegen."
|
|
|
|
|
)
|
|
|
|
|
segs = _parse_redline_segments(text)
|
|
|
|
|
del_texts = [s["text"] for s in segs if s["type"] == "del"]
|
|
|
|
|
ins_texts = [s["text"] for s in segs if s["type"] == "ins"]
|
|
|
|
|
assert "soll" in del_texts
|
|
|
|
|
assert "muss" in ins_texts
|
|
|
|
|
assert "31.12.2026" in del_texts
|
|
|
|
|
assert "30.06.2025" in ins_texts
|
2026-04-28 08:39:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── build_pdf_href Tests (#134 Coverage-Backfill) ───────────────────────────
|
|
|
|
|
|
|
|
|
|
class TestBuildPdfHref:
|
|
|
|
|
"""Tests fuer build_pdf_href: rekonstruiert PDF-URLs aus Zitat-Metadaten,
|
|
|
|
|
bevorzugt die explizite url, faellt auf WAHLPROGRAMME-Lookup zurueck."""
|
|
|
|
|
|
|
|
|
|
def test_explicit_url_passed_through(self):
|
|
|
|
|
from app.redline_utils import build_pdf_href
|
|
|
|
|
zitat = {"url": "/api/wahlprogramm-cite?pid=cdu-nrw-2022&seite=15"}
|
|
|
|
|
assert build_pdf_href(zitat) == "/api/wahlprogramm-cite?pid=cdu-nrw-2022&seite=15"
|
|
|
|
|
|
|
|
|
|
def test_empty_url_falls_back_to_quelle_lookup(self):
|
refactor(programme): WAHLPROGRAMME → programme.PROGRAMME konsolidiert (#222)
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).
2026-05-09 00:37:35 +02:00
|
|
|
"""Ohne url muss die quelle reconstruiert werden via programme.PROGRAMME."""
|
2026-04-28 08:39:05 +02:00
|
|
|
from app.redline_utils import build_pdf_href
|
refactor(programme): WAHLPROGRAMME → programme.PROGRAMME konsolidiert (#222)
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).
2026-05-09 00:37:35 +02:00
|
|
|
from app.programme import all_programme
|
|
|
|
|
prog = next(
|
|
|
|
|
(p for p in all_programme() if p.get("titel")),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
if prog is None:
|
|
|
|
|
pytest.skip("Kein Programm-Eintrag mit titel verfügbar")
|
|
|
|
|
titel = prog["titel"]
|
2026-04-28 08:39:05 +02:00
|
|
|
zitat = {
|
|
|
|
|
"quelle": f"{titel} · S. 42",
|
|
|
|
|
"text": "Wir wollen die Energiewende",
|
|
|
|
|
"url": "",
|
|
|
|
|
}
|
|
|
|
|
href = build_pdf_href(zitat)
|
|
|
|
|
assert "/api/wahlprogramm-cite" in href
|
|
|
|
|
assert "seite=42" in href
|
|
|
|
|
assert "#page=42" in href # URL-Hash fuer Browser-PDF-Viewer
|
|
|
|
|
|
|
|
|
|
def test_no_seitenzahl_returns_empty(self):
|
|
|
|
|
from app.redline_utils import build_pdf_href
|
|
|
|
|
zitat = {"quelle": "Irgendein Programm ohne Seite", "text": "x", "url": ""}
|
|
|
|
|
assert build_pdf_href(zitat) == ""
|
|
|
|
|
|
|
|
|
|
def test_unmatched_quelle_returns_empty(self):
|
|
|
|
|
from app.redline_utils import build_pdf_href
|
|
|
|
|
zitat = {
|
|
|
|
|
"quelle": "Erfundenes Programm 1995, S. 1",
|
|
|
|
|
"text": "x",
|
|
|
|
|
"url": "",
|
|
|
|
|
}
|
|
|
|
|
assert build_pdf_href(zitat) == ""
|
|
|
|
|
|
|
|
|
|
def test_query_uses_first_5_words_of_text(self):
|
|
|
|
|
from app.redline_utils import build_pdf_href
|
refactor(programme): WAHLPROGRAMME → programme.PROGRAMME konsolidiert (#222)
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).
2026-05-09 00:37:35 +02:00
|
|
|
from app.programme import all_programme
|
|
|
|
|
prog = next((p for p in all_programme() if p.get("titel")), None)
|
|
|
|
|
if prog is None:
|
|
|
|
|
pytest.skip("Kein Programm-Eintrag mit titel verfügbar")
|
|
|
|
|
titel = prog["titel"]
|
2026-04-28 08:39:05 +02:00
|
|
|
zitat = {
|
|
|
|
|
"quelle": f"{titel} · S. 5",
|
|
|
|
|
"text": "Eins zwei drei vier fünf sechs sieben",
|
|
|
|
|
"url": "",
|
|
|
|
|
}
|
|
|
|
|
href = build_pdf_href(zitat)
|
|
|
|
|
# max. 5 Worte → "sechs sieben" muessen im Query fehlen
|
|
|
|
|
assert "sechs" not in href
|
|
|
|
|
assert "sieben" not in href
|
|
|
|
|
# erste fuenf Wortteile sollten kodiert in q= auftauchen
|
|
|
|
|
assert "Eins" in href or "Eins" in href.replace("+", " ")
|
|
|
|
|
|
|
|
|
|
def test_handles_seite_with_comma_separator(self):
|
|
|
|
|
"""Quelle 'Titel, S. 42' (Komma) muss genauso parsen wie '· S. 42'."""
|
|
|
|
|
from app.redline_utils import build_pdf_href
|
refactor(programme): WAHLPROGRAMME → programme.PROGRAMME konsolidiert (#222)
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).
2026-05-09 00:37:35 +02:00
|
|
|
from app.programme import all_programme
|
|
|
|
|
prog = next((p for p in all_programme() if p.get("titel")), None)
|
|
|
|
|
if prog is None:
|
|
|
|
|
pytest.skip("Kein Programm-Eintrag mit titel verfügbar")
|
|
|
|
|
titel = prog["titel"]
|
2026-04-28 08:39:05 +02:00
|
|
|
zitat = {"quelle": f"{titel}, S. 17", "text": "x", "url": ""}
|
|
|
|
|
href = build_pdf_href(zitat)
|
|
|
|
|
assert "seite=17" in href
|