441 lines
14 KiB
Python
441 lines
14 KiB
Python
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.core.process_steps import StepName
|
|
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_rejects_unregistered_nodes_from_registry(monkeypatch):
|
|
from app.domains.rendering import workflow_schema as schema_module
|
|
|
|
original = schema_module.get_node_definition
|
|
|
|
def fake_get_node_definition(step):
|
|
if step == StepName.GLB_BBOX:
|
|
return None
|
|
return original(step)
|
|
|
|
monkeypatch.setattr(schema_module, "get_node_definition", fake_get_node_definition)
|
|
|
|
with pytest.raises(ValidationError, match="is not registered in workflow_node_registry"):
|
|
WorkflowConfig.model_validate(
|
|
{
|
|
"version": 1,
|
|
"nodes": [
|
|
{
|
|
"id": "bbox",
|
|
"step": StepName.GLB_BBOX.value,
|
|
"params": {},
|
|
},
|
|
],
|
|
"edges": [],
|
|
"ui": {"family": "order_line"},
|
|
}
|
|
)
|
|
|
|
|
|
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_invalid_glb_path_format():
|
|
with pytest.raises(ValidationError, match="must point to a .glb file"):
|
|
WorkflowConfig.model_validate(
|
|
{
|
|
"version": 1,
|
|
"nodes": [
|
|
{
|
|
"id": "bbox",
|
|
"step": "glb_bbox",
|
|
"params": {"glb_path": "/tmp/model.gltf"},
|
|
},
|
|
],
|
|
"edges": [],
|
|
}
|
|
)
|
|
|
|
|
|
def test_workflow_schema_rejects_invalid_template_id_override_format():
|
|
with pytest.raises(ValidationError, match="must be a valid UUID"):
|
|
WorkflowConfig.model_validate(
|
|
{
|
|
"version": 1,
|
|
"nodes": [
|
|
{
|
|
"id": "template",
|
|
"step": "resolve_template",
|
|
"params": {"template_id_override": "not-a-uuid"},
|
|
},
|
|
],
|
|
"edges": [],
|
|
}
|
|
)
|
|
|
|
|
|
def test_workflow_schema_rejects_invalid_material_library_path_format():
|
|
with pytest.raises(ValidationError, match="must point to a .blend file"):
|
|
WorkflowConfig.model_validate(
|
|
{
|
|
"version": 1,
|
|
"nodes": [
|
|
{
|
|
"id": "template",
|
|
"step": "resolve_template",
|
|
"params": {"material_library_path": "/tmp/library.txt"},
|
|
},
|
|
],
|
|
"edges": [],
|
|
}
|
|
)
|
|
|
|
|
|
def test_workflow_schema_rejects_invalid_noise_threshold_format():
|
|
with pytest.raises(ValidationError, match="must be a valid numeric string"):
|
|
WorkflowConfig.model_validate(
|
|
{
|
|
"version": 1,
|
|
"nodes": [
|
|
{
|
|
"id": "render",
|
|
"step": "blender_still",
|
|
"params": {"noise_threshold": "fast"},
|
|
},
|
|
],
|
|
"edges": [],
|
|
}
|
|
)
|
|
|
|
|
|
def test_workflow_schema_rejects_invalid_bg_color_format():
|
|
with pytest.raises(ValidationError, match="must be a hex color"):
|
|
WorkflowConfig.model_validate(
|
|
{
|
|
"version": 1,
|
|
"nodes": [
|
|
{
|
|
"id": "turntable",
|
|
"step": "blender_turntable",
|
|
"params": {"bg_color": "blue"},
|
|
},
|
|
],
|
|
"edges": [],
|
|
}
|
|
)
|
|
|
|
|
|
def test_workflow_schema_rejects_invalid_output_name_suffix_format():
|
|
with pytest.raises(ValidationError, match="may only contain letters, numbers"):
|
|
WorkflowConfig.model_validate(
|
|
{
|
|
"version": 1,
|
|
"nodes": [
|
|
{
|
|
"id": "blend",
|
|
"step": "export_blend",
|
|
"params": {"output_name_suffix": "../unsafe"},
|
|
},
|
|
],
|
|
"edges": [],
|
|
}
|
|
)
|
|
|
|
|
|
def test_workflow_schema_accepts_empty_optional_text_overrides():
|
|
config = WorkflowConfig.model_validate(
|
|
{
|
|
"version": 1,
|
|
"nodes": [
|
|
{
|
|
"id": "template",
|
|
"step": "resolve_template",
|
|
"params": {
|
|
"template_id_override": "",
|
|
"material_library_path": "",
|
|
},
|
|
},
|
|
{
|
|
"id": "render",
|
|
"step": "blender_still",
|
|
"params": {
|
|
"noise_threshold": "",
|
|
"material_override": "",
|
|
},
|
|
},
|
|
{
|
|
"id": "turntable",
|
|
"step": "blender_turntable",
|
|
"params": {"bg_color": ""},
|
|
},
|
|
{
|
|
"id": "blend",
|
|
"step": "export_blend",
|
|
"params": {"output_name_suffix": ""},
|
|
},
|
|
],
|
|
"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_accepts_cad_intake_contract_wiring_with_shared_bbox_node():
|
|
config = WorkflowConfig.model_validate(
|
|
{
|
|
"version": 1,
|
|
"nodes": [
|
|
{"id": "resolve_step", "step": "resolve_step_path", "params": {}},
|
|
{"id": "export_glb", "step": "occ_glb_export", "params": {}},
|
|
{"id": "bbox", "step": "glb_bbox", "params": {}},
|
|
{"id": "threejs_thumb", "step": "threejs_render", "params": {}},
|
|
{"id": "save", "step": "thumbnail_save", "params": {}},
|
|
],
|
|
"edges": [
|
|
{"from": "resolve_step", "to": "export_glb"},
|
|
{"from": "export_glb", "to": "bbox"},
|
|
{"from": "export_glb", "to": "threejs_thumb"},
|
|
{"from": "bbox", "to": "threejs_thumb"},
|
|
{"from": "threejs_thumb", "to": "save"},
|
|
],
|
|
"ui": {"family": "cad_file", "execution_mode": "graph"},
|
|
}
|
|
)
|
|
|
|
assert config.ui is not None
|
|
assert config.ui.family == "cad_file"
|
|
|
|
|
|
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"},
|
|
}
|
|
)
|