235 lines
6.1 KiB
Python
235 lines
6.1 KiB
Python
|
|
"""
|
||
|
|
Unit Tests für result_container_parser.py (Phase 1)
|
||
|
|
|
||
|
|
Run with: PYTHONPATH=./backend pytest tests/backend/test_phase1_result_container_parser.py -v
|
||
|
|
"""
|
||
|
|
import pytest
|
||
|
|
from result_container_parser import (
|
||
|
|
parse_result_container,
|
||
|
|
extract_section,
|
||
|
|
parse_decision_questions,
|
||
|
|
validate_decision_signal,
|
||
|
|
parse_result_container_robust
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_extract_section_basic():
|
||
|
|
"""Test: Einfache Sektion extrahieren"""
|
||
|
|
text = """
|
||
|
|
## Analyse
|
||
|
|
Das ist der Analysekern.
|
||
|
|
Mehrere Zeilen.
|
||
|
|
|
||
|
|
## Entscheidungsfragen
|
||
|
|
- Relevanz: ja
|
||
|
|
"""
|
||
|
|
result = extract_section(text, "Analyse")
|
||
|
|
assert result == "Das ist der Analysekern.\nMehrere Zeilen."
|
||
|
|
|
||
|
|
|
||
|
|
def test_extract_section_not_found():
|
||
|
|
"""Test: Nicht vorhandene Sektion"""
|
||
|
|
text = "## Analyse\nInhalt"
|
||
|
|
result = extract_section(text, "Begründung")
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_extract_section_empty():
|
||
|
|
"""Test: Leere Sektion (nur Whitespace am Ende)"""
|
||
|
|
text = "## Analyse\n\n"
|
||
|
|
result = extract_section(text, "Analyse")
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_decision_questions_basic():
|
||
|
|
"""Test: Standard-Format parsen"""
|
||
|
|
section = """
|
||
|
|
- Relevanz: ja
|
||
|
|
- Priorität: hoch
|
||
|
|
- Selektion: nein
|
||
|
|
"""
|
||
|
|
result = parse_decision_questions(section)
|
||
|
|
assert result == {
|
||
|
|
"relevanz": "ja",
|
||
|
|
"priorität": "hoch",
|
||
|
|
"selektion": "nein"
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_decision_questions_bold():
|
||
|
|
"""Test: Format mit **bold** Markup"""
|
||
|
|
section = """
|
||
|
|
- **Relevanz**: ja
|
||
|
|
- **Priorität**: hoch
|
||
|
|
"""
|
||
|
|
result = parse_decision_questions(section)
|
||
|
|
assert result == {
|
||
|
|
"relevanz": "ja",
|
||
|
|
"priorität": "hoch"
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_decision_questions_without_dash():
|
||
|
|
"""Test: Format ohne führendes Minus"""
|
||
|
|
section = """
|
||
|
|
Relevanz: ja
|
||
|
|
Priorität: hoch
|
||
|
|
"""
|
||
|
|
result = parse_decision_questions(section)
|
||
|
|
assert result == {
|
||
|
|
"relevanz": "ja",
|
||
|
|
"priorität": "hoch"
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_decision_questions_brackets():
|
||
|
|
"""Test: Format mit [Klammern]"""
|
||
|
|
section = """
|
||
|
|
- Relevanz: [ja]
|
||
|
|
- Priorität: [hoch]
|
||
|
|
"""
|
||
|
|
result = parse_decision_questions(section)
|
||
|
|
assert result == {
|
||
|
|
"relevanz": "ja",
|
||
|
|
"priorität": "hoch"
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_validate_decision_signal_exact_match():
|
||
|
|
"""Test: Exakte Übereinstimmung"""
|
||
|
|
value, status = validate_decision_signal("ja", ["ja", "nein", "unklar"])
|
||
|
|
assert value == "ja"
|
||
|
|
assert status == "valid"
|
||
|
|
|
||
|
|
|
||
|
|
def test_validate_decision_signal_normalized():
|
||
|
|
"""Test: Case-insensitive Normalisierung"""
|
||
|
|
value, status = validate_decision_signal("JA", ["ja", "nein", "unklar"])
|
||
|
|
assert value == "ja"
|
||
|
|
assert status == "normalized"
|
||
|
|
|
||
|
|
|
||
|
|
def test_validate_decision_signal_invalid():
|
||
|
|
"""Test: Ungültige Antwort"""
|
||
|
|
value, status = validate_decision_signal("vielleicht", ["ja", "nein", "unklar"])
|
||
|
|
assert value == "vielleicht"
|
||
|
|
assert status == "invalid"
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_result_container_complete():
|
||
|
|
"""Test: Vollständiger Container mit allen Sektionen"""
|
||
|
|
llm_output = """
|
||
|
|
## Analyse
|
||
|
|
Der Nutzer zeigt eine positive Gewichtsentwicklung.
|
||
|
|
Kaloriendefizit wird eingehalten.
|
||
|
|
|
||
|
|
## Entscheidungsfragen
|
||
|
|
- Relevanz: ja
|
||
|
|
- Priorität: hoch
|
||
|
|
|
||
|
|
## Begründung
|
||
|
|
Die Gewichtsabnahme ist im Zielbereich von 0.5-1% pro Woche.
|
||
|
|
"""
|
||
|
|
result = parse_result_container(llm_output)
|
||
|
|
|
||
|
|
assert result["parsing_status"] == "complete"
|
||
|
|
assert "Gewichtsentwicklung" in result["analysis_core"]
|
||
|
|
assert result["decision_signals"]["relevanz"] == "ja"
|
||
|
|
assert result["decision_signals"]["priorität"] == "hoch"
|
||
|
|
assert "Zielbereich" in result["reasoning_anchors"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_result_container_partial():
|
||
|
|
"""Test: Container ohne Begründung (partial)"""
|
||
|
|
llm_output = """
|
||
|
|
## Analyse
|
||
|
|
Analyse-Inhalt
|
||
|
|
|
||
|
|
## Entscheidungsfragen
|
||
|
|
- Relevanz: ja
|
||
|
|
"""
|
||
|
|
result = parse_result_container(llm_output)
|
||
|
|
|
||
|
|
assert result["parsing_status"] == "complete"
|
||
|
|
assert result["analysis_core"] == "Analyse-Inhalt"
|
||
|
|
assert result["decision_signals"]["relevanz"] == "ja"
|
||
|
|
assert result["reasoning_anchors"] is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_result_container_no_structure():
|
||
|
|
"""Test: Unstrukturierte Antwort (Fallback)"""
|
||
|
|
llm_output = "Einfache Textantwort ohne Strukturierung."
|
||
|
|
|
||
|
|
result = parse_result_container(llm_output)
|
||
|
|
|
||
|
|
assert result["parsing_status"] == "fallback"
|
||
|
|
assert result["analysis_core"] == "Einfache Textantwort ohne Strukturierung."
|
||
|
|
assert result["decision_signals"] == {}
|
||
|
|
assert result["reasoning_anchors"] is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_result_container_only_questions():
|
||
|
|
"""Test: Nur Entscheidungsfragen, keine Analyse (partial)"""
|
||
|
|
llm_output = """
|
||
|
|
## Entscheidungsfragen
|
||
|
|
- Relevanz: nein
|
||
|
|
- Priorität: niedrig
|
||
|
|
"""
|
||
|
|
result = parse_result_container(llm_output)
|
||
|
|
|
||
|
|
assert result["parsing_status"] == "partial"
|
||
|
|
assert result["analysis_core"] == ""
|
||
|
|
assert result["decision_signals"]["relevanz"] == "nein"
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_result_container_robust_with_warnings():
|
||
|
|
"""Test: Robuste Variante mit erwarteten Fragen"""
|
||
|
|
llm_output = """
|
||
|
|
## Analyse
|
||
|
|
Inhalt
|
||
|
|
|
||
|
|
## Entscheidungsfragen
|
||
|
|
- Relevanz: ja
|
||
|
|
"""
|
||
|
|
expected_questions = ["relevanz", "prioritaet", "selektion"]
|
||
|
|
|
||
|
|
result = parse_result_container_robust(llm_output, expected_questions)
|
||
|
|
|
||
|
|
assert "warnings" in result
|
||
|
|
assert any("Fehlende Entscheidungsfragen" in w for w in result["warnings"])
|
||
|
|
assert any("prioritaet" in w for w in result["warnings"])
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_result_container_case_insensitive():
|
||
|
|
"""Test: Case-insensitive Sektion-Matching"""
|
||
|
|
llm_output = """
|
||
|
|
## ANALYSE
|
||
|
|
Großgeschrieben
|
||
|
|
|
||
|
|
## entscheidungsfragen
|
||
|
|
- Relevanz: ja
|
||
|
|
"""
|
||
|
|
result = parse_result_container(llm_output)
|
||
|
|
|
||
|
|
assert result["parsing_status"] == "complete"
|
||
|
|
assert result["analysis_core"] == "Großgeschrieben"
|
||
|
|
assert result["decision_signals"]["relevanz"] == "ja"
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_decision_questions_mixed_formats():
|
||
|
|
"""Test: Gemischte Formate in einer Sektion"""
|
||
|
|
section = """
|
||
|
|
- **Relevanz**: ja
|
||
|
|
Priorität: hoch
|
||
|
|
- Selektion: [nein]
|
||
|
|
"""
|
||
|
|
result = parse_decision_questions(section)
|
||
|
|
|
||
|
|
assert result["relevanz"] == "ja"
|
||
|
|
assert result["priorität"] == "hoch"
|
||
|
|
assert result["selektion"] == "nein"
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
pytest.main([__file__, "-v"])
|