feat: make output types workflow-first contracts

This commit is contained in:
2026-04-08 21:43:55 +02:00
parent bd18cccb5e
commit 8c9648d5dc
8 changed files with 1049 additions and 110 deletions
@@ -0,0 +1,268 @@
from __future__ import annotations
import uuid
import pytest
from app.domains.rendering.models import WorkflowDefinition
from app.domains.rendering.workflow_config_utils import (
build_preset_workflow_config,
build_workflow_blueprint_config,
)
@pytest.mark.asyncio
async def test_create_output_type_infers_artifact_kind_from_format_and_animation(
client,
db,
auth_headers,
):
response = await client.post(
"/api/output-types",
json={
"name": f"Turntable {uuid.uuid4().hex[:8]}",
"renderer": "blender",
"output_format": "mp4",
"render_backend": "celery",
"workflow_family": "order_line",
"is_animation": True,
},
headers=auth_headers,
)
assert response.status_code == 201, response.text
payload = response.json()
assert payload["workflow_family"] == "order_line"
assert payload["artifact_kind"] == "turntable_video"
assert payload["invocation_overrides"] == {}
@pytest.mark.asyncio
async def test_create_output_type_rejects_workflow_family_mismatch(
client,
db,
auth_headers,
):
workflow = WorkflowDefinition(
name=f"CAD Intake {uuid.uuid4().hex[:8]}",
config=build_workflow_blueprint_config("cad_intake"),
is_active=True,
)
db.add(workflow)
await db.commit()
await db.refresh(workflow)
response = await client.post(
"/api/output-types",
json={
"name": f"Still {uuid.uuid4().hex[:8]}",
"renderer": "blender",
"output_format": "png",
"render_backend": "celery",
"workflow_family": "order_line",
"workflow_definition_id": str(workflow.id),
},
headers=auth_headers,
)
assert response.status_code == 400, response.text
assert "Workflow family mismatch" in response.json()["detail"]
@pytest.mark.asyncio
async def test_create_output_type_rejects_artifact_kind_incompatible_with_family(
client,
auth_headers,
):
response = await client.post(
"/api/output-types",
json={
"name": f"Bad Thumbnail {uuid.uuid4().hex[:8]}",
"renderer": "blender",
"output_format": "png",
"render_backend": "celery",
"workflow_family": "order_line",
"artifact_kind": "thumbnail_image",
},
headers=auth_headers,
)
assert response.status_code == 400, response.text
assert "not allowed for workflow_family" in response.json()["detail"]
@pytest.mark.asyncio
async def test_create_output_type_rejects_turntable_video_without_animation(
client,
auth_headers,
):
response = await client.post(
"/api/output-types",
json={
"name": f"Bad Turntable {uuid.uuid4().hex[:8]}",
"renderer": "blender",
"output_format": "mp4",
"render_backend": "celery",
"workflow_family": "order_line",
"artifact_kind": "turntable_video",
"is_animation": False,
},
headers=auth_headers,
)
assert response.status_code == 400, response.text
assert response.json()["detail"] == "Artifact kind 'turntable_video' requires is_animation=true"
@pytest.mark.asyncio
async def test_update_output_type_rejects_mixed_family_workflow(
client,
db,
auth_headers,
):
output_type_response = await client.post(
"/api/output-types",
json={
"name": f"Still {uuid.uuid4().hex[:8]}",
"renderer": "blender",
"output_format": "png",
"render_backend": "celery",
"workflow_family": "order_line",
},
headers=auth_headers,
)
assert output_type_response.status_code == 201, output_type_response.text
output_type = output_type_response.json()
workflow = WorkflowDefinition(
name=f"Mixed {uuid.uuid4().hex[:8]}",
config={
"version": 1,
"nodes": build_workflow_blueprint_config("cad_intake")["nodes"][:1]
+ build_preset_workflow_config("still_graph")["nodes"][:1],
"edges": [],
"ui": {"preset": "custom", "execution_mode": "graph"},
},
is_active=True,
)
db.add(workflow)
await db.commit()
await db.refresh(workflow)
response = await client.patch(
f"/api/output-types/{output_type['id']}",
json={"workflow_definition_id": str(workflow.id)},
headers=auth_headers,
)
assert response.status_code == 400, response.text
assert response.json()["detail"] == "Output types cannot link mixed-family workflows"
@pytest.mark.asyncio
async def test_create_output_type_backfills_invocation_overrides_from_legacy_render_settings(
client,
auth_headers,
):
response = await client.post(
"/api/output-types",
json={
"name": f"Legacy Still {uuid.uuid4().hex[:8]}",
"renderer": "blender",
"output_format": "png",
"render_backend": "celery",
"workflow_family": "order_line",
"render_settings": {
"width": 1600,
"height": 900,
"engine": "cycles",
},
},
headers=auth_headers,
)
assert response.status_code == 201, response.text
payload = response.json()
assert payload["artifact_kind"] == "still_image"
assert payload["invocation_overrides"] == {
"width": 1600,
"height": 900,
"engine": "cycles",
}
assert payload["render_settings"]["width"] == 1600
assert payload["render_settings"]["height"] == 900
assert payload["render_settings"]["engine"] == "cycles"
@pytest.mark.asyncio
async def test_patch_output_type_invocation_overrides_syncs_legacy_render_settings(
client,
auth_headers,
):
output_type_response = await client.post(
"/api/output-types",
json={
"name": f"Still {uuid.uuid4().hex[:8]}",
"renderer": "blender",
"output_format": "png",
"render_backend": "celery",
"workflow_family": "order_line",
},
headers=auth_headers,
)
assert output_type_response.status_code == 201, output_type_response.text
output_type = output_type_response.json()
response = await client.patch(
f"/api/output-types/{output_type['id']}",
json={
"invocation_overrides": {
"width": 1600,
"height": 900,
"engine": "cycles",
}
},
headers=auth_headers,
)
assert response.status_code == 200, response.text
payload = response.json()
assert payload["invocation_overrides"]["width"] == 1600
assert payload["invocation_overrides"]["height"] == 900
assert payload["invocation_overrides"]["engine"] == "cycles"
assert payload["render_settings"]["width"] == 1600
assert payload["render_settings"]["height"] == 900
assert payload["render_settings"]["engine"] == "cycles"
@pytest.mark.asyncio
async def test_patch_output_type_recomputes_artifact_kind_when_switching_family(
client,
auth_headers,
):
output_type_response = await client.post(
"/api/output-types",
json={
"name": f"Still {uuid.uuid4().hex[:8]}",
"renderer": "blender",
"output_format": "png",
"render_backend": "celery",
"workflow_family": "order_line",
},
headers=auth_headers,
)
assert output_type_response.status_code == 201, output_type_response.text
output_type = output_type_response.json()
response = await client.patch(
f"/api/output-types/{output_type['id']}",
json={
"workflow_family": "cad_file",
},
headers=auth_headers,
)
assert response.status_code == 200, response.text
payload = response.json()
assert payload["workflow_family"] == "cad_file"
assert payload["artifact_kind"] == "thumbnail_image"