""" 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"])