gwoe-antragspruefer/tests/test_marker.py
Dotty Dotter 9498ca4b97 refactor + tests: marker.py + pm_render.py mit 56 Unit-Tests
Logik aus dem Jinja-Template (Heuchelei-Marker, Konsistenz-Block,
decisive-Outcome-Selection) in app/marker.py extrahiert. Template
ruft die drei Helper als Jinja-Globals auf. Damit ist die Logik
testbar ohne Render-Kontext.

Plus: app/pm_render.py als Python-Spiegelbild des JS-Mini-Markdown-
Renderers in aktuelle-themen.html — fuer Tests und potenzielle
Server-side-Render-Optionen (z.B. PM-Mail).

Tests:
- tests/test_marker.py (35 Cases): heuchelei_score, decisive_outcome,
  consistency_state inkl. Multi-Vote, ambivalente Empfehlung,
  Edge-Cases.
- tests/test_pm_render.py (21 Cases): Bold, Italic, Listen,
  HTML-Escape, Paragraph-Splitting, snake_case-Schutz.

Refs: ADR 0010, ADR 0011

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:44:12 +02:00

193 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Unit-Tests für app.marker — Heuchelei + Konsistenz."""
import pytest
try:
from app.marker import (
heuchelei_score,
decisive_outcome,
consistency_state,
DECISIVE_OUTCOMES,
)
_HAS_MARKER = True
except ImportError:
_HAS_MARKER = False
pytestmark = pytest.mark.skipif(not _HAS_MARKER, reason="app.marker nicht importierbar")
# ─── heuchelei_score ────────────────────────────────────────────────────────
class TestHeucheleiScore:
"""heuchelei_score liefert WP-Score wenn Fraktion NEIN-stimmt aber WP≥7."""
def test_score_high_returns_score(self):
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 7.0}}]
assert heuchelei_score("CDU", fs) == 7.0
def test_score_at_threshold_returns_score(self):
# 7.0 ist genau Schwelle und sollte greifen
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 7.0}}]
assert heuchelei_score("CDU", fs, threshold=7.0) == 7.0
def test_score_below_returns_none(self):
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 6.9}}]
assert heuchelei_score("CDU", fs) is None
def test_score_above_default_returns_score(self):
fs = [{"fraktion": "FDP", "wahlprogramm": {"score": 8.0}}]
assert heuchelei_score("FDP", fs) == 8.0
def test_unknown_fraktion_returns_none(self):
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 9.0}}]
assert heuchelei_score("AfD", fs) is None
def test_empty_list_returns_none(self):
assert heuchelei_score("CDU", []) is None
def test_none_input_returns_none(self):
assert heuchelei_score("CDU", None) is None
def test_missing_score_field_returns_none(self):
fs = [{"fraktion": "CDU", "wahlprogramm": {}}]
assert heuchelei_score("CDU", fs) is None
def test_score_as_int_handled(self):
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 7}}]
assert heuchelei_score("CDU", fs) == 7.0
def test_invalid_score_returns_none(self):
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": "not-a-number"}}]
assert heuchelei_score("CDU", fs) is None
def test_custom_threshold(self):
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 5.0}}]
# bei threshold=4 greift's
assert heuchelei_score("CDU", fs, threshold=4.0) == 5.0
# bei threshold=6 nicht
assert heuchelei_score("CDU", fs, threshold=6.0) is None
def test_finds_first_match(self):
"""Wenn dieselbe Fraktion mehrfach in der Liste ist, wird der erste Treffer genommen."""
fs = [
{"fraktion": "CDU", "wahlprogramm": {"score": 8.0}},
{"fraktion": "CDU", "wahlprogramm": {"score": 3.0}},
]
assert heuchelei_score("CDU", fs) == 8.0
# ─── decisive_outcome ───────────────────────────────────────────────────────
class TestDecisiveOutcome:
"""decisive_outcome wählt erstes Vote mit angenommen|abgelehnt|bestätigt."""
def test_single_decisive(self):
votes = [{"ergebnis": "angenommen"}]
assert decisive_outcome(votes) == "angenommen"
def test_single_abgelehnt(self):
votes = [{"ergebnis": "abgelehnt"}]
assert decisive_outcome(votes) == "abgelehnt"
def test_single_bestaetigt(self):
votes = [{"ergebnis": "bestätigt"}]
assert decisive_outcome(votes) == "bestätigt"
def test_skip_ueberwiesen_to_decisive(self):
"""Bei Überweisung → angenommen sollte angenommen gewählt werden."""
votes = [
{"ergebnis": "überwiesen"},
{"ergebnis": "angenommen"},
]
assert decisive_outcome(votes) == "angenommen"
def test_only_ueberwiesen_returns_none(self):
votes = [{"ergebnis": "überwiesen"}]
assert decisive_outcome(votes) is None
def test_only_zurueckgezogen_returns_none(self):
votes = [{"ergebnis": "zurückgezogen"}]
assert decisive_outcome(votes) is None
def test_empty_list_returns_none(self):
assert decisive_outcome([]) is None
def test_none_input_returns_none(self):
assert decisive_outcome(None) is None
def test_first_decisive_wins(self):
"""Wenn beide Votes definitiv sind, wird das erste gewählt."""
votes = [
{"ergebnis": "angenommen"},
{"ergebnis": "abgelehnt"},
]
assert decisive_outcome(votes) == "angenommen"
def test_case_insensitive(self):
votes = [{"ergebnis": "Angenommen"}]
assert decisive_outcome(votes) == "angenommen"
def test_decisive_outcomes_constant(self):
"""Konstante DECISIVE_OUTCOMES enthält genau die drei Werte."""
assert DECISIVE_OUTCOMES == frozenset({"angenommen", "abgelehnt", "bestätigt"})
# ─── consistency_state ──────────────────────────────────────────────────────
class TestConsistencyState:
"""consistency_state vergleicht GWÖ-Empfehlung × Beschluss."""
def test_supports_passed_aligned(self):
votes = [{"ergebnis": "angenommen"}]
assert consistency_state("Uneingeschränkt unterstützen", votes) == "aligned"
def test_supports_failed_conflict(self):
votes = [{"ergebnis": "abgelehnt"}]
assert consistency_state("Uneingeschränkt unterstützen", votes) == "conflict"
def test_rejects_failed_aligned(self):
votes = [{"ergebnis": "abgelehnt"}]
assert consistency_state("Ablehnen", votes) == "aligned"
def test_rejects_passed_conflict(self):
votes = [{"ergebnis": "angenommen"}]
assert consistency_state("Ablehnen", votes) == "conflict"
def test_supports_with_changes_passed_aligned(self):
"""„Unterstützen mit Änderungen" enthält 'unterstützen' → rec_supports."""
votes = [{"ergebnis": "angenommen"}]
assert consistency_state("Unterstützen mit Änderungen", votes) == "aligned"
def test_befuerworten_synonym(self):
votes = [{"ergebnis": "angenommen"}]
assert consistency_state("Befürworten", votes) == "aligned"
def test_ambivalent_returns_none(self):
"""„Mit Vorbehalt" enthält weder 'unterstützen' noch 'ablehnen'."""
votes = [{"ergebnis": "angenommen"}]
assert consistency_state("Mit Vorbehalt", votes) is None
def test_ueberwiesen_returns_none(self):
votes = [{"ergebnis": "überwiesen"}]
assert consistency_state("Uneingeschränkt unterstützen", votes) is None
def test_no_votes_returns_none(self):
assert consistency_state("Uneingeschränkt unterstützen", []) is None
def test_no_verdict_returns_none(self):
votes = [{"ergebnis": "angenommen"}]
assert consistency_state("", votes) is None
assert consistency_state(None, votes) is None
def test_multivote_picks_decisive(self):
"""Bei Überweisung → Endabstimmung wird die Endabstimmung gewertet."""
votes = [
{"ergebnis": "überwiesen"},
{"ergebnis": "abgelehnt"},
]
assert consistency_state("Uneingeschränkt unterstützen", votes) == "conflict"
def test_bestaetigt_treated_as_passed(self):
votes = [{"ergebnis": "bestätigt"}]
assert consistency_state("Uneingeschränkt unterstützen", votes) == "aligned"
assert consistency_state("Ablehnen", votes) == "conflict"