gwoe-antragspruefer/tests/test_protokoll_parsers_hb.py
Dotty Dotter d9ae0b0db8 feat(#153): HB-Parser produktiv — Bremer Beschlussprotokolle (Status-Only)
Bremen publiziert wie Hessen nur Beschlussprotokolle (TOPs + Status-Saetze),
KEINE Wortprotokolle mit Vote-Block. Daher minimaler Parser:
- Drucksache + Status (angenommen/abgelehnt/ueberwiesen)
- Vote-Listen bleiben leer (HB hat keine Fraktions-Detail)

Anchor-Regex: "Die Buergerschaft (Landtag|Stadtbuergerschaft) <verb> <rest> <terminator>"
Verb-Mapping:
- "lehnt ... ab" → abgelehnt
- "stimmt ... zu" → angenommen
- "beschliesst ..." → angenommen
- "verabschiedet ..." → angenommen
- "verweist|ueberweist|leitet" → ueberwiesen
- "nimmt ... Kenntnis" → uebersprungen (kein Vote)

Drucksachen-Aufloesung: erst Inline-Form "(21/N)", dann Block-Form
"Drucksache 21/N" rueckwaerts vom Anchor.

URL-Pattern (verifiziert WP21 Sitzung 33 Land):
https://www.bremische-buergerschaft.de/dokumente/wp21/land/protokoll/b21l{n4}.pdf

Cron unterstuetzt jetzt {n4}-Platzhalter (4-stellig). HB Land WP21
ingestiert via direktes URL-Probing (b21l0001.pdf … b21l9999.pdf).
Stadtbuergerschaft (b21s*) als Folge-Issue.

Tests: 21 HB-Tests, Verifikation S33 → 20 Beschluesse extrahiert.
Stand: 8 produktive Parser (NRW, BUND, BE, HH, TH, HE, SH, HB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:41:40 +02:00

108 lines
3.7 KiB
Python

"""Tests fuer app/protokoll_parsers/hb.py — HB Beschlussprotokoll-Parser (#153).
HB ist (wie HE) ein Beschlussprotokoll-Parser: Status-only, keine Vote-Detail.
Stichprobe-getestet gegen WP21 Sitzung 33 (Bremen Landtag).
"""
from __future__ import annotations
import pytest
from app.protokoll_parsers.hb import (
_classify_status,
_normalize_text,
_resolve_drucksache_hb,
ANCHOR_RE,
DS_INLINE_RE,
DS_BLOCK_RE,
ALLE_FRAKTIONEN_HB,
)
class TestClassifyStatus:
def test_lehnt_ab_to_abgelehnt(self):
assert _classify_status("lehnt", "den Antrag", " ab.") == "abgelehnt"
def test_stimmt_zu_to_angenommen(self):
assert _classify_status("stimmt", "dem Antrag", " zu.") == "angenommen"
def test_beschliesst_to_angenommen(self):
assert _classify_status("beschließt", "das Gesetz", ".") == "angenommen"
def test_verabschiedet_to_angenommen(self):
assert _classify_status("verabschiedet", "den Haushalt", ".") == "angenommen"
def test_nimmt_kenntnis_skipped(self):
assert _classify_status(
"nimmt", "von der Mitteilung Kenntnis", "."
) == "kenntnis"
def test_ueberweist_to_ueberwiesen(self):
assert _classify_status("überweist", "den Antrag", ".") == "überwiesen"
class TestNormalizeText:
def test_collapses_whitespace(self):
assert _normalize_text("a b\n\tc") == "a b c"
def test_repairs_soft_hyphenation(self):
assert _normalize_text("Bürger- schaft") == "Bürgerschaft"
class TestAnchorRegex:
def test_matches_landtag_lehnt(self):
text = "Die Bürgerschaft (Landtag) lehnt den Antrag ab."
m = ANCHOR_RE.search(text)
assert m and m.group("verb") == "lehnt"
def test_matches_stadtbuergerschaft_stimmt(self):
text = "Die Bürgerschaft (Stadtbürgerschaft) stimmt dem Antrag zu."
m = ANCHOR_RE.search(text)
assert m and m.group("verb") == "stimmt"
def test_matches_beschliesst_gesetz(self):
text = "Die Bürgerschaft (Landtag) beschließt das Gesetz."
m = ANCHOR_RE.search(text)
assert m and m.group("verb") == "beschließt"
def test_matches_nimmt_kenntnis(self):
text = "Die Bürgerschaft (Landtag) nimmt von der Antwort Kenntnis."
m = ANCHOR_RE.search(text)
assert m and m.group("verb") == "nimmt"
class TestDrucksacheRegex:
def test_inline_form_with_parens(self):
m = DS_INLINE_RE.search("lehnt den Änderungsantrag (21/1688) ab")
assert m and m.group(1) == "21/1688"
def test_block_form_with_drucksache_label(self):
m = DS_BLOCK_RE.search("Antrag der Fraktion (Drucksache 21/1234)")
assert m and m.group(1) == "21/1234"
class TestResolveDrucksacheHb:
def test_inline_takes_priority(self):
text = "Drucksache 21/1000 ... Die Bürgerschaft lehnt (21/2000) ab."
rest = "den Antrag (21/2000)"
anchor = text.index("Die Bürgerschaft")
assert _resolve_drucksache_hb(text, anchor, rest) == "21/2000"
def test_block_form_used_when_no_inline(self):
text = "Drucksache 21/1500. Die Bürgerschaft lehnt den Antrag ab."
rest = "den Antrag"
anchor = text.index("Die Bürgerschaft")
assert _resolve_drucksache_hb(text, anchor, rest) == "21/1500"
def test_returns_none_when_no_drucksache(self):
assert _resolve_drucksache_hb(
"Die Bürgerschaft lehnt den Antrag ab.", 0, "den Antrag"
) is None
class TestConstants:
def test_all_fraktionen_set(self):
# WP21-HB Konstellation: SPD-GRÜNE-LINKE Koalition + CDU/FDP Opposition + BIW
assert "SPD" in ALLE_FRAKTIONEN_HB
assert "GRÜNE" in ALLE_FRAKTIONEN_HB
assert "CDU" in ALLE_FRAKTIONEN_HB