mitai-jinkendo/tests/backend/test_phase3_logic_evaluator.py
Lars 2ce0874dcb
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
feat: Phase 3 - Logic Nodes + Conditional Branching
Backend:
- logic_evaluator.py (NEU, 307 Zeilen): Deterministischer Logic Evaluator
  - Vergleichsoperatoren: EQ, NEQ, IN, NOT_IN, GT, LT, GTE, LTE, CONTAINS
  - Logische Operatoren: AND, OR, NOT mit Verschachtelung
  - Resolve signal references (node_id.question_type)
  - Error handling für UNCLEAR/INVALID/NOT_DECIDABLE Signale

- workflow_executor.py (ERWEITERT):
  - execute_logic_node(): Bedingungen evaluieren, Pfade aktivieren/deaktivieren
  - execute_workflow(): BFS-Traversierung mit Edge-Activation statt Sequential
  - _apply_fallback(): 4 Fallback-Strategien (CONSERVATIVE_SKIP, DEFAULT_PATH, UNCERTAINTY_PATH, DOCUMENT_ONLY)
  - _has_active_incoming_edge(): Prüft ob Node erreichbar ist
  - _get_edges_by_label(): Findet then/else/uncertainty Pfade

- workflow_models.py (ERWEITERT):
  - LogicOperator.CONTAINS hinzugefügt

- version.py: 0.9k → 0.9l, workflow 0.3.0 → 0.4.0

Tests:
- test_phase3_logic_evaluator.py (NEU): 20 Unit Tests (alle passing)
  - Comparison operators (EQ, NEQ, IN, GT, LT, CONTAINS)
  - Logical operators (AND, OR, NOT)
  - Nested expressions
  - Error handling (missing refs, UNCLEAR/INVALID signals)

- test_phase2_workflow_executor.py (AKTUALISIERT): 11 Tests (alle passing)
  - execute_node() graph parameter hinzugefügt (Phase 3 requirement)
  - test_execute_node_unknown_type: logic → join (logic jetzt implementiert)

- test_phase3_workflow_branching.py (NEU): Integration Tests vorbereitet
  - Erfordert vollständige DB-Mock-Strategie (wird in E2E-Test nachgeholt)

Phase 2 Backward Compatibility:  Alle Phase 2 Tests bestehen weiterhin

Konzept: .claude/task/Workflow_engine_prompting_engine/konzept_workflow_engine_konsolidated.md
Anforderungsanalyse: .claude/task/Workflow_engine_prompting_engine/phase3_anforderungsanalyse.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 08:02:22 +02:00

721 lines
21 KiB
Python

"""
Unit Tests für logic_evaluator.py (Phase 3)
Run with: PYTHONPATH=./backend pytest tests/backend/test_phase3_logic_evaluator.py -v
"""
import pytest
from logic_evaluator import (
evaluate_logic_expression,
resolve_signal_reference,
compare_values
)
from workflow_models import (
LogicExpression,
LogicOperator,
NormalizedSignal,
SignalStatus,
NodeExecutionState,
NodeStatus
)
# ── Comparison Operator Tests ──────────────────────────────────────────────────
def test_evaluate_eq_true():
"""Test: EQ operator - match"""
expression = LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="decrease"
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is True
assert error is None
def test_evaluate_eq_false():
"""Test: EQ operator - no match"""
expression = LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="increase"
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is False
assert error is None
def test_evaluate_neq():
"""Test: NEQ operator"""
expression = LogicExpression(
operator=LogicOperator.NEQ,
ref="body.relevanz",
value="stable"
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is True
assert error is None
def test_evaluate_in_true():
"""Test: IN operator - value in list"""
expression = LogicExpression(
operator=LogicOperator.IN,
ref="body.prioritaet",
value=["hoch", "mittel"]
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="prioritaet",
raw_value="hoch",
normalized_value="hoch",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is True
assert error is None
def test_evaluate_in_false():
"""Test: IN operator - value not in list"""
expression = LogicExpression(
operator=LogicOperator.IN,
ref="body.prioritaet",
value=["hoch", "mittel"]
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="prioritaet",
raw_value="niedrig",
normalized_value="niedrig",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is False
assert error is None
def test_evaluate_gt():
"""Test: GT operator - greater than"""
expression = LogicExpression(
operator=LogicOperator.GT,
ref="body.score",
value=50
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="score",
raw_value="75",
normalized_value="75",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is True
assert error is None
def test_evaluate_lt():
"""Test: LT operator - less than"""
expression = LogicExpression(
operator=LogicOperator.LT,
ref="body.score",
value=50
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="score",
raw_value="25",
normalized_value="25",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is True
assert error is None
def test_evaluate_contains_string():
"""Test: CONTAINS operator - string contains substring"""
expression = LogicExpression(
operator=LogicOperator.CONTAINS,
ref="body.kategorie",
value="Gewicht"
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="kategorie",
raw_value="Gewichtsverlust positiv",
normalized_value="Gewichtsverlust positiv",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is True
assert error is None
# ── Logical Operator Tests ──────────────────────────────────────────────────
def test_evaluate_and_both_true():
"""Test: AND operator - both operands true"""
expression = LogicExpression(
operator=LogicOperator.AND,
operands=[
LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="decrease"
),
LogicExpression(
operator=LogicOperator.EQ,
ref="activity.intensitaet",
value="hoch"
)
]
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
),
"activity": NodeExecutionState(
node_id="activity",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="intensitaet",
raw_value="hoch",
normalized_value="hoch",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is True
assert error is None
def test_evaluate_and_one_false():
"""Test: AND operator - one operand false"""
expression = LogicExpression(
operator=LogicOperator.AND,
operands=[
LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="decrease"
),
LogicExpression(
operator=LogicOperator.EQ,
ref="activity.intensitaet",
value="niedrig"
)
]
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
),
"activity": NodeExecutionState(
node_id="activity",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="intensitaet",
raw_value="hoch",
normalized_value="hoch",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is False
assert error is None
def test_evaluate_or_one_true():
"""Test: OR operator - one operand true"""
expression = LogicExpression(
operator=LogicOperator.OR,
operands=[
LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="decrease"
),
LogicExpression(
operator=LogicOperator.EQ,
ref="activity.intensitaet",
value="niedrig"
)
]
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
),
"activity": NodeExecutionState(
node_id="activity",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="intensitaet",
raw_value="hoch",
normalized_value="hoch",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is True
assert error is None
def test_evaluate_or_both_false():
"""Test: OR operator - both operands false"""
expression = LogicExpression(
operator=LogicOperator.OR,
operands=[
LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="increase"
),
LogicExpression(
operator=LogicOperator.EQ,
ref="activity.intensitaet",
value="niedrig"
)
]
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
),
"activity": NodeExecutionState(
node_id="activity",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="intensitaet",
raw_value="hoch",
normalized_value="hoch",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is False
assert error is None
def test_evaluate_not():
"""Test: NOT operator - negation"""
expression = LogicExpression(
operator=LogicOperator.NOT,
operands=[
LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="increase"
)
]
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is True # NOT (decrease == increase) = NOT False = True
assert error is None
# ── Nested Expressions Tests ──────────────────────────────────────────────────
def test_evaluate_nested_and_or():
"""Test: Nested expression - (A AND B) OR C"""
expression = LogicExpression(
operator=LogicOperator.OR,
operands=[
LogicExpression(
operator=LogicOperator.AND,
operands=[
LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="decrease"
),
LogicExpression(
operator=LogicOperator.EQ,
ref="activity.intensitaet",
value="niedrig"
)
]
),
LogicExpression(
operator=LogicOperator.EQ,
ref="nutrition.defizit",
value="hoch"
)
]
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
),
"activity": NodeExecutionState(
node_id="activity",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="intensitaet",
raw_value="hoch", # FALSE für AND-Teil
normalized_value="hoch",
status=SignalStatus.VALID
)
]
),
"nutrition": NodeExecutionState(
node_id="nutrition",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="defizit",
raw_value="hoch", # TRUE für OR-Teil
normalized_value="hoch",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
# (decrease AND niedrig) OR hoch = (True AND False) OR True = False OR True = True
assert result is True
assert error is None
# ── Error Handling Tests ──────────────────────────────────────────────────
def test_evaluate_missing_node():
"""Test: Error handling - node not found"""
expression = LogicExpression(
operator=LogicOperator.EQ,
ref="missing_node.relevanz",
value="decrease"
)
context = {
"node_results": {}
}
result, error = evaluate_logic_expression(expression, context)
assert result is False
assert error is not None
assert "not found" in error.lower()
def test_evaluate_missing_signal():
"""Test: Error handling - signal not found in node"""
expression = LogicExpression(
operator=LogicOperator.EQ,
ref="body.missing_signal",
value="decrease"
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="decrease",
normalized_value="decrease",
status=SignalStatus.VALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is False
assert error is not None
assert "not found" in error.lower()
def test_evaluate_unclear_signal():
"""Test: Error handling - signal has UNCLEAR status"""
expression = LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="decrease"
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="maybe",
normalized_value="unklar",
status=SignalStatus.UNCLEAR
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is False
assert error is not None
assert "unclear" in error.lower() or "status" in error.lower()
def test_evaluate_invalid_signal():
"""Test: Error handling - signal has INVALID status"""
expression = LogicExpression(
operator=LogicOperator.EQ,
ref="body.relevanz",
value="decrease"
)
context = {
"node_results": {
"body": NodeExecutionState(
node_id="body",
status=NodeStatus.EXECUTED,
normalized_signals=[
NormalizedSignal(
question_type="relevanz",
raw_value="invalid_value",
normalized_value=None,
status=SignalStatus.INVALID
)
]
)
}
}
result, error = evaluate_logic_expression(expression, context)
assert result is False
assert error is not None
assert "invalid" in error.lower() or "status" in error.lower()
def test_compare_values_gt_non_numeric():
"""Test: Error handling - GT with non-numeric values"""
result, error = compare_values(LogicOperator.GT, "text", 50)
assert result is False
assert error is not None
assert "cannot compare" in error.lower()
def test_compare_values_in_non_list():
"""Test: Error handling - IN with non-list right value"""
result, error = compare_values(LogicOperator.IN, "value", "not_a_list")
assert result is False
assert error is not None
assert "requires list" in error.lower()
if __name__ == "__main__":
pytest.main([__file__, "-v"])