"""Unit-Tests für app.marker — Heuchelei + Konsistenz.""" import pytest try: from app.marker import ( heuchelei_score, opportunismus_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 # ─── opportunismus_score ──────────────────────────────────────────────────── class TestOpportunismusScore: """opportunismus_score liefert WP-Score wenn Fraktion JA-stimmt aber WP<3.""" def test_score_low_returns_score(self): fs = [{"fraktion": "AfD", "wahlprogramm": {"score": 1.0}}] assert opportunismus_score("AfD", fs) == 1.0 def test_score_zero_returns_score(self): fs = [{"fraktion": "GRÜNE", "wahlprogramm": {"score": 0.0}}] assert opportunismus_score("GRÜNE", fs) == 0.0 def test_score_at_threshold_returns_none(self): # 3.0 ist genau Schwelle, sollte NICHT greifen (wir wollen < 3) fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 3.0}}] assert opportunismus_score("CDU", fs) is None def test_score_above_returns_none(self): fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 5.0}}] assert opportunismus_score("CDU", fs) is None def test_unknown_fraktion_returns_none(self): fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 1.0}}] assert opportunismus_score("AfD", fs) is None def test_empty_list_returns_none(self): assert opportunismus_score("CDU", []) is None def test_none_input_returns_none(self): assert opportunismus_score("CDU", None) is None def test_missing_score_returns_none(self): fs = [{"fraktion": "CDU", "wahlprogramm": {}}] assert opportunismus_score("CDU", fs) is None def test_custom_threshold(self): fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 4.0}}] # threshold=5 → 4 < 5 → opportunism greift assert opportunismus_score("CDU", fs, threshold=5.0) == 4.0 # threshold=3 → 4 ≥ 3 → kein opportunism assert opportunismus_score("CDU", fs, threshold=3.0) is None # ─── 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"