from __future__ import annotations import uuid import pytest from sqlalchemy import select from sqlalchemy.orm import selectinload from app.config import settings from app.domains.orders.models import Order, OrderLine from app.domains.products.models import Product from app.domains.rendering.dispatch_service import dispatch_render_with_workflow from app.domains.rendering.models import OutputType, WorkflowDefinition, WorkflowRun from app.domains.rendering.workflow_config_utils import build_preset_workflow_config def _use_test_database(monkeypatch) -> None: monkeypatch.setattr(settings, "postgres_host", "postgres") monkeypatch.setattr(settings, "postgres_port", 5432) monkeypatch.setattr(settings, "postgres_user", "hartomat") monkeypatch.setattr(settings, "postgres_password", "hartomat") monkeypatch.setattr(settings, "postgres_db", "hartomat_test") async def _seed_order_line( db, admin_user, *, workflow_config: dict | None = None, ) -> dict[str, object]: product = Product( pim_id=f"PIM-{uuid.uuid4().hex[:8]}", name="Workflow Test Product", ) output_type = OutputType( name=f"Workflow Output {uuid.uuid4().hex[:8]}", render_backend="auto", ) order = Order( order_number=f"WF-{uuid.uuid4().hex[:10]}", created_by=admin_user.id, ) db.add_all([product, output_type, order]) await db.flush() workflow_definition = None if workflow_config is not None: workflow_definition = WorkflowDefinition( name=f"Workflow {uuid.uuid4().hex[:8]}", output_type_id=output_type.id, config=workflow_config, is_active=True, ) db.add(workflow_definition) await db.flush() output_type.workflow_definition_id = workflow_definition.id order_line = OrderLine( order_id=order.id, product_id=product.id, output_type_id=output_type.id, ) db.add(order_line) await db.commit() return { "order_line": order_line, "workflow_definition": workflow_definition, "output_type": output_type, } @pytest.mark.asyncio async def test_dispatch_render_with_workflow_falls_back_to_legacy_without_workflow_definition( db, admin_user, monkeypatch, ): _use_test_database(monkeypatch) seeded = await _seed_order_line(db, admin_user) monkeypatch.setattr( "app.domains.rendering.dispatch_service._legacy_dispatch", lambda order_line_id: {"backend": "legacy", "order_line_id": order_line_id}, ) result = dispatch_render_with_workflow(str(seeded["order_line"].id)) await db.rollback() assert result == { "backend": "legacy", "order_line_id": str(seeded["order_line"].id), } runs = (await db.execute(select(WorkflowRun))).scalars().all() assert runs == [] @pytest.mark.asyncio async def test_dispatch_render_with_workflow_creates_run_and_node_results_for_preset_dispatch( db, admin_user, monkeypatch, ): _use_test_database(monkeypatch) seeded = await _seed_order_line( db, admin_user, workflow_config=build_preset_workflow_config("still", {"width": 1024, "height": 1024}), ) monkeypatch.setattr( "app.domains.rendering.workflow_builder.dispatch_workflow", lambda workflow_type, order_line_id, params=None: "canvas-123", ) result = dispatch_render_with_workflow(str(seeded["order_line"].id)) await db.rollback() run_result = await db.execute( select(WorkflowRun) .where(WorkflowRun.id == uuid.UUID(result["workflow_run_id"])) .options(selectinload(WorkflowRun.node_results)) ) run = run_result.scalar_one() assert result["backend"] == "workflow" assert result["workflow_type"] == "still" assert result["celery_task_id"] == "canvas-123" assert run.workflow_def_id == seeded["workflow_definition"].id assert run.order_line_id == seeded["order_line"].id assert run.celery_task_id == "canvas-123" assert {node_result.node_name for node_result in run.node_results} == { "setup", "template", "render", "output", } assert all(node_result.status == "pending" for node_result in run.node_results) @pytest.mark.asyncio async def test_dispatch_render_with_workflow_falls_back_when_workflow_runtime_preparation_is_invalid( db, admin_user, monkeypatch, ): _use_test_database(monkeypatch) seeded = await _seed_order_line( db, admin_user, workflow_config={ "version": 1, "nodes": [ {"id": "render", "step": "blender_still", "params": {}}, ], "edges": [ {"from": "missing", "to": "render"}, ], }, ) monkeypatch.setattr( "app.domains.rendering.dispatch_service._legacy_dispatch", lambda order_line_id: {"backend": "legacy", "order_line_id": order_line_id}, ) result = dispatch_render_with_workflow(str(seeded["order_line"].id)) await db.rollback() assert result == { "backend": "legacy", "order_line_id": str(seeded["order_line"].id), } runs = (await db.execute(select(WorkflowRun))).scalars().all() assert runs == [] @pytest.mark.asyncio async def test_workflow_dispatch_endpoint_returns_workflow_run_with_node_results( client, db, auth_headers, monkeypatch, ): workflow_definition = WorkflowDefinition( name=f"Dispatch Workflow {uuid.uuid4().hex[:8]}", config=build_preset_workflow_config("still_with_exports", {"width": 640, "height": 640}), is_active=True, ) db.add(workflow_definition) await db.commit() await db.refresh(workflow_definition) calls: list[tuple[str, list[str], dict]] = [] def _fake_send_task(task_name: str, args: list[str], kwargs: dict): calls.append((task_name, args, kwargs)) return type("Result", (), {"id": f"task-{len(calls)}"})() context_id = str(uuid.uuid4()) monkeypatch.setattr("app.tasks.celery_app.celery_app.send_task", _fake_send_task) response = await client.post( f"/api/workflows/{workflow_definition.id}/dispatch", params={"context_id": context_id}, headers=auth_headers, ) assert response.status_code == 200 body = response.json() assert body["context_id"] == context_id assert body["execution_mode"] == "graph" assert body["dispatched"] == 2 assert body["task_ids"] == ["task-1", "task-2"] assert calls == [ ( "app.domains.rendering.tasks.render_order_line_still_task", [context_id], {"width": 640, "height": 640}, ), ( "app.domains.rendering.tasks.export_blend_for_order_line_task", [context_id], {}, ), ] node_results = {node["node_name"]: node for node in body["workflow_run"]["node_results"]} assert body["workflow_run"]["status"] == "pending" assert body["workflow_run"]["celery_task_id"] == "task-1" assert node_results["render"]["status"] == "queued" assert node_results["render"]["output"]["task_id"] == "task-1" assert node_results["blend"]["status"] == "queued" assert node_results["blend"]["output"]["task_id"] == "task-2" assert node_results["setup"]["status"] == "skipped" assert node_results["template"]["status"] == "skipped" assert node_results["output"]["status"] == "skipped"