170 lines
5.9 KiB
Python
170 lines
5.9 KiB
Python
"""Tests for the captured working Modal workflow artifact."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
class _FakeRegressionRemoteModelNode:
|
|
"""Fake remote node that produces a non-transportable MODEL output."""
|
|
|
|
RETURN_TYPES = ("MODEL",)
|
|
RETURN_NAMES = ("model",)
|
|
OUTPUT_IS_LIST = (False,)
|
|
|
|
|
|
class _FakeRegressionRemoteSamplerNode:
|
|
"""Fake remote node that produces a transportable LATENT output."""
|
|
|
|
RETURN_TYPES = ("LATENT",)
|
|
RETURN_NAMES = ("latent",)
|
|
OUTPUT_IS_LIST = (False,)
|
|
|
|
|
|
class _FakeRegressionLatentSourceNode:
|
|
"""Fake local node that produces a LATENT input for remote samplers."""
|
|
|
|
RETURN_TYPES = ("LATENT",)
|
|
RETURN_NAMES = ("latent",)
|
|
OUTPUT_IS_LIST = (False,)
|
|
|
|
|
|
class _FakeRegressionModalMapInputNode:
|
|
"""Fake Modal map marker node used during rewrite regression tests."""
|
|
|
|
RETURN_TYPES = ("*",)
|
|
RETURN_NAMES = ("value",)
|
|
OUTPUT_IS_LIST = (False,)
|
|
|
|
|
|
class _FakeRegressionLocalSinkNode:
|
|
"""Fake local sink node used to validate downstream rewiring."""
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("image",)
|
|
OUTPUT_IS_LIST = (False,)
|
|
|
|
|
|
def test_modal_test_workflow_captures_known_working_remote_component() -> None:
|
|
"""The checked-in workflow artifact should preserve the first known working remote graph shape."""
|
|
workflow_path = Path(__file__).resolve().parents[1] / "modal_test_workflow.json"
|
|
workflow = json.loads(workflow_path.read_text(encoding="utf-8"))
|
|
|
|
nodes = workflow["nodes"]
|
|
node_types_by_id = {int(node["id"]): str(node["type"]) for node in nodes}
|
|
remote_node_ids = {
|
|
int(node["id"])
|
|
for node in nodes
|
|
if bool(node.get("properties", {}).get("is_modal_remote"))
|
|
}
|
|
|
|
assert workflow_path.exists()
|
|
assert workflow["version"] == 0.4
|
|
assert workflow["extra"]["comfy_modal"]["gpu"] == "RTX-PRO-6000"
|
|
assert node_types_by_id[2] == "KSampler"
|
|
assert node_types_by_id[7] == "UNETLoader"
|
|
assert node_types_by_id[11] == "CLIPLoader"
|
|
assert remote_node_ids == {2, 4, 5, 6, 7, 11}
|
|
assert any(str(node["type"]).startswith("ModalUniversalExecutor") for node in nodes)
|
|
assert node_types_by_id[8] == "VAEDecode"
|
|
assert node_types_by_id[9] == "PreviewImage"
|
|
|
|
|
|
def test_hybrid_split_workflow_artifact_rewrites_into_static_and_mapped_proxies(
|
|
api_intercept_module: Any,
|
|
modal_executor_module: Any,
|
|
settings_module: Any,
|
|
sync_engine_module: Any,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The checked-in hybrid artifact should keep rewriting into static and mapped split proxies."""
|
|
artifact_path = (
|
|
Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "modal_hybrid_split_regression.json"
|
|
)
|
|
artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
|
|
workflow = artifact["workflow"]
|
|
prompt = artifact["prompt"]
|
|
|
|
settings = settings_module.ModalSyncSettings(
|
|
app_name="app",
|
|
auto_deploy=True,
|
|
allow_ephemeral_fallback=False,
|
|
enable_memory_snapshot=True,
|
|
enable_gpu_memory_snapshot=False,
|
|
execution_mode="local",
|
|
sync_custom_nodes=False,
|
|
volume_name="volume",
|
|
route_path="/modal/queue_prompt",
|
|
marker_property="is_modal_remote",
|
|
local_storage_root=tmp_path / "storage",
|
|
remote_storage_root="/storage",
|
|
custom_nodes_archive_name="custom_nodes_bundle.zip",
|
|
comfyui_root=None,
|
|
custom_nodes_dir=tmp_path / "custom_nodes",
|
|
)
|
|
settings.custom_nodes_dir.mkdir()
|
|
sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings)
|
|
fake_nodes_module = type(
|
|
"FakeNodesModule",
|
|
(),
|
|
{
|
|
"NODE_CLASS_MAPPINGS": {
|
|
"RemoteModel": _FakeRegressionRemoteModelNode,
|
|
"RemoteSampler": _FakeRegressionRemoteSamplerNode,
|
|
"LatentSource": _FakeRegressionLatentSourceNode,
|
|
"ModalMapInput": _FakeRegressionModalMapInputNode,
|
|
"LocalSink": _FakeRegressionLocalSinkNode,
|
|
},
|
|
"NODE_DISPLAY_NAME_MAPPINGS": {},
|
|
},
|
|
)()
|
|
|
|
rewritten_prompt, summary = api_intercept_module.rewrite_prompt_for_modal(
|
|
prompt=prompt,
|
|
workflow=workflow,
|
|
sync_engine=sync_engine,
|
|
settings=settings,
|
|
nodes_module=fake_nodes_module,
|
|
)
|
|
|
|
assert artifact_path.exists()
|
|
assert artifact["name"] == "hybrid_split_static_then_mapped"
|
|
assert {int(node["id"]) for node in workflow["nodes"] if node["properties"]["is_modal_remote"]} == {
|
|
1,
|
|
3,
|
|
6,
|
|
7,
|
|
}
|
|
assert rewritten_prompt["4"]["inputs"]["image"] == ["1", 0]
|
|
assert rewritten_prompt["8"]["inputs"]["image"] == ["1__mapped", 0]
|
|
assert summary.remote_component_ids == ["1", "1__mapped"]
|
|
assert summary.component_dependency_ids_by_representative == {
|
|
"1": [],
|
|
"1__mapped": ["1"],
|
|
}
|
|
assert summary.component_execution_stages == [["1"], ["1__mapped"]]
|
|
|
|
static_payload = rewritten_prompt["1"]["inputs"]["original_node_data"]
|
|
mapped_payload = rewritten_prompt["1__mapped"]["inputs"]["original_node_data"]
|
|
static_execution_payload = modal_executor_module._rehydrate_proxy_payload(
|
|
static_payload,
|
|
unique_id="1",
|
|
)
|
|
mapped_execution_payload = modal_executor_module._rehydrate_proxy_payload(
|
|
mapped_payload,
|
|
unique_id="1__mapped",
|
|
)
|
|
|
|
assert static_payload["component_node_ids"] == ["1", "3"]
|
|
assert mapped_payload["component_node_ids"] == ["6", "7"]
|
|
assert static_payload["boundary_outputs"][1]["session_output"] is True
|
|
assert "remote_session" not in static_payload
|
|
assert (
|
|
static_execution_payload["remote_session"]["session_id"]
|
|
== mapped_execution_payload["remote_session"]["session_id"]
|
|
)
|
|
assert "clear_remote_session" not in mapped_payload
|
|
assert mapped_execution_payload["clear_remote_session"] is True
|