feat: make output types workflow-first contracts
This commit is contained in:
@@ -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"
|
||||
Reference in New Issue
Block a user