Critical fix: Without extra='forbid', Pydantic accepted UI format
{operator: "and", operands: [...]} as valid Condition by ignoring
unknown fields, resulting in Condition(expression=None).
With extra='forbid':
- Condition rejects unknown fields → fails
- Union tries next type → LogicExpression → success
Test Results (9/9 passed):
- Simple comparisons (eq, neq, gt, lt, in) ✅
- AND/OR combinations ✅
- Deep nesting (3+ levels) ✅
- NOT operator ✅
- All operators (eq, neq, in, not_in, gt, lt, gte, lte, and, or, not) ✅
- Legacy format (Condition wrapper) ✅
- Complex real-world scenarios ✅
Added comprehensive test suite in:
- test_condition_parsing.py (9 test cases)
- test_condition_union.py (Union resolution verification)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
247 lines
6.9 KiB
Python
247 lines
6.9 KiB
Python
"""
|
|
Test Condition Parsing - Alle Formate und Verschachtelungen
|
|
|
|
Testet ob Pydantic die verschiedenen Condition-Formate korrekt deserialisiert.
|
|
"""
|
|
import sys
|
|
import os
|
|
|
|
# Force UTF-8 encoding on Windows
|
|
if sys.platform == 'win32':
|
|
import io
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
|
|
|
sys.path.insert(0, 'backend')
|
|
|
|
from workflow_models import LogicExpression, Condition, WorkflowNode, LogicOperator
|
|
from pydantic import ValidationError
|
|
import json
|
|
|
|
def test_case(name, data, expected_type, should_fail=False):
|
|
"""Test a single condition format"""
|
|
print(f"\n{'='*60}")
|
|
print(f"TEST: {name}")
|
|
print(f"{'='*60}")
|
|
print(f"Input: {json.dumps(data, indent=2)}")
|
|
|
|
try:
|
|
# Test 1: Direct deserialization
|
|
if expected_type == LogicExpression:
|
|
result = LogicExpression(**data)
|
|
elif expected_type == Condition:
|
|
result = Condition(**data)
|
|
|
|
if should_fail:
|
|
print("❌ FAILED: Should have raised ValidationError but didn't")
|
|
return False
|
|
|
|
print(f"✅ PASSED: Deserialized as {type(result).__name__}")
|
|
print(f"Result: {result.model_dump()}")
|
|
|
|
# Test 2: As part of WorkflowNode
|
|
node_data = {
|
|
"id": "test_node",
|
|
"type": "logic",
|
|
"condition": data
|
|
}
|
|
node = WorkflowNode(**node_data)
|
|
print(f"✅ PASSED: WorkflowNode.condition type: {type(node.condition).__name__}")
|
|
|
|
return True
|
|
|
|
except ValidationError as e:
|
|
if should_fail:
|
|
print(f"✅ PASSED: Correctly raised ValidationError")
|
|
return True
|
|
else:
|
|
print(f"❌ FAILED: {e}")
|
|
return False
|
|
except Exception as e:
|
|
print(f"❌ FAILED: Unexpected error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
|
|
# ============================================================================
|
|
# Test Cases
|
|
# ============================================================================
|
|
|
|
test_results = []
|
|
|
|
# Test 1: Simple comparison (UI format - einfachster Fall)
|
|
test_results.append(test_case(
|
|
"Simple comparison (eq)",
|
|
{
|
|
"operator": "eq",
|
|
"ref": "node_1.q1",
|
|
"value": "ja"
|
|
},
|
|
LogicExpression
|
|
))
|
|
|
|
# Test 2: Simple AND (UI format - wie im Workflow)
|
|
test_results.append(test_case(
|
|
"Simple AND with 2 operands",
|
|
{
|
|
"operator": "and",
|
|
"operands": [
|
|
{"operator": "eq", "ref": "node_5.qTiefananalyseRecovery", "value": "ja"},
|
|
{"operator": "neq", "ref": "node_6.qKonsistenz", "value": "nein"}
|
|
]
|
|
},
|
|
LogicExpression
|
|
))
|
|
|
|
# Test 3: Simple OR
|
|
test_results.append(test_case(
|
|
"Simple OR with 3 operands",
|
|
{
|
|
"operator": "or",
|
|
"operands": [
|
|
{"operator": "eq", "ref": "node_1.q1", "value": "ja"},
|
|
{"operator": "eq", "ref": "node_1.q2", "value": "nein"},
|
|
{"operator": "eq", "ref": "node_1.q3", "value": "unklar"}
|
|
]
|
|
},
|
|
LogicExpression
|
|
))
|
|
|
|
# Test 4: Nested AND/OR
|
|
test_results.append(test_case(
|
|
"Nested: OR with nested AND",
|
|
{
|
|
"operator": "or",
|
|
"operands": [
|
|
{
|
|
"operator": "and",
|
|
"operands": [
|
|
{"operator": "eq", "ref": "node_1.q1", "value": "ja"},
|
|
{"operator": "neq", "ref": "node_1.q2", "value": "nein"}
|
|
]
|
|
},
|
|
{"operator": "eq", "ref": "node_2.q1", "value": "hoch"}
|
|
]
|
|
},
|
|
LogicExpression
|
|
))
|
|
|
|
# Test 5: Deep nesting (3 levels)
|
|
test_results.append(test_case(
|
|
"Deep nesting (3 levels)",
|
|
{
|
|
"operator": "and",
|
|
"operands": [
|
|
{
|
|
"operator": "or",
|
|
"operands": [
|
|
{"operator": "eq", "ref": "node_1.q1", "value": "ja"},
|
|
{
|
|
"operator": "and",
|
|
"operands": [
|
|
{"operator": "eq", "ref": "node_2.q1", "value": "hoch"},
|
|
{"operator": "neq", "ref": "node_2.q2", "value": "niedrig"}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{"operator": "eq", "ref": "node_3.q1", "value": "aktiv"}
|
|
]
|
|
},
|
|
LogicExpression
|
|
))
|
|
|
|
# Test 6: Different operators
|
|
test_results.append(test_case(
|
|
"Different comparison operators (gt, lt, in)",
|
|
{
|
|
"operator": "and",
|
|
"operands": [
|
|
{"operator": "gt", "ref": "node_1.score", "value": 50},
|
|
{"operator": "lt", "ref": "node_1.score", "value": 100},
|
|
{"operator": "in", "ref": "node_1.category", "value": ["high", "medium"]}
|
|
]
|
|
},
|
|
LogicExpression
|
|
))
|
|
|
|
# Test 7: Legacy format (wrapped in Condition)
|
|
test_results.append(test_case(
|
|
"Legacy format: Condition with expression",
|
|
{
|
|
"type": "if",
|
|
"expression": {
|
|
"operator": "eq",
|
|
"ref": "node_1.q1",
|
|
"value": "ja"
|
|
},
|
|
"then_path": "edge_1",
|
|
"else_path": "edge_2"
|
|
},
|
|
Condition
|
|
))
|
|
|
|
# Test 8: NOT operator
|
|
test_results.append(test_case(
|
|
"NOT operator",
|
|
{
|
|
"operator": "not",
|
|
"operands": [
|
|
{"operator": "eq", "ref": "node_1.q1", "value": "nein"}
|
|
]
|
|
},
|
|
LogicExpression
|
|
))
|
|
|
|
# Test 9: Complex real-world scenario
|
|
test_results.append(test_case(
|
|
"Complex real-world: Multiple nested conditions",
|
|
{
|
|
"operator": "and",
|
|
"operands": [
|
|
{
|
|
"operator": "or",
|
|
"operands": [
|
|
{"operator": "eq", "ref": "node_1.relevance", "value": "high"},
|
|
{
|
|
"operator": "and",
|
|
"operands": [
|
|
{"operator": "eq", "ref": "node_1.relevance", "value": "medium"},
|
|
{"operator": "gt", "ref": "node_1.priority", "value": 5}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{"operator": "neq", "ref": "node_2.status", "value": "blocked"},
|
|
{
|
|
"operator": "in",
|
|
"ref": "node_3.category",
|
|
"value": ["training", "nutrition", "recovery"]
|
|
}
|
|
]
|
|
},
|
|
LogicExpression
|
|
))
|
|
|
|
# ============================================================================
|
|
# Results Summary
|
|
# ============================================================================
|
|
|
|
print("\n" + "="*60)
|
|
print("TEST RESULTS SUMMARY")
|
|
print("="*60)
|
|
|
|
passed = sum(test_results)
|
|
total = len(test_results)
|
|
failed = total - passed
|
|
|
|
print(f"\n✅ Passed: {passed}/{total}")
|
|
if failed > 0:
|
|
print(f"❌ Failed: {failed}/{total}")
|
|
print(f"\n⚠️ CRITICAL: Some tests failed! Do NOT deploy until fixed.")
|
|
sys.exit(1)
|
|
else:
|
|
print(f"\n🎉 All tests passed! Safe to deploy.")
|
|
sys.exit(0)
|