import pytest from pydantic import ValidationError from app.domains.rendering.workflow_schema import WorkflowConfig def test_workflow_schema_rejects_duplicate_edges(): with pytest.raises(ValidationError, match="duplicate edge is not allowed"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ {"id": "setup", "step": "order_line_setup", "params": {}}, {"id": "render", "step": "blender_still", "params": {}}, ], "edges": [ {"from": "setup", "to": "render"}, {"from": "setup", "to": "render"}, ], } ) def test_workflow_schema_rejects_self_referential_edges(): with pytest.raises(ValidationError, match="self-referential edge is not allowed"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ {"id": "render", "step": "blender_still", "params": {}}, ], "edges": [ {"from": "render", "to": "render"}, ], } ) def test_workflow_schema_rejects_cycles(): with pytest.raises(ValidationError, match="workflow graph must be acyclic"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ {"id": "setup", "step": "order_line_setup", "params": {}}, {"id": "template", "step": "resolve_template", "params": {}}, {"id": "render", "step": "blender_still", "params": {}}, ], "edges": [ {"from": "setup", "to": "template"}, {"from": "template", "to": "render"}, {"from": "render", "to": "setup"}, ], } ) def test_workflow_schema_rejects_unknown_node_params(): with pytest.raises(ValidationError, match="uses unknown param key"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ { "id": "bbox", "step": "glb_bbox", "params": {"glb_path": "/tmp/model.glb", "bogus": "value"}, }, ], "edges": [], } ) def test_workflow_schema_accepts_known_node_params(): config = WorkflowConfig.model_validate( { "version": 1, "nodes": [ { "id": "bbox", "step": "glb_bbox", "params": {"glb_path": "/tmp/model.glb"}, }, ], "edges": [], "ui": {"family": "order_line"}, } ) assert config.ui is not None assert config.ui.family == "order_line" def test_workflow_schema_rejects_ui_family_mismatch(): with pytest.raises(ValidationError, match="ui.family"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ {"id": "setup", "step": "order_line_setup", "params": {}}, {"id": "render", "step": "blender_still", "params": {}}, ], "edges": [ {"from": "setup", "to": "render"}, ], "ui": {"family": "cad_file"}, } ) def test_workflow_schema_accepts_explicit_mixed_family_when_declared(): config = WorkflowConfig.model_validate( { "version": 1, "nodes": [ {"id": "cad", "step": "resolve_step_path", "params": {}}, {"id": "render", "step": "blender_still", "params": {}}, ], "edges": [], "ui": {"family": "mixed"}, } ) assert config.ui is not None assert config.ui.family == "mixed" def test_workflow_schema_rejects_invalid_select_value(): with pytest.raises(ValidationError, match="must be one of"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ { "id": "notify", "step": "notify", "params": {"channel": "email"}, }, ], "edges": [], } ) def test_workflow_schema_rejects_invalid_number_value(): with pytest.raises(ValidationError, match="must be a number"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ { "id": "render", "step": "blender_still", "params": {"samples": "high"}, }, ], "edges": [], } ) def test_workflow_schema_rejects_invalid_boolean_value(): with pytest.raises(ValidationError, match="must be a boolean"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ { "id": "render", "step": "blender_still", "params": {"use_custom_render_settings": "yes"}, }, ], "edges": [], } ) def test_workflow_schema_rejects_missing_required_upstream_artifact(): with pytest.raises(ValidationError, match="missing required input artifact"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ {"id": "setup", "step": "order_line_setup", "params": {}}, {"id": "render", "step": "blender_still", "params": {}}, ], "edges": [ {"from": "setup", "to": "render"}, ], "ui": {"execution_mode": "graph", "family": "order_line"}, } ) def test_workflow_schema_accepts_transitive_contract_wiring(): config = WorkflowConfig.model_validate( { "version": 1, "nodes": [ {"id": "setup", "step": "order_line_setup", "params": {}}, {"id": "template", "step": "resolve_template", "params": {}}, {"id": "populate_materials", "step": "auto_populate_materials", "params": {}}, {"id": "bbox", "step": "glb_bbox", "params": {}}, {"id": "resolve_materials", "step": "material_map_resolve", "params": {}}, {"id": "render", "step": "blender_still", "params": {}}, {"id": "output", "step": "output_save", "params": {}}, ], "edges": [ {"from": "setup", "to": "template"}, {"from": "setup", "to": "populate_materials"}, {"from": "setup", "to": "bbox"}, {"from": "template", "to": "resolve_materials"}, {"from": "populate_materials", "to": "resolve_materials"}, {"from": "template", "to": "render"}, {"from": "bbox", "to": "render"}, {"from": "resolve_materials", "to": "render"}, {"from": "render", "to": "output"}, ], "ui": {"family": "order_line", "execution_mode": "graph"}, } ) assert config.ui is not None assert config.ui.execution_mode == "graph" def test_workflow_schema_rejects_mixed_family_graph_execution(): with pytest.raises(ValidationError, match="single-family"): WorkflowConfig.model_validate( { "version": 1, "nodes": [ {"id": "cad", "step": "resolve_step_path", "params": {}}, {"id": "render", "step": "blender_still", "params": {}}, ], "edges": [], "ui": {"execution_mode": "graph", "family": "mixed"}, } )