Backend: - question_augmenter.py (290 Zeilen): Hybrid-Modell für Fragenergänzungen * merge_question_augmentations(): Knotengebundene Fragen überschreiben Prompt-Defaults * augment_prompt_with_questions(): Markdown-formatierte Fragenergänzung * parse_question_augmentations_from_jsonb(): JSONB → QuestionAugmentation[] - result_container_parser.py (250 Zeilen): Markdown-Sektionen-Parsing * parse_result_container(): Extrahiert Analysekern, Entscheidungsanteil, Begründungsanker * validate_decision_signal(): Normalisierung gegen answer_spectrum * Fallback-Parsing bei unstrukturierten Antworten - routers/workflow_questions.py (236 Zeilen): CRUD für workflow_question_catalog * GET /api/workflow/questions (mit active_only Filter) * POST/PUT/DELETE (Admin only, Soft Delete) - prompt_executor.py: Integration in execute_base_prompt() * Fragenergänzung vor LLM-Call (wenn node_questions oder catalog vorhanden) * Result-Container-Parsing nach LLM-Response - main.py: Router-Registrierung (workflow_questions) Tests: - test_phase1_question_augmenter.py (8 Tests): Hybrid-Modell, Formatierung, JSONB-Parsing - test_phase1_result_container_parser.py (17 Tests): Sektion-Extraktion, Decision-Parsing, Validierung Alle 25 Unit-Tests bestanden. version: 0.9j (backend) module: workflow 0.2.0 Konzept: .claude/task/Workflow_engine_prompting_engine/konzept_workflow_engine_konsolidated.md (Phase 1)
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"])
|