220 lines
7.5 KiB
Python
220 lines
7.5 KiB
Python
"""Tests for dynamic Modal proxy nodes and local execution fallback."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
class _FakeOriginalNode:
|
|
"""Simple fake legacy node for proxy signature mirroring."""
|
|
|
|
RETURN_TYPES = ("IMAGE", "INT")
|
|
RETURN_NAMES = ("image", "count")
|
|
OUTPUT_IS_LIST = (False, False)
|
|
FUNCTION = "run"
|
|
|
|
def run(self, **kwargs: Any) -> tuple[Any, ...]:
|
|
"""Return a tuple that exposes the inputs for verification."""
|
|
return (kwargs["value"], 1)
|
|
|
|
|
|
def test_dynamic_proxy_node_preserves_output_signature(
|
|
modal_executor_module: Any,
|
|
) -> None:
|
|
"""Dynamic Modal proxies should mirror the original output count and names."""
|
|
fake_nodes_module = type(
|
|
"FakeNodesModule",
|
|
(),
|
|
{
|
|
"NODE_CLASS_MAPPINGS": {"OriginalNode": _FakeOriginalNode},
|
|
"NODE_DISPLAY_NAME_MAPPINGS": {},
|
|
},
|
|
)()
|
|
|
|
proxy_id = modal_executor_module.ensure_modal_proxy_node_registered(
|
|
original_class_type="OriginalNode",
|
|
original_class=_FakeOriginalNode,
|
|
nodes_module=fake_nodes_module,
|
|
)
|
|
|
|
proxy_class = fake_nodes_module.NODE_CLASS_MAPPINGS[proxy_id]
|
|
schema = proxy_class.GET_SCHEMA()
|
|
|
|
assert schema.node_id == proxy_id
|
|
assert [output.display_name for output in schema.outputs] == ["image", "count"]
|
|
assert [output.io_type for output in schema.outputs] == ["IMAGE", "INT"]
|
|
|
|
|
|
def test_proxy_execution_uses_injected_remote_client(
|
|
modal_executor_module: Any,
|
|
) -> None:
|
|
"""Proxy execution should delegate to the configured remote client."""
|
|
fake_nodes_module = type(
|
|
"FakeNodesModule",
|
|
(),
|
|
{
|
|
"NODE_CLASS_MAPPINGS": {"OriginalNode": _FakeOriginalNode},
|
|
"NODE_DISPLAY_NAME_MAPPINGS": {},
|
|
},
|
|
)()
|
|
|
|
proxy_id = modal_executor_module.ensure_modal_proxy_node_registered(
|
|
original_class_type="OriginalNode",
|
|
original_class=_FakeOriginalNode,
|
|
nodes_module=fake_nodes_module,
|
|
)
|
|
proxy_class = fake_nodes_module.NODE_CLASS_MAPPINGS[proxy_id]
|
|
|
|
class FakeClient:
|
|
"""Test client that returns deterministic outputs."""
|
|
|
|
def execute_payload(self, payload: dict[str, Any], kwargs: dict[str, Any]) -> tuple[str, int]:
|
|
"""Return values derived from the proxied node payload."""
|
|
return (f"{payload['class_type']}::{kwargs['value']}", 3)
|
|
|
|
modal_executor_module.set_remote_executor_client_factory(lambda: FakeClient())
|
|
try:
|
|
result = proxy_class.execute(original_node_data={"class_type": "OriginalNode"}, value="payload")
|
|
finally:
|
|
modal_executor_module.set_remote_executor_client_factory(None)
|
|
|
|
assert result.result == ("OriginalNode::payload", 3)
|
|
|
|
|
|
def test_local_remote_app_executes_original_node(
|
|
remote_modal_app_module: Any,
|
|
serialization_module: Any,
|
|
) -> None:
|
|
"""The local fallback remote app should execute a mapped legacy node."""
|
|
payload = remote_modal_app_module.execute_node_locally(
|
|
node_data={"class_type": "OriginalNode"},
|
|
kwargs_payload='{"value": "hello"}',
|
|
node_mapping={"OriginalNode": _FakeOriginalNode},
|
|
)
|
|
outputs = serialization_module.deserialize_node_outputs(payload)
|
|
assert outputs == ("hello", 1)
|
|
|
|
|
|
def test_stable_modal_cloud_entry_imports_without_modal_sdk(
|
|
modal_cloud_module: Any,
|
|
) -> None:
|
|
"""The stable Modal cloud module should stay importable when modal is unavailable."""
|
|
assert modal_cloud_module.__name__ == "comfyui_modal_sync_cloud"
|
|
assert hasattr(modal_cloud_module, "RemoteEngine")
|
|
|
|
|
|
def test_modal_cloud_ignores_heavy_comfyui_paths(
|
|
modal_cloud_module: Any,
|
|
) -> None:
|
|
"""The Modal cloud module should skip heavyweight ComfyUI runtime artifacts."""
|
|
from pathlib import Path
|
|
|
|
assert modal_cloud_module._should_ignore_comfyui_path(Path("models/checkpoint.safetensors"))
|
|
assert modal_cloud_module._should_ignore_comfyui_path(Path("output/run/output.png"))
|
|
assert modal_cloud_module._should_ignore_comfyui_path(Path("__pycache__/execution.pyc"))
|
|
assert not modal_cloud_module._should_ignore_comfyui_path(Path("execution.py"))
|
|
assert not modal_cloud_module._should_ignore_comfyui_path(Path("comfy/model_management.py"))
|
|
assert not modal_cloud_module._should_ignore_comfyui_path(Path("comfy/ldm/models/diffusion/ddpm.py"))
|
|
|
|
|
|
def test_modal_cloud_installs_comfyui_runtime_packages(
|
|
modal_cloud_module: Any,
|
|
) -> None:
|
|
"""The Modal cloud image should include the core packages ComfyUI imports at runtime."""
|
|
packages = set(modal_cloud_module._comfyui_runtime_packages())
|
|
|
|
assert "psutil" in packages
|
|
assert "torch" in packages
|
|
assert "torchvision" in packages
|
|
assert "torchsde" in packages
|
|
assert "transformers" in packages
|
|
assert "sentencepiece" in packages
|
|
assert "aiohttp" in packages
|
|
assert "opencv-python-headless" in packages
|
|
|
|
|
|
class _BoundarySourceNode:
|
|
"""Simple source node used for subgraph execution tests."""
|
|
|
|
RETURN_TYPES = ("INT",)
|
|
RETURN_NAMES = ("value",)
|
|
OUTPUT_IS_LIST = (False,)
|
|
FUNCTION = "run"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls) -> dict[str, dict[str, tuple[str]]]:
|
|
"""Return the minimal V1 input schema."""
|
|
return {"required": {"value": ("INT",)}}
|
|
|
|
def run(self, value: int) -> tuple[int]:
|
|
"""Increment the boundary input."""
|
|
return (value + 1,)
|
|
|
|
|
|
class _BoundarySinkNode:
|
|
"""Simple downstream node used for subgraph execution tests."""
|
|
|
|
RETURN_TYPES = ("INT",)
|
|
RETURN_NAMES = ("value",)
|
|
OUTPUT_IS_LIST = (False,)
|
|
FUNCTION = "run"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls) -> dict[str, dict[str, tuple[str]]]:
|
|
"""Return the minimal V1 input schema."""
|
|
return {"required": {"value": ("INT",)}}
|
|
|
|
def run(self, value: int) -> tuple[int]:
|
|
"""Double the upstream value."""
|
|
return (value * 2,)
|
|
|
|
|
|
def test_local_remote_app_executes_subgraph_payload(
|
|
remote_modal_app_module: Any,
|
|
serialization_module: Any,
|
|
) -> None:
|
|
"""The local fallback remote app should execute rewritten subgraph payloads."""
|
|
payload = remote_modal_app_module.execute_subgraph_locally(
|
|
payload={
|
|
"payload_kind": "subgraph",
|
|
"component_id": "component-1",
|
|
"subgraph_prompt": {
|
|
"remote_1": {
|
|
"class_type": "BoundarySource",
|
|
"inputs": {"value": 0},
|
|
"_meta": {},
|
|
},
|
|
"remote_2": {
|
|
"class_type": "BoundarySink",
|
|
"inputs": {"value": ["remote_1", 0]},
|
|
"_meta": {},
|
|
},
|
|
},
|
|
"boundary_inputs": [
|
|
{
|
|
"proxy_input_name": "remote_input_0",
|
|
"targets": [{"node_id": "remote_1", "input_name": "value"}],
|
|
}
|
|
],
|
|
"boundary_outputs": [
|
|
{
|
|
"proxy_output_name": "remote_2_value",
|
|
"node_id": "remote_2",
|
|
"output_index": 0,
|
|
"io_type": "INT",
|
|
"is_list": False,
|
|
}
|
|
],
|
|
"execute_node_ids": ["remote_2"],
|
|
"extra_data": {},
|
|
"custom_nodes_bundle": None,
|
|
},
|
|
kwargs_payload='{"remote_input_0": 4}',
|
|
node_mapping={
|
|
"BoundarySource": _BoundarySourceNode,
|
|
"BoundarySink": _BoundarySinkNode,
|
|
},
|
|
)
|
|
outputs = serialization_module.deserialize_node_outputs(payload)
|
|
assert outputs == (10,)
|