import asyncio from unittest.mock import AsyncMock import pytest import torch from comfy.cli_args import args if not torch.cuda.is_available(): args.cpu = True from comfy_api_nodes.apis.comfy_cloud import ( ComfyCloudGenerateRequest, ComfyCloudGenerateResponse, ComfyCloudStatusResponse, ComfyCloudWorkflowInputs, ) from comfy_api_nodes import nodes_comfy_cloud @pytest.mark.parametrize( ("node", "workflow", "returns_video", "requires_image"), [ (nodes_comfy_cloud.ComfyCloudTextToImageNode, "text-to-image", False, False), (nodes_comfy_cloud.ComfyCloudTextToVideoNode, "text-to-video", True, False), (nodes_comfy_cloud.ComfyCloudImageToVideoNode, "image-to-video", True, True), (nodes_comfy_cloud.ComfyCloudImageEditNode, "image-edit", False, True), ], ) def test_workflow_submission_polling_and_download(monkeypatch, node, workflow, returns_video, requires_image): sync = AsyncMock( return_value=ComfyCloudGenerateResponse( task_id="task-1", status="queued", polling_url="/proxy/comfy-cloud/workflow/tasks/task-1", cancel_url="/proxy/comfy-cloud/workflow/tasks/task-1/cancel", ) ) poll = AsyncMock( return_value=ComfyCloudStatusResponse( task_id="task-1", status="completed", progress=100, output_url="https://example.com/output", ) ) upload = AsyncMock(return_value="https://example.com/input.png") image_download = AsyncMock(return_value="image-output") video_download = AsyncMock(return_value="video-output") monkeypatch.setattr(nodes_comfy_cloud, "sync_op", sync) monkeypatch.setattr(nodes_comfy_cloud, "poll_op", poll) monkeypatch.setattr(nodes_comfy_cloud, "upload_image_to_comfyapi", upload) monkeypatch.setattr(nodes_comfy_cloud, "download_url_to_image_tensor", image_download) monkeypatch.setattr(nodes_comfy_cloud, "download_url_to_video_output", video_download) monkeypatch.setattr(nodes_comfy_cloud, "get_number_of_images", lambda image: 1) image = object() if requires_image else None output = asyncio.run(node.execute("A tiny fennec fox", image)) endpoint = sync.call_args.args[1] request = sync.call_args.kwargs["data"] assert endpoint.path == "/proxy/comfy-cloud/workflow/generate" assert endpoint.method == "POST" assert request == ComfyCloudGenerateRequest( workflow=workflow, inputs=ComfyCloudWorkflowInputs( prompt="A tiny fennec fox", image_url="https://example.com/input.png" if requires_image else None, ), ) assert upload.await_count == int(requires_image) poll_endpoint = poll.call_args.args[1] cancel_endpoint = poll.call_args.kwargs["cancel_endpoint"] assert poll_endpoint.path == "/proxy/comfy-cloud/workflow/tasks/task-1" assert cancel_endpoint.path == "/proxy/comfy-cloud/workflow/tasks/task-1/cancel" assert cancel_endpoint.method == "POST" assert output[0] == ("video-output" if returns_video else "image-output") @pytest.mark.parametrize( "node", [nodes_comfy_cloud.ComfyCloudImageToVideoNode, nodes_comfy_cloud.ComfyCloudImageEditNode], ) def test_image_workflows_reject_batches(monkeypatch, node): upload = AsyncMock() monkeypatch.setattr(nodes_comfy_cloud, "get_number_of_images", lambda image: 2) monkeypatch.setattr(nodes_comfy_cloud, "upload_image_to_comfyapi", upload) with pytest.raises(ValueError, match="Exactly one input image"): asyncio.run(node.execute("Animate this", object())) upload.assert_not_awaited() def test_contract_omits_optional_status_fields(): request = ComfyCloudGenerateRequest( workflow="text-to-image", inputs=ComfyCloudWorkflowInputs(prompt="A lighthouse"), ) status = ComfyCloudStatusResponse(task_id="task-1", status="queued") assert request.model_dump(exclude_none=True) == { "workflow": "text-to-image", "inputs": {"prompt": "A lighthouse"}, } assert status.model_dump(exclude_none=True) == {"task_id": "task-1", "status": "queued"}