"""Tests for prompt rewriting and asset sync integration.""" from __future__ import annotations import asyncio import json from pathlib import Path from types import SimpleNamespace from typing import Any import pytest class _FakeRemoteModelNode: """Fake node that produces a non-transportable MODEL output.""" RETURN_TYPES = ("MODEL",) RETURN_NAMES = ("model",) OUTPUT_IS_LIST = (False,) class _FakeCheckpointLoaderSimpleNode: """Fake root loader node that produces a non-transportable MODEL output.""" RETURN_TYPES = ("MODEL",) RETURN_NAMES = ("model",) OUTPUT_IS_LIST = (False,) class _FakeRemoteSamplerNode: """Fake node that consumes a model and produces a transportable latent.""" RETURN_TYPES = ("LATENT",) RETURN_NAMES = ("latent",) OUTPUT_IS_LIST = (False,) class _FakeVAELoaderNode: """Fake VAE loader that produces a non-transportable VAE output.""" RETURN_TYPES = ("VAE",) RETURN_NAMES = ("vae",) OUTPUT_IS_LIST = (False,) class _FakeVAEDecodeNode: """Fake VAE decoder that consumes latent and VAE inputs and produces an image.""" RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) OUTPUT_IS_LIST = (False,) class _FakeVAEEncodeNode: """Fake VAE encoder that consumes image and VAE inputs and produces a latent.""" RETURN_TYPES = ("LATENT",) RETURN_NAMES = ("latent",) OUTPUT_IS_LIST = (False,) class _FakeLatentSourceNode: """Fake node that produces a transportable LATENT output.""" RETURN_TYPES = ("LATENT",) RETURN_NAMES = ("latent",) OUTPUT_IS_LIST = (False,) class _FakeRemoteClipNode: """Fake node that produces a non-transportable CLIP output.""" RETURN_TYPES = ("CLIP",) RETURN_NAMES = ("clip",) OUTPUT_IS_LIST = (False,) class _FakeLocalSinkNode: """Fake local node used to verify downstream rewiring.""" RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) OUTPUT_IS_LIST = (False,) class _FakeRemoteConditioningNode: """Fake node that produces a non-transportable CONDITIONING output.""" RETURN_TYPES = ("CONDITIONING",) RETURN_NAMES = ("conditioning",) OUTPUT_IS_LIST = (False,) class _FakeRemoteImageNode: """Fake remote node that produces a transportable IMAGE output.""" RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) OUTPUT_IS_LIST = (False,) class _FakeRemoteImageConsumerNode: """Fake remote node that consumes IMAGE and produces IMAGE.""" RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) OUTPUT_IS_LIST = (False,) class _FakePreviewImageNode: """Fake preview node that behaves like a terminal UI output.""" RETURN_TYPES = () RETURN_NAMES = () OUTPUT_IS_LIST = () OUTPUT_NODE = True def test_rewrite_remote_mode_rejects_local_sync_backend( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Remote execution must not queue payloads whose synced assets only exist in local mirror storage.""" 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="remote", sync_custom_nodes=True, 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=None, ) sync_engine = sync_engine_module.ModalAssetSyncEngine( volume=sync_engine_module.LocalMirrorVolume(settings.local_storage_root), settings=settings, ) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": {"RemoteImage": _FakeRemoteImageNode}, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() with pytest.raises(api_intercept_module.ModalPromptValidationError) as exc_info: api_intercept_module.rewrite_prompt_for_modal( prompt={"1": {"class_type": "RemoteImage", "inputs": {}}}, workflow={"nodes": [{"id": 1, "properties": {"is_modal_remote": True}}]}, sync_engine=sync_engine, settings=settings, nodes_module=fake_nodes_module, ) assert "requires asset sync to use the Modal volume backend" in str(exc_info.value) class _FakeRemoteModelAndImageNode: """Fake remote node that produces both MODEL and IMAGE outputs.""" RETURN_TYPES = ("MODEL", "IMAGE") RETURN_NAMES = ("model", "image") OUTPUT_IS_LIST = (False, False) class _FakeRemoteModelAndImageConsumerNode: """Fake remote node that consumes MODEL and IMAGE and produces IMAGE.""" RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) OUTPUT_IS_LIST = (False,) class _FakePromptListNode: """Fake upstream node that represents a prompt-list producer.""" RETURN_TYPES = ("STRING",) RETURN_NAMES = ("text",) OUTPUT_IS_LIST = (False,) class _FakeModalMapInputNode: """Fake Modal map marker node that passes a wildcard value through.""" RETURN_TYPES = ("*",) RETURN_NAMES = ("value",) OUTPUT_IS_LIST = (False,) class _FakeRemoteStringEchoNode: """Fake remote node that echoes a string output.""" RETURN_TYPES = ("STRING",) RETURN_NAMES = ("text",) OUTPUT_IS_LIST = (False,) class _FakeLocalStringSinkNode: """Fake local node used to consume remote STRING outputs.""" RETURN_TYPES = ("STRING",) RETURN_NAMES = ("text",) OUTPUT_IS_LIST = (False,) def test_queue_prompt_json_includes_resolved_modal_metadata( api_intercept_module: Any, monkeypatch: Any, ) -> None: """Successful queue responses should include resolved remote nodes and component membership.""" class FakePromptQueue: """Minimal prompt queue sink.""" def __init__(self) -> None: """Initialize captured queue items.""" self.items: list[tuple[Any, ...]] = [] def put(self, item: tuple[Any, ...]) -> None: """Record one queued prompt item.""" self.items.append(item) class FakePromptServer: """Minimal PromptServer double for queue-response tests.""" def __init__(self) -> None: """Initialize queue state.""" self.number = 0 self.prompt_queue = FakePromptQueue() def trigger_on_prompt(self, json_data: dict[str, Any]) -> dict[str, Any]: """Return the prompt unchanged.""" return json_data class FakeExecutionModule: """Minimal execution module exposing prompt validation.""" SENSITIVE_EXTRA_DATA_KEYS: tuple[str, ...] = () @staticmethod async def validate_prompt( prompt_id: str, prompt: dict[str, Any], partial_execution_targets: Any, ) -> tuple[bool, None, list[str], list[Any]]: """Accept the supplied prompt with one fake execution target.""" return True, None, ["1"], [] monkeypatch.setattr(api_intercept_module, "_get_execution_module", lambda: FakeExecutionModule) prompt_server = FakePromptServer() response = asyncio.run( api_intercept_module._queue_prompt_json( prompt_server, { "prompt_id": "prompt-1", "prompt": {"1": {"class_type": "Anything", "inputs": {}}}, "extra_data": {}, }, modal_response_payload={ "modal_remote_node_ids": ["1", "2"], "modal_components": [ { "representative_node_id": "1", "node_ids": ["1", "2"], } ], }, ) ) response_payload = json.loads(response.text) assert response_payload["prompt_id"] == "prompt-1" assert response_payload["modal_remote_node_ids"] == ["1", "2"] assert response_payload["modal_components"] == [ { "representative_node_id": "1", "node_ids": ["1", "2"], } ] def test_rewritten_prompt_diagnostics_reports_dependency_cycles( api_intercept_module: Any, monkeypatch: pytest.MonkeyPatch, ) -> None: """Rewritten prompt diagnostics should name local dependency cycles before Comfy executes.""" prompt = { "1": { "class_type": "ModalUniversalExecutor_a", "inputs": {"remote_input_0": ["2", 0]}, }, "2": { "class_type": "ModalUniversalExecutor_b", "inputs": {"remote_input_0": ["1", 0]}, }, } diagnostics = api_intercept_module._modal_rewritten_prompt_diagnostics(prompt) assert diagnostics["cycles"] == [["1", "2", "1"]] warning_messages: list[str] = [] log_messages: list[str] = [] def record_warning(message: str, *args: Any, **_kwargs: Any) -> None: """Record one warning log message.""" warning_messages.append(message % args) def record_log(_level: int, message: str, *args: Any, **_kwargs: Any) -> None: """Record one generic log message.""" log_messages.append(message % args) monkeypatch.setattr(api_intercept_module.logger, "warning", record_warning) monkeypatch.setattr(api_intercept_module.logger, "log", record_log) api_intercept_module._log_modal_rewritten_prompt_diagnostics( prompt_id="prompt-cycle", prompt=prompt, reason="test", ) assert any("Modal rewritten prompt contains dependency cycle(s)" in item for item in warning_messages) assert any("prompt-cycle" in item for item in warning_messages) assert any("Modal rewritten prompt diagnostics" in item for item in log_messages) def test_queue_prompt_json_logs_rewritten_modal_diagnostics_on_validation_failure( api_intercept_module: Any, monkeypatch: pytest.MonkeyPatch, ) -> None: """Validation failures for Modal prompts should log the rewritten dependency graph.""" class FakePromptQueue: """Minimal prompt queue sink.""" def put(self, _item: tuple[Any, ...]) -> None: """Fail if an invalid prompt reaches the queue.""" raise AssertionError("invalid prompt must not be queued") class FakePromptServer: """Minimal PromptServer double for validation-failure tests.""" number = 0 prompt_queue = FakePromptQueue() def trigger_on_prompt(self, json_data: dict[str, Any]) -> dict[str, Any]: """Return the prompt unchanged.""" return json_data class FakeExecutionModule: """Minimal execution module that rejects the prompt.""" SENSITIVE_EXTRA_DATA_KEYS: tuple[str, ...] = () @staticmethod async def validate_prompt( prompt_id: str, prompt: dict[str, Any], partial_execution_targets: Any, ) -> tuple[bool, dict[str, Any], list[str], dict[str, Any]]: """Reject the supplied prompt with a dependency-cycle shaped error.""" del prompt_id, prompt, partial_execution_targets return ( False, { "type": "execution_error", "message": "Dependency cycle detected", "details": "", "extra_info": {}, }, [], {}, ) prompt = { "1": { "class_type": "ModalUniversalExecutor_a", "inputs": {"remote_input_0": ["2", 0]}, }, "2": { "class_type": "ModalUniversalExecutor_b", "inputs": {"remote_input_0": ["1", 0]}, }, } diagnostic_calls: list[dict[str, Any]] = [] def record_diagnostics(**kwargs: Any) -> None: """Record one rewritten-prompt diagnostics request.""" diagnostic_calls.append(dict(kwargs)) monkeypatch.setattr(api_intercept_module, "_get_execution_module", lambda: FakeExecutionModule) monkeypatch.setattr( api_intercept_module, "_log_modal_rewritten_prompt_diagnostics", record_diagnostics, ) response = asyncio.run( api_intercept_module._queue_prompt_json( FakePromptServer(), { "prompt_id": "prompt-cycle", "prompt": prompt, "extra_data": { "modal": { "remote_component_ids": ["1", "2"], } }, }, ) ) assert response.status == 400 assert diagnostic_calls == [ { "prompt_id": "prompt-cycle", "prompt": prompt, "reason": "comfy_validation_failure", "level": api_intercept_module.logging.WARNING, } ] def test_split_phase_order_accounts_for_local_feedback_dependencies( api_intercept_module: Any, ) -> None: """Split phase ordering should treat local re-entry paths as real dependencies.""" prompt = { "3": { "class_type": "RemoteSampler", "inputs": {"model": ["14", 0]}, }, "11": { "class_type": "VAEDecode", "inputs": {"samples": ["3", 0]}, }, "14": { "class_type": "RemoteModel", "inputs": {}, }, "191": { "class_type": "RemoteSampler", "inputs": {"model": ["14", 0], "conditioning": ["358", 0]}, }, "357": { "class_type": "BetterGrok", "inputs": {"prompt_images": ["11", 0]}, }, "358": { "class_type": "RemoteTextEncode", "inputs": {"text": ["357", 1]}, }, } component_prompt = { "3": prompt["3"], "14": prompt["14"], "191": prompt["191"], "358": prompt["358"], } ordered_execute_node_ids = ( api_intercept_module._order_execute_node_ids_for_transportable_splits( prompt=prompt, component_prompt=component_prompt, component_node_ids={"3", "14", "191", "358"}, execute_node_ids=["191", "3"], ) ) assert ordered_execute_node_ids == ["3", "191"] def test_queue_prompt_route_does_not_warm_modal_at_queue_time( api_intercept_module: Any, remote_modal_app_module: Any, settings_module: Any, monkeypatch: Any, tmp_path: Path, ) -> None: """Accepting a queued prompt should not launch Modal warmup containers.""" class FakeRoutes: """Capture aiohttp route registrations.""" def __init__(self) -> None: """Initialize the route handler map.""" self.handlers: dict[str, Any] = {} def post(self, path: str) -> Any: """Return a decorator that records one POST handler.""" def register(handler: Any) -> Any: """Store the decorated handler unchanged.""" self.handlers[path] = handler return handler return register class FakePromptQueue: """Minimal prompt queue sink.""" def __init__(self) -> None: """Initialize captured queue items.""" self.items: list[tuple[Any, ...]] = [] def put(self, item: tuple[Any, ...]) -> None: """Record one queued prompt item.""" self.items.append(item) class FakePromptServer: """Minimal PromptServer double with route registration.""" def __init__(self) -> None: """Initialize routing and queue state.""" self.number = 0 self.routes = FakeRoutes() self.prompt_queue = FakePromptQueue() def trigger_on_prompt(self, json_data: dict[str, Any]) -> dict[str, Any]: """Return the prompt unchanged.""" return json_data class FakeRequest: """Minimal aiohttp request double.""" async def json(self) -> dict[str, Any]: """Return one Modal-marked prompt request.""" return { "prompt_id": "prompt-queue-warmup", "prompt": {"1": {"class_type": "RemoteImage", "inputs": {}}}, "extra_data": { "extra_pnginfo": { "workflow": { "nodes": [ { "id": 1, "type": "RemoteImage", "properties": {"is_modal_remote": True}, } ] } } }, } class FakeExecutionModule: """Minimal execution module exposing prompt validation.""" SENSITIVE_EXTRA_DATA_KEYS: tuple[str, ...] = () @staticmethod async def validate_prompt( prompt_id: str, prompt: dict[str, Any], partial_execution_targets: Any, ) -> tuple[bool, None, list[str], list[Any]]: """Accept the supplied prompt with one fake execution target.""" return True, None, ["1"], [] def fail_queue_time_warmup(*_args: Any, **_kwargs: Any) -> int: """Fail the test if queue handling tries to launch proactive warmup.""" raise AssertionError("queue route must not schedule Modal warmup") 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="remote", 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=None, ) prompt_server = FakePromptServer() summary = api_intercept_module.RewriteSummary( remote_node_ids=["1"], remote_component_ids=["1"], component_node_ids_by_representative={"1": ["1"]}, component_execution_stages=[["1"]], estimated_max_parallel_requests=1, max_parallel_requests_upper_bound=1, ) monkeypatch.setattr(api_intercept_module, "_ROUTE_REGISTERED", False) monkeypatch.setattr( api_intercept_module, "_get_server_module", lambda: SimpleNamespace(PromptServer=SimpleNamespace(instance=prompt_server)), ) monkeypatch.setattr(api_intercept_module, "_get_execution_module", lambda: FakeExecutionModule) monkeypatch.setattr(api_intercept_module, "_emit_modal_status", lambda **_kwargs: None) monkeypatch.setattr( api_intercept_module, "rewrite_prompt_for_modal", lambda **kwargs: (kwargs["prompt"], summary), ) monkeypatch.setattr(remote_modal_app_module, "ensure_remote_warm_capacity", fail_queue_time_warmup) api_intercept_module.setup_modal_queue_route( prompt_server=prompt_server, sync_engine=object(), settings=settings, ) response = asyncio.run(prompt_server.routes.handlers["/modal/queue_prompt"](FakeRequest())) response_payload = json.loads(response.text) assert response_payload["prompt_id"] == "prompt-queue-warmup" assert response_payload["modal_remote_node_ids"] == ["1"] assert len(prompt_server.prompt_queue.items) == 1 def test_queue_prompt_route_without_remote_nodes_skips_modal_status_and_rewrite( api_intercept_module: Any, settings_module: Any, monkeypatch: Any, tmp_path: Path, ) -> None: """Prompts with no Modal-enabled workflow nodes should queue without Modal UI setup.""" class FakeRoutes: """Capture aiohttp route registrations.""" def __init__(self) -> None: """Initialize the route handler map.""" self.handlers: dict[str, Any] = {} def post(self, path: str) -> Any: """Return a decorator that records one POST handler.""" def register(handler: Any) -> Any: """Store the decorated handler unchanged.""" self.handlers[path] = handler return handler return register class FakePromptQueue: """Minimal prompt queue sink.""" def __init__(self) -> None: """Initialize captured queue items.""" self.items: list[tuple[Any, ...]] = [] def put(self, item: tuple[Any, ...]) -> None: """Record one queued prompt item.""" self.items.append(item) class FakePromptServer: """Minimal PromptServer double with route registration.""" def __init__(self) -> None: """Initialize routing and queue state.""" self.number = 0 self.routes = FakeRoutes() self.prompt_queue = FakePromptQueue() def trigger_on_prompt(self, json_data: dict[str, Any]) -> dict[str, Any]: """Return the prompt unchanged.""" return json_data class FakeRequest: """Minimal aiohttp request double.""" async def json(self) -> dict[str, Any]: """Return one ordinary prompt request.""" return { "prompt_id": "prompt-no-modal", "prompt": {"1": {"class_type": "LocalImage", "inputs": {}}}, "extra_data": {"extra_pnginfo": {"workflow": {"nodes": []}}}, } class FakeExecutionModule: """Minimal execution module exposing prompt validation.""" SENSITIVE_EXTRA_DATA_KEYS: tuple[str, ...] = () @staticmethod async def validate_prompt( prompt_id: str, prompt: dict[str, Any], partial_execution_targets: Any, ) -> tuple[bool, None, list[str], list[Any]]: """Accept the supplied prompt with one fake execution target.""" return True, None, ["1"], [] def fail_modal_status(*_args: Any, **_kwargs: Any) -> None: """Fail if the no-remote fast path emits Modal UI state.""" raise AssertionError("no-remote prompts must not emit Modal status") def fail_rewrite(*_args: Any, **_kwargs: Any) -> tuple[dict[str, Any], Any]: """Fail if the no-remote fast path enters Modal prompt rewriting.""" raise AssertionError("no-remote prompts must not be rewritten for Modal") 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="remote", 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=None, ) prompt_server = FakePromptServer() monkeypatch.setattr(api_intercept_module, "_ROUTE_REGISTERED", False) monkeypatch.setattr( api_intercept_module, "_get_server_module", lambda: SimpleNamespace(PromptServer=SimpleNamespace(instance=prompt_server)), ) monkeypatch.setattr(api_intercept_module, "_get_execution_module", lambda: FakeExecutionModule) monkeypatch.setattr(api_intercept_module, "_emit_modal_status", fail_modal_status) monkeypatch.setattr(api_intercept_module, "rewrite_prompt_for_modal", fail_rewrite) api_intercept_module.setup_modal_queue_route( prompt_server=prompt_server, sync_engine=object(), settings=settings, ) response = asyncio.run(prompt_server.routes.handlers["/modal/queue_prompt"](FakeRequest())) response_payload = json.loads(response.text) assert response_payload["prompt_id"] == "prompt-no-modal" assert "modal_remote_node_ids" not in response_payload assert len(prompt_server.prompt_queue.items) == 1 def test_rewrite_groups_connected_remote_nodes_into_single_proxy( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Connected remote nodes should collapse into one proxy-backed component.""" model_path = tmp_path / "weights.safetensors" model_path.write_bytes(b"weights") custom_nodes_dir = tmp_path / "custom_nodes" custom_nodes_dir.mkdir() (custom_nodes_dir / "__init__.py").write_text("NODE_CLASS_MAPPINGS = {}\n", encoding="utf-8") 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=custom_nodes_dir, ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteModel": _FakeRemoteModelNode, "RemoteSampler": _FakeRemoteSamplerNode, "LocalSink": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteModel", "inputs": {"model_name": str(model_path)}, "_meta": {"title": "Model"}, }, "2": { "class_type": "RemoteSampler", "inputs": {"model": ["1", 0]}, "_meta": {"title": "Sampler"}, }, "3": { "class_type": "LocalSink", "inputs": {"latent": ["2", 0]}, "_meta": {"title": "Sink"}, }, } 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, extra_data={"extra_pnginfo": {"workflow": workflow}}, ) assert set(rewritten_prompt) == {"1", "3"} rewritten_node = rewritten_prompt["1"] payload = rewritten_node["inputs"]["original_node_data"] assert rewritten_node["class_type"].startswith("ModalUniversalExecutor_") assert payload["payload_kind"] == "subgraph" assert "prompt_id" not in payload assert payload["component_node_ids"] == ["1", "2"] assert payload["subgraph_prompt"]["1"]["inputs"]["model_name"].startswith("/assets/") assert payload["execute_node_ids"] == ["2"] assert "requires_volume_reload" not in payload assert "volume_reload_marker" not in payload assert "uploaded_volume_paths" not in payload assert payload["terminate_container_on_error"] is True assert payload["boundary_inputs"] == [] assert payload["boundary_outputs"] == [ { "proxy_output_name": "2_latent", "node_id": "2", "output_index": 0, "io_type": "LATENT", "is_list": False, "preview_target_node_ids": [], } ] assert rewritten_prompt["3"]["inputs"]["latent"] == ["1", 0] assert summary.remote_node_ids == ["1", "2"] assert summary.remote_component_ids == ["1"] assert summary.component_node_ids_by_representative == {"1": ["1", "2"]} assert summary.rewritten_node_id_map == {"1": "1", "2": "1"} assert len(summary.synced_assets) == 1 assert summary.synced_assets[0].uploaded is True def test_rewrite_strips_prompt_id_from_cache_safe_proxy_payload( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Cache-safe remote proxies should not bake prompt_id into original_node_data inputs.""" 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", ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteModel": _FakeRemoteModelNode, "RemoteSampler": _FakeRemoteSamplerNode, "LocalSink": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteModel", "inputs": {}, "_meta": {"title": "Model"}, }, "2": { "class_type": "RemoteSampler", "inputs": {"model": ["1", 0]}, "_meta": {"title": "Sampler"}, }, "3": { "class_type": "LocalSink", "inputs": {"latent": ["2", 0]}, "_meta": {"title": "Sink"}, }, } 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, extra_data={"prompt_id": "prompt-1", "client_id": "client-1"}, ) payload = rewritten_prompt["1"]["inputs"]["original_node_data"] assert "prompt_id" not in payload def test_rewrite_records_local_preview_targets_for_remote_boundary_images( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Boundary IMAGE outputs should remember direct local PreviewImage consumers.""" 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": { "RemoteImage": _FakeRemoteImageNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 9, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteImage", "inputs": {}, "_meta": {"title": "Remote Image"}, }, "9": { "class_type": "PreviewImage", "inputs": {"images": ["1", 0]}, "_meta": {"title": "Preview"}, }, } 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, ) payload = rewritten_prompt["1"]["inputs"]["original_node_data"] assert payload["boundary_outputs"] == [ { "proxy_output_name": "1_image", "node_id": "1", "output_index": 0, "io_type": "IMAGE", "is_list": False, "preview_target_node_ids": ["9"], } ] def test_rewrite_splits_remote_chain_across_transportable_edges( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Transportable remote-to-remote edges should become ordered proxy boundaries.""" 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": { "RemoteImage": _FakeRemoteImageNode, "RemoteImageConsumer": _FakeRemoteImageConsumerNode, "LocalSink": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteImage", "inputs": {}, "_meta": {"title": "Remote Image"}, }, "2": { "class_type": "RemoteImageConsumer", "inputs": {"image": ["1", 0]}, "_meta": {"title": "Remote Image Consumer"}, }, "3": { "class_type": "LocalSink", "inputs": {"image": ["2", 0]}, "_meta": {"title": "Local Sink"}, }, } 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 set(rewritten_prompt) == {"1", "2", "3"} assert summary.remote_component_ids == ["1", "2"] assert summary.component_node_ids_by_representative == {"1": ["1"], "2": ["2"]} assert summary.rewritten_node_id_map == {"1": "1", "2": "2"} first_payload = rewritten_prompt["1"]["inputs"]["original_node_data"] second_payload = rewritten_prompt["2"]["inputs"]["original_node_data"] assert first_payload["component_node_ids"] == ["1"] assert first_payload["boundary_inputs"] == [] assert first_payload["boundary_outputs"] == [ { "proxy_output_name": "1_image", "node_id": "1", "output_index": 0, "io_type": "IMAGE", "is_list": False, "preview_target_node_ids": [], } ] assert second_payload["component_node_ids"] == ["2"] assert second_payload["boundary_inputs"] == [ { "proxy_input_name": "remote_input_0", "io_type": "IMAGE", "targets": [{"node_id": "2", "input_name": "image"}], } ] assert second_payload["boundary_outputs"] == [ { "proxy_output_name": "2_image", "node_id": "2", "output_index": 0, "io_type": "IMAGE", "is_list": False, "preview_target_node_ids": [], } ] assert rewritten_prompt["2"]["inputs"]["remote_input_0"] == ["1", 0] assert rewritten_prompt["3"]["inputs"]["image"] == ["2", 0] def test_rewrite_keeps_non_returning_local_preview_taps_local( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Local preview branches should stay local even when a remote chain continues.""" 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": { "RemoteImage": _FakeRemoteImageNode, "RemoteImageConsumer": _FakeRemoteImageConsumerNode, "LocalSink": _FakeLocalSinkNode, "PreviewImage": _FakePreviewImageNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": False}}, {"id": 9, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteImage", "inputs": {}, "_meta": {"title": "Remote Image"}, }, "2": { "class_type": "RemoteImageConsumer", "inputs": {"image": ["1", 0]}, "_meta": {"title": "Remote Image Consumer"}, }, "3": { "class_type": "LocalSink", "inputs": {"image": ["2", 0]}, "_meta": {"title": "Local Sink"}, }, "9": { "class_type": "PreviewImage", "inputs": {"images": ["1", 0]}, "_meta": {"title": "Interim Preview"}, }, } 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 set(rewritten_prompt) == {"1", "2", "3", "9"} assert summary.remote_component_ids == ["1", "2"] assert summary.component_node_ids_by_representative == {"1": ["1"], "2": ["2"]} assert summary.rewritten_node_id_map == {"1": "1", "2": "2"} remote_payloads = [ rewritten_node["inputs"]["original_node_data"] for rewritten_node in rewritten_prompt.values() if isinstance(rewritten_node.get("inputs"), dict) and "original_node_data" in rewritten_node["inputs"] ] assert remote_payloads assert all("9" not in payload["component_node_ids"] for payload in remote_payloads) assert all("9" not in payload["subgraph_prompt"] for payload in remote_payloads) assert all("9" not in payload["execute_node_ids"] for payload in remote_payloads) assert rewritten_prompt["9"]["inputs"]["images"] == ["1", 0] assert rewritten_prompt["3"]["inputs"]["image"] == ["2", 0] def test_rewrite_keeps_unmarked_preview_subgraph_nodes_local( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Unmarked preview producer nodes must not execute remotely.""" 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": { "RemoteSampler": _FakeRemoteSamplerNode, "LocalSink": _FakeLocalSinkNode, "PreviewImage": _FakePreviewImageNode, "VAEDecode": _FakeVAEDecodeNode, "VAEEncode": _FakeVAEEncodeNode, "VAELoader": _FakeVAELoaderNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": False}}, {"id": 7, "properties": {"is_modal_remote": False}}, {"id": 8, "properties": {"is_modal_remote": False}}, {"id": 9, "properties": {"is_modal_remote": False}}, {"id": 11, "properties": {"is_modal_remote": False}}, {"id": 90, "properties": {"is_modal_remote": False}}, {"id": 192, "properties": {"is_modal_remote": False}}, ] } prompt = { "9": { "class_type": "VAELoader", "inputs": {"vae_name": "vae.safetensors"}, "_meta": {"title": "VAE Loader"}, }, "1": { "class_type": "RemoteSampler", "inputs": {}, "_meta": {"title": "Remote Sampler 1"}, }, "192": { "class_type": "VAEDecode", "inputs": {"samples": ["1", 0], "vae": ["9", 0]}, "_meta": {"title": "VAE Decode Preview"}, }, "8": { "class_type": "VAEEncode", "inputs": {"pixels": ["192", 0], "vae": ["9", 0]}, "_meta": {"title": "Local VAE Encode"}, }, "7": { "class_type": "LocalSink", "inputs": {"image": ["8", 0]}, "_meta": {"title": "Local Encoded Sink"}, }, "11": { "class_type": "VAEDecode", "inputs": {"samples": ["1", 0], "vae": ["9", 0]}, "_meta": {"title": "Local VAE Decode"}, }, "90": { "class_type": "PreviewImage", "inputs": {"images": ["192", 0]}, "_meta": {"title": "Preview"}, }, "2": { "class_type": "RemoteSampler", "inputs": {"latent": ["1", 0]}, "_meta": {"title": "Remote Sampler 2"}, }, "3": { "class_type": "LocalSink", "inputs": {"image": ["2", 0]}, "_meta": {"title": "Local Sink"}, }, } 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 set(rewritten_prompt) == {"1", "2", "3", "7", "8", "9", "11", "90", "192"} assert summary.remote_component_ids == ["1", "2"] assert summary.component_node_ids_by_representative == {"1": ["1"], "2": ["2"]} assert summary.rewritten_node_id_map == {"1": "1", "2": "2"} remote_payloads = [ rewritten_node["inputs"]["original_node_data"] for rewritten_node in rewritten_prompt.values() if isinstance(rewritten_node.get("inputs"), dict) and "original_node_data" in rewritten_node["inputs"] ] local_node_ids = {"7", "8", "9", "11", "90", "192"} assert len(remote_payloads) == 2 for payload in remote_payloads: assert not (local_node_ids & set(payload["component_node_ids"])) assert not (local_node_ids & set(payload["subgraph_prompt"])) assert not (local_node_ids & set(payload["execute_node_ids"])) assert rewritten_prompt["90"]["inputs"]["images"] == ["192", 0] assert rewritten_prompt["3"]["inputs"]["image"] == ["2", 0] def test_rewrite_keeps_local_branches_that_feed_remote_as_boundaries( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Local branches that later feed remote work are dependencies, not preview taps.""" 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": { "RemoteImage": _FakeRemoteImageNode, "RemoteImageConsumer": _FakeRemoteImageConsumerNode, "LocalSink": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": False}}, {"id": 4, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteImage", "inputs": {}, "_meta": {"title": "Remote Image"}, }, "4": { "class_type": "LocalSink", "inputs": {"image": ["1", 0]}, "_meta": {"title": "Local Transform"}, }, "2": { "class_type": "RemoteImageConsumer", "inputs": {"image": ["4", 0]}, "_meta": {"title": "Remote Image Consumer"}, }, "3": { "class_type": "LocalSink", "inputs": {"image": ["2", 0]}, "_meta": {"title": "Local Sink"}, }, } 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 set(rewritten_prompt) == {"1", "2", "3", "4"} assert summary.remote_component_ids == ["1", "2"] assert summary.component_node_ids_by_representative == {"1": ["1"], "2": ["2"]} assert summary.rewritten_node_id_map == {"1": "1", "2": "2"} assert rewritten_prompt["4"]["inputs"]["image"] == ["1", 0] assert rewritten_prompt["2"]["inputs"]["remote_input_0"] == ["4", 0] assert rewritten_prompt["3"]["inputs"]["image"] == ["2", 0] second_payload = rewritten_prompt["2"]["inputs"]["original_node_data"] assert second_payload["component_node_ids"] == ["2"] assert second_payload["boundary_inputs"] == [ { "proxy_input_name": "remote_input_0", "io_type": "IMAGE", "targets": [{"node_id": "2", "input_name": "image"}], } ] def test_component_local_reentry_dependency_detection( api_intercept_module: Any, ) -> None: """Boundary inputs that trace back to the same component require a split-capable proxy.""" prompt = { "1": { "class_type": "RemoteImage", "inputs": {}, "_meta": {"title": "Remote Image"}, }, "4": { "class_type": "LocalSink", "inputs": {"image": ["1", 0]}, "_meta": {"title": "Local Transform"}, }, "2": { "class_type": "RemoteImageConsumer", "inputs": {"image": ["4", 0]}, "_meta": {"title": "Remote Consumer"}, }, } component = api_intercept_module.RemoteComponentPlan( node_ids=["1", "2"], representative_node_id="1", boundary_inputs=[ api_intercept_module.BoundaryInputSpec( proxy_input_name="remote_input_0", source=api_intercept_module.LinkedOutputRef(node_id="4", output_index=0), io_type="IMAGE", targets=[ api_intercept_module.InputTarget( node_id="2", input_name="image", ) ], ) ], boundary_outputs=[], execute_node_ids=["1", "2"], contains_output_node=False, local_tap_node_ids=["9"], ) assert api_intercept_module._component_has_local_reentry_dependency( prompt=prompt, component=component, ) def test_rewrite_reports_parallel_component_stages( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Prompt rewrites should report best-effort concurrent stages for independent remote components.""" 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": { "RemoteImage": _FakeRemoteImageNode, "LocalSink": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": False}}, {"id": 4, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": {"class_type": "RemoteImage", "inputs": {}, "_meta": {"title": "Remote A"}}, "2": {"class_type": "RemoteImage", "inputs": {}, "_meta": {"title": "Remote B"}}, "3": {"class_type": "LocalSink", "inputs": {"image": ["1", 0]}, "_meta": {"title": "Sink A"}}, "4": {"class_type": "LocalSink", "inputs": {"image": ["2", 0]}, "_meta": {"title": "Sink B"}}, } _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 summary.component_execution_stages == [["1", "2"]] assert summary.component_dependency_ids_by_representative == {"1": [], "2": []} assert summary.mapped_component_ids == [] assert summary.estimated_max_parallel_requests == 2 assert summary.max_parallel_requests_upper_bound == 2 def test_rewrite_reports_mapped_parallelism_upper_bound( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Mapped components should warm only the single container needed for one in-process mapped run.""" 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", max_containers=5, ) settings.custom_nodes_dir.mkdir() sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "PromptList": _FakePromptListNode, "ModalMapInput": _FakeModalMapInputNode, "RemoteStringEcho": _FakeRemoteStringEchoNode, "LocalStringSink": _FakeLocalStringSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": False}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": True}}, {"id": 4, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": {"class_type": "PromptList", "inputs": {}, "_meta": {"title": "Prompt List"}}, "2": {"class_type": "ModalMapInput", "inputs": {"value": ["1", 0]}, "_meta": {"title": "Map"}}, "3": {"class_type": "RemoteStringEcho", "inputs": {"text": ["2", 0]}, "_meta": {"title": "Echo"}}, "4": {"class_type": "LocalStringSink", "inputs": {"text": ["3", 0]}, "_meta": {"title": "Sink"}}, } _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 summary.component_execution_stages == [["2"]] assert summary.mapped_component_ids == ["2"] assert summary.estimated_max_parallel_requests == 1 assert summary.max_parallel_requests_upper_bound == 1 def test_rewrite_uses_one_request_wide_volume_reload_marker_across_components( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, monkeypatch: Any, tmp_path: Path, ) -> None: """All components in one rewritten prompt should share one reload marker and decision.""" 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", ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteImage": _FakeRemoteImageNode, "RemoteImageConsumer": _FakeRemoteImageConsumerNode, "LocalSink": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteImage", "inputs": {}, "_meta": {"title": "Remote Image"}, }, "2": { "class_type": "RemoteImageConsumer", "inputs": {"image": ["1", 0]}, "_meta": {"title": "Remote Image Consumer"}, }, "3": { "class_type": "LocalSink", "inputs": {"image": ["2", 0]}, "_meta": {"title": "Local Sink"}, }, } uploaded_asset = sync_engine_module.SyncedAsset( local_path=tmp_path / "uploaded.bin", remote_path="/assets/uploaded.bin", sha256="uploaded", uploaded=True, ) def fake_sync_component_prompt_inputs( *, component: Any, rewritten_prompt: dict[str, Any], sync_engine: Any, status_callback: Any = None, ) -> tuple[dict[str, Any], list[Any]]: if component.representative_node_id == "1": return {"1": rewritten_prompt["1"]}, [] return { "2": { "class_type": rewritten_prompt["2"]["class_type"], "inputs": {"image_path": uploaded_asset.remote_path}, "_meta": rewritten_prompt["2"]["_meta"], } }, [uploaded_asset] monkeypatch.setattr( api_intercept_module, "_sync_component_prompt_inputs", fake_sync_component_prompt_inputs, ) 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, ) first_payload = rewritten_prompt["1"]["inputs"]["original_node_data"] second_payload = rewritten_prompt["2"]["inputs"]["original_node_data"] assert summary.remote_component_ids == ["1", "2"] assert summary.synced_assets == [uploaded_asset] assert "requires_volume_reload" not in first_payload assert "requires_volume_reload" not in second_payload assert "volume_reload_marker" not in first_payload assert "volume_reload_marker" not in second_payload assert "uploaded_volume_paths" not in first_payload assert "uploaded_volume_paths" not in second_payload assert summary.requires_volume_reload is True assert isinstance(summary.volume_reload_marker, str) assert summary.volume_reload_marker assert summary.uploaded_volume_paths == [uploaded_asset.remote_path] def test_rewrite_merges_cyclic_coarse_components_back_into_single_proxy( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """A cyclic quotient between coarse groups should collapse back into one remote proxy.""" 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": { "RemoteModelAndImage": _FakeRemoteModelAndImageNode, "RemoteImageConsumer": _FakeRemoteImageConsumerNode, "RemoteModelAndImageConsumer": _FakeRemoteModelAndImageConsumerNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": True}}, {"id": 4, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteModelAndImage", "inputs": {}, "_meta": {"title": "Remote Model And Image"}, }, "2": { "class_type": "RemoteImageConsumer", "inputs": {"image": ["1", 1]}, "_meta": {"title": "Remote Image Consumer"}, }, "3": { "class_type": "RemoteModelAndImageConsumer", "inputs": {"model": ["1", 0], "image": ["2", 0]}, "_meta": {"title": "Remote Model And Image Consumer"}, }, "4": { "class_type": "PreviewImage", "inputs": {"images": ["3", 0]}, "_meta": {"title": "Preview"}, }, } 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 set(rewritten_prompt) == {"1", "4"} assert summary.remote_component_ids == ["1"] assert summary.component_node_ids_by_representative == {"1": ["1", "2", "3"]} payload = rewritten_prompt["1"]["inputs"]["original_node_data"] assert payload["component_node_ids"] == ["1", "2", "3"] assert payload["boundary_inputs"] == [] assert payload["boundary_outputs"] == [ { "proxy_output_name": "3_image", "node_id": "3", "output_index": 0, "io_type": "IMAGE", "is_list": False, "preview_target_node_ids": ["4"], } ] assert rewritten_prompt["4"]["inputs"]["images"] == ["1", 0] def test_rewrite_marks_modal_map_boundary_as_mapped_subgraph( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """A remote component fed through ModalMapInput should rewrite to a mapped payload.""" 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": { "PromptList": _FakePromptListNode, "ModalMapInput": _FakeModalMapInputNode, "RemoteStringEcho": _FakeRemoteStringEchoNode, "LocalStringSink": _FakeLocalStringSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": False}}, {"id": 2, "properties": {"is_modal_remote": True}}, {"id": 3, "properties": {"is_modal_remote": True}}, {"id": 5, "properties": {"is_modal_remote": True}}, {"id": 4, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "PromptList", "inputs": {}, "_meta": {"title": "Prompt List"}, }, "2": { "class_type": "ModalMapInput", "inputs": {"value": ["1", 0]}, "_meta": {"title": "Map Input"}, }, "3": { "class_type": "RemoteStringEcho", "inputs": {"text": ["2", 0]}, "_meta": {"title": "Remote Echo"}, }, "5": { "class_type": "RemoteStringEcho", "inputs": {"text": ["3", 0]}, "_meta": {"title": "Remote Echo 2"}, }, "4": { "class_type": "LocalStringSink", "inputs": {"text": ["5", 0]}, "_meta": {"title": "Local Sink"}, }, } 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 set(rewritten_prompt) == {"1", "2", "4"} assert summary.remote_component_ids == ["2"] payload = rewritten_prompt["2"]["inputs"]["original_node_data"] assert payload["payload_kind"] == "mapped_subgraph" assert payload["component_node_ids"] == ["2", "3", "5"] assert payload["mapped_input"] == { "proxy_input_name": "remote_input_0", "io_type": "STRING", } assert payload["boundary_inputs"] == [ { "proxy_input_name": "remote_input_0", "io_type": "STRING", "targets": [{"node_id": "2", "input_name": "value"}], } ] assert payload["static_to_mapped_boundaries"] == [] assert payload["static_phase"] == { "component_node_ids": [], "subgraph_prompt": {}, "boundary_inputs": [], "boundary_outputs": [], "execute_node_ids": [], } assert payload["mapped_phase"] == { "component_node_ids": ["2", "3", "5"], "subgraph_prompt": { "2": prompt["2"], "3": prompt["3"], "5": prompt["5"], }, "boundary_inputs": [ { "proxy_input_name": "remote_input_0", "io_type": "STRING", "targets": [{"node_id": "2", "input_name": "value"}], } ], "boundary_outputs": [ { "proxy_output_name": "5_text", "node_id": "5", "output_index": 0, "io_type": "STRING", "is_list": False, "preview_target_node_ids": [], "mapped_output": True, } ], "execute_node_ids": ["5"], } assert rewritten_prompt["4"]["inputs"]["text"] == ["2", 0] def test_rewrite_marks_local_modal_map_source_as_mapped_subgraph( api_intercept_module: Any, modal_executor_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """A local ModalMapInput feeding a remote node should still rewrite to mapped remote execution.""" 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": { "PromptList": _FakePromptListNode, "ModalMapInput": _FakeModalMapInputNode, "RemoteStringEcho": _FakeRemoteStringEchoNode, "LocalStringSink": _FakeLocalStringSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": False}}, {"id": 2, "properties": {"is_modal_remote": False}}, {"id": 3, "properties": {"is_modal_remote": True}}, {"id": 4, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "PromptList", "inputs": {}, "_meta": {"title": "Prompt List"}, }, "2": { "class_type": "ModalMapInput", "inputs": {"value": ["1", 0]}, "_meta": {"title": "Map Input"}, }, "3": { "class_type": "RemoteStringEcho", "inputs": {"text": ["2", 0]}, "_meta": {"title": "Remote Echo"}, }, "4": { "class_type": "LocalStringSink", "inputs": {"text": ["3", 0]}, "_meta": {"title": "Local Sink"}, }, } 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 set(rewritten_prompt) == {"1", "2", "3", "4"} assert summary.remote_component_ids == ["3"] payload = rewritten_prompt["3"]["inputs"]["original_node_data"] assert payload["payload_kind"] == "mapped_subgraph" assert payload["component_node_ids"] == ["3"] assert payload["mapped_input"] == { "proxy_input_name": "remote_input_0", "io_type": "STRING", } assert payload["boundary_inputs"] == [ { "proxy_input_name": "remote_input_0", "io_type": "*", "targets": [{"node_id": "3", "input_name": "text"}], } ] assert payload["mapped_phase"] == { "component_node_ids": ["3"], "subgraph_prompt": { "3": prompt["3"], }, "boundary_inputs": [ { "proxy_input_name": "remote_input_0", "io_type": "*", "targets": [{"node_id": "3", "input_name": "text"}], } ], "boundary_outputs": [ { "proxy_output_name": "3_text", "node_id": "3", "output_index": 0, "io_type": "STRING", "is_list": False, "preview_target_node_ids": [], "mapped_output": True, } ], "execute_node_ids": ["3"], } with modal_executor_module._MODAL_MAP_WARMUP_CONTEXTS_LOCK: warmup_context = modal_executor_module._MODAL_MAP_WARMUP_CONTEXTS["2"] assert warmup_context.mapped_io_type == "STRING" assert warmup_context.execution_payload["component_id"] == "3" assert rewritten_prompt["4"]["inputs"]["text"] == ["3", 0] def test_rewrite_supports_mapped_branch_that_shares_non_transportable_upstream_with_unmapped_sibling( api_intercept_module: Any, modal_executor_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Mapped execution should separate static and per-item execute targets within one coarse component.""" 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", ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteModel": _FakeRemoteModelNode, "RemoteSampler": _FakeRemoteSamplerNode, "LatentSource": _FakeLatentSourceNode, "ModalMapInput": _FakeModalMapInputNode, "LocalSink": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": False}}, {"id": 3, "properties": {"is_modal_remote": True}}, {"id": 4, "properties": {"is_modal_remote": False}}, {"id": 5, "properties": {"is_modal_remote": False}}, {"id": 6, "properties": {"is_modal_remote": True}}, {"id": 7, "properties": {"is_modal_remote": True}}, {"id": 8, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteModel", "inputs": {}, "_meta": {"title": "Shared Model"}, }, "2": { "class_type": "LatentSource", "inputs": {}, "_meta": {"title": "Single Latent"}, }, "3": { "class_type": "RemoteSampler", "inputs": {"model": ["1", 0], "latent": ["2", 0]}, "_meta": {"title": "Unmapped Sampler"}, }, "4": { "class_type": "LocalSink", "inputs": {"image": ["3", 0]}, "_meta": {"title": "Local Sink 1"}, }, "5": { "class_type": "LatentSource", "inputs": {}, "_meta": {"title": "Batch Latent Source"}, }, "6": { "class_type": "ModalMapInput", "inputs": {"value": ["5", 0]}, "_meta": {"title": "Map Input"}, }, "7": { "class_type": "RemoteSampler", "inputs": {"model": ["1", 0], "latent": ["6", 0]}, "_meta": {"title": "Mapped Sampler"}, }, "8": { "class_type": "LocalSink", "inputs": {"image": ["7", 0]}, "_meta": {"title": "Local Sink 2"}, }, } 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 set(rewritten_prompt) == {"1", "1__mapped", "2", "4", "5", "8"} assert summary.remote_component_ids == ["1", "1__mapped"] assert summary.component_node_ids_by_representative == { "1": ["1", "3"], "1__mapped": ["6", "7"], } assert summary.component_dependency_ids_by_representative == { "1": [], "1__mapped": ["1"], } assert summary.component_execution_stages == [["1"], ["1__mapped"]] assert summary.mapped_component_ids == ["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["payload_kind"] == "subgraph" assert static_payload["component_node_ids"] == ["1", "3"] assert static_payload["boundary_inputs"] == [ { "proxy_input_name": "remote_input_0", "io_type": "LATENT", "targets": [{"node_id": "3", "input_name": "latent"}], } ] assert static_payload["boundary_outputs"] == [ { "proxy_output_name": "3_latent", "node_id": "3", "output_index": 0, "io_type": "LATENT", "is_list": False, "preview_target_node_ids": [], }, { "proxy_output_name": "static_input_0", "node_id": "1", "output_index": 0, "io_type": "MODEL", "is_list": False, "preview_target_node_ids": [], "session_output": True, }, ] assert static_payload["execute_node_ids"] == ["1", "3"] assert "remote_session" not in static_payload assert static_execution_payload["remote_session"]["owner_component_id"] == "1" assert mapped_payload["payload_kind"] == "subgraph" assert mapped_payload["component_node_ids"] == ["6", "7"] assert mapped_payload["boundary_inputs"] == [ { "proxy_input_name": "remote_input_1", "io_type": "LATENT", "targets": [{"node_id": "6", "input_name": "value"}], }, { "proxy_input_name": "static_input_0", "io_type": "MODEL", "targets": [{"node_id": "7", "input_name": "model"}], "source_signature": api_intercept_module._boundary_source_signature( prompt, api_intercept_module.LinkedOutputRef(node_id="1", output_index=0), ), }, ] assert mapped_payload["boundary_outputs"] == [ { "proxy_output_name": "7_latent", "node_id": "7", "output_index": 0, "io_type": "LATENT", "is_list": False, "preview_target_node_ids": [], } ] assert mapped_payload["execute_node_ids"] == ["7"] assert "clear_remote_session" not in mapped_payload assert mapped_payload["mapped_progress_display_node_id"] == "1" assert mapped_execution_payload["clear_remote_session"] is True assert ( mapped_execution_payload["remote_session"]["session_id"] == static_execution_payload["remote_session"]["session_id"] ) assert rewritten_prompt["1__mapped"]["inputs"]["remote_input_0"] == ["2", 0] assert rewritten_prompt["1__mapped"]["inputs"]["static_input_0"] == ["1", 1] assert rewritten_prompt["4"]["inputs"]["image"] == ["1", 0] assert rewritten_prompt["8"]["inputs"]["image"] == ["1__mapped", 0] def test_rewrite_stamps_snapshot_profile_on_split_static_and_mapped_payloads( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Split static and mapped payloads should inherit the same loader snapshot profile.""" settings = settings_module.ModalSyncSettings( app_name="app", auto_deploy=True, allow_ephemeral_fallback=False, enable_memory_snapshot=True, enable_gpu_memory_snapshot=True, 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", ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "CheckpointLoaderSimple": _FakeCheckpointLoaderSimpleNode, "RemoteSampler": _FakeRemoteSamplerNode, "LatentSource": _FakeLatentSourceNode, "ModalMapInput": _FakeModalMapInputNode, "LocalSink": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": False}}, {"id": 3, "properties": {"is_modal_remote": True}}, {"id": 4, "properties": {"is_modal_remote": False}}, {"id": 5, "properties": {"is_modal_remote": False}}, {"id": 6, "properties": {"is_modal_remote": True}}, {"id": 7, "properties": {"is_modal_remote": True}}, {"id": 8, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "base.safetensors"}, "_meta": {"title": "Checkpoint"}, }, "2": { "class_type": "LatentSource", "inputs": {}, "_meta": {"title": "Single Latent"}, }, "3": { "class_type": "RemoteSampler", "inputs": {"model": ["1", 0], "latent": ["2", 0]}, "_meta": {"title": "Unmapped Sampler"}, }, "4": { "class_type": "LocalSink", "inputs": {"image": ["3", 0]}, "_meta": {"title": "Local Sink 1"}, }, "5": { "class_type": "LatentSource", "inputs": {}, "_meta": {"title": "Batch Latent Source"}, }, "6": { "class_type": "ModalMapInput", "inputs": {"value": ["5", 0]}, "_meta": {"title": "Map Input"}, }, "7": { "class_type": "RemoteSampler", "inputs": {"model": ["1", 0], "latent": ["6", 0]}, "_meta": {"title": "Mapped Sampler"}, }, "8": { "class_type": "LocalSink", "inputs": {"image": ["7", 0]}, "_meta": {"title": "Local Sink 2"}, }, } rewritten_prompt, _ = api_intercept_module.rewrite_prompt_for_modal( prompt=prompt, workflow=workflow, sync_engine=sync_engine, settings=settings, nodes_module=fake_nodes_module, ) static_payload = rewritten_prompt["1"]["inputs"]["original_node_data"] mapped_payload = rewritten_prompt["1__mapped"]["inputs"]["original_node_data"] assert static_payload["snapshot_profile_key"].startswith("loader-profile:") assert mapped_payload["snapshot_profile_key"] == static_payload["snapshot_profile_key"] def test_rewrite_splits_unmapped_remote_siblings_that_share_non_transportable_upstream( api_intercept_module: Any, modal_executor_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Unmapped remote execute siblings should become ordered proxies when they only share remote-only upstream state.""" 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", ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteModel": _FakeRemoteModelNode, "RemoteSampler": _FakeRemoteSamplerNode, "LatentSource": _FakeLatentSourceNode, "LocalSink": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": False}}, {"id": 3, "properties": {"is_modal_remote": True}}, {"id": 4, "properties": {"is_modal_remote": False}}, {"id": 5, "properties": {"is_modal_remote": False}}, {"id": 6, "properties": {"is_modal_remote": True}}, {"id": 7, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteModel", "inputs": {}, "_meta": {"title": "Shared Model"}, }, "2": { "class_type": "LatentSource", "inputs": {}, "_meta": {"title": "Latent A"}, }, "3": { "class_type": "RemoteSampler", "inputs": {"model": ["1", 0], "latent": ["2", 0]}, "_meta": {"title": "Sampler A"}, }, "4": { "class_type": "LocalSink", "inputs": {"image": ["3", 0]}, "_meta": {"title": "Sink A"}, }, "5": { "class_type": "LatentSource", "inputs": {}, "_meta": {"title": "Latent B"}, }, "6": { "class_type": "RemoteSampler", "inputs": {"model": ["1", 0], "latent": ["5", 0]}, "_meta": {"title": "Sampler B"}, }, "7": { "class_type": "LocalSink", "inputs": {"image": ["6", 0]}, "_meta": {"title": "Sink B"}, }, } 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 set(rewritten_prompt) == {"2", "3", "4", "5", "6", "7"} assert summary.remote_component_ids == ["3", "6"] assert summary.component_node_ids_by_representative == { "3": ["1", "3"], "6": ["6"], } assert summary.component_dependency_ids_by_representative == { "3": [], "6": ["3"], } assert summary.component_execution_stages == [["3"], ["6"]] assert summary.rewritten_node_id_map["1"] == "3" assert summary.rewritten_node_id_map["3"] == "3" assert summary.rewritten_node_id_map["6"] == "6" first_payload = rewritten_prompt["3"]["inputs"]["original_node_data"] second_payload = rewritten_prompt["6"]["inputs"]["original_node_data"] first_execution_payload = modal_executor_module._rehydrate_proxy_payload( first_payload, unique_id="3", ) second_execution_payload = modal_executor_module._rehydrate_proxy_payload( second_payload, unique_id="6", ) assert first_payload["payload_kind"] == "subgraph" assert first_payload["component_node_ids"] == ["1", "3"] assert first_payload["boundary_inputs"] == [ { "proxy_input_name": "remote_input_0", "io_type": "LATENT", "targets": [{"node_id": "3", "input_name": "latent"}], } ] assert first_payload["boundary_outputs"][0] == { "proxy_output_name": "3_latent", "node_id": "3", "output_index": 0, "io_type": "LATENT", "is_list": False, "preview_target_node_ids": [], } assert first_payload["boundary_outputs"][1]["node_id"] == "1" assert first_payload["boundary_outputs"][1]["output_index"] == 0 assert first_payload["boundary_outputs"][1]["io_type"] == "MODEL" assert first_payload["boundary_outputs"][1]["session_output"] is True assert first_payload["execute_node_ids"] == ["3"] assert "remote_session" not in first_payload assert first_execution_payload["remote_session"]["owner_component_id"] == "1" assert second_payload["payload_kind"] == "subgraph" assert second_payload["component_node_ids"] == ["6"] assert second_payload["boundary_inputs"] == [ { "proxy_input_name": "remote_input_1", "io_type": "LATENT", "targets": [{"node_id": "6", "input_name": "latent"}], }, { "proxy_input_name": first_payload["boundary_outputs"][1]["proxy_output_name"], "io_type": "MODEL", "targets": [{"node_id": "6", "input_name": "model"}], "source_signature": api_intercept_module._boundary_source_signature( prompt, api_intercept_module.LinkedOutputRef(node_id="1", output_index=0), ), }, ] assert second_payload["boundary_outputs"] == [ { "proxy_output_name": "6_latent", "node_id": "6", "output_index": 0, "io_type": "LATENT", "is_list": False, "preview_target_node_ids": [], } ] assert second_payload["execute_node_ids"] == ["6"] assert "clear_remote_session" not in second_payload assert second_execution_payload["clear_remote_session"] is True assert ( second_execution_payload["remote_session"]["session_id"] == first_execution_payload["remote_session"]["session_id"] ) assert rewritten_prompt["6"]["inputs"][first_payload["boundary_outputs"][1]["proxy_output_name"]] == ["3", 1] assert rewritten_prompt["4"]["inputs"]["image"] == ["3", 0] assert rewritten_prompt["7"]["inputs"]["image"] == ["6", 0] def test_boundary_source_signature_changes_with_upstream_prompt_structure( api_intercept_module: Any, ) -> None: """Non-transportable boundary provenance should change when the upstream prompt changes.""" source = api_intercept_module.LinkedOutputRef(node_id="2", output_index=0) base_prompt = { "1": { "class_type": "CheckpointLoader", "inputs": {"ckpt_name": "base.safetensors"}, }, "2": { "class_type": "LoraLoader", "inputs": {"model": ["1", 0], "strength_model": 0.8}, }, } changed_prompt = { "1": { "class_type": "CheckpointLoader", "inputs": {"ckpt_name": "base.safetensors"}, }, "2": { "class_type": "LoraLoader", "inputs": {"model": ["1", 0], "strength_model": 0.5}, }, } first_signature = api_intercept_module._boundary_source_signature(base_prompt, source) second_signature = api_intercept_module._boundary_source_signature(base_prompt, source) changed_signature = api_intercept_module._boundary_source_signature(changed_prompt, source) assert first_signature == second_signature assert changed_signature != first_signature def test_extract_remote_node_ids_recurses_into_nested_subgraph_workflows( api_intercept_module: Any, settings_module: Any, ) -> None: """Modal marker extraction should find nodes nested inside saved subgraph metadata.""" 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=Path("/tmp/storage"), remote_storage_root="/storage", custom_nodes_archive_name="custom_nodes_bundle.zip", comfyui_root=None, custom_nodes_dir=Path("/tmp/custom_nodes"), ) workflow = { "nodes": [ { "id": 100, "properties": {"is_modal_remote": False}, "subgraph": { "nodes": [ {"id": 11, "properties": {"is_modal_remote": True}}, {"id": 12, "properties": {"is_modal_remote": False}}, ] }, } ] } assert api_intercept_module.extract_remote_node_ids(workflow, settings) == {"11"} assert api_intercept_module.extract_remote_node_ids( workflow, settings, prompt_node_ids={"100"}, ) == {"100"} assert api_intercept_module.extract_remote_node_ids( workflow, settings, prompt_node_ids={"100:11"}, ) == {"100:11"} def test_extract_remote_node_ids_maps_subgraph_container_to_descendant_prompt_nodes( api_intercept_module: Any, settings_module: Any, ) -> None: """A marked subgraph container should remote its expanded descendant prompt nodes.""" 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=Path("/tmp/storage"), remote_storage_root="/storage", custom_nodes_archive_name="custom_nodes_bundle.zip", comfyui_root=None, custom_nodes_dir=Path("/tmp/custom_nodes"), ) workflow = { "nodes": [ { "id": 24, "properties": {"is_modal_remote": True}, "subgraph": { "nodes": [ {"id": 23, "properties": {"is_modal_remote": False}}, {"id": 25, "properties": {"is_modal_remote": False}}, ] }, } ] } assert api_intercept_module.extract_remote_node_ids( workflow, settings, prompt_node_ids={"24:23", "24:25", "99"}, ) == {"24:23", "24:25"} def test_rewrite_rejects_non_transportable_remote_inputs( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Remote nodes should absorb a single non-transportable upstream dependency automatically.""" custom_nodes_dir = tmp_path / "custom_nodes" custom_nodes_dir.mkdir() (custom_nodes_dir / "__init__.py").write_text("NODE_CLASS_MAPPINGS = {}\n", encoding="utf-8") 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=custom_nodes_dir, ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteConsumer": _FakeRemoteSamplerNode, "ModelSource": _FakeRemoteModelNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": False}}, {"id": 2, "properties": {"is_modal_remote": True}}, ] } prompt = { "1": { "class_type": "ModelSource", "inputs": {}, "_meta": {"title": "Model Source"}, }, "2": { "class_type": "RemoteConsumer", "inputs": {"model": ["1", 0]}, "_meta": {"title": "Remote Consumer"}, }, } 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 list(rewritten_prompt) == ["1"] assert summary.remote_node_ids == ["1", "2"] assert summary.remote_component_ids == ["1"] def test_rewrite_detects_remote_marker_inside_nested_subgraph_workflow( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Prompt rewrite should honor Modal markers found inside nested subgraph metadata.""" custom_nodes_dir = tmp_path / "custom_nodes" custom_nodes_dir.mkdir() (custom_nodes_dir / "__init__.py").write_text("NODE_CLASS_MAPPINGS = {}\n", encoding="utf-8") 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=custom_nodes_dir, ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteConsumer": _FakeRemoteSamplerNode, "LocalConsumer": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ { "id": 99, "properties": {"is_modal_remote": False}, "subgraph": { "nodes": [ {"id": 1, "properties": {"is_modal_remote": False}}, {"id": 2, "properties": {"is_modal_remote": True}}, ] }, }, {"id": 3, "properties": {"is_modal_remote": False}}, ] } prompt = { "99": { "class_type": "RemoteConsumer", "inputs": {}, "_meta": {"title": "Subgraph Container"}, }, "4": { "class_type": "LocalConsumer", "inputs": {"latent": ["99", 0]}, "_meta": {"title": "Local Consumer"}, }, } 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 list(rewritten_prompt) == ["99", "4"] assert rewritten_prompt["4"]["inputs"]["latent"] == ["99", 0] assert summary.remote_node_ids == ["99"] assert summary.remote_component_ids == ["99"] def test_rewrite_detects_marked_inner_subgraph_prompt_node_ids( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """A marked nested workflow node should resolve to its composed prompt id.""" custom_nodes_dir = tmp_path / "custom_nodes" custom_nodes_dir.mkdir() (custom_nodes_dir / "__init__.py").write_text("NODE_CLASS_MAPPINGS = {}\n", encoding="utf-8") 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=custom_nodes_dir, ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteClip": _FakeRemoteClipNode, "RemoteConsumer": _FakeRemoteSamplerNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ { "id": 24, "properties": {"is_modal_remote": False}, "subgraph": { "nodes": [ {"id": 23, "properties": {"is_modal_remote": True}}, ] }, }, {"id": 30, "properties": {"is_modal_remote": True}}, ] } prompt = { "30": { "class_type": "RemoteClip", "inputs": {}, "_meta": {"title": "Remote VAE Source"}, }, "24:23": { "class_type": "RemoteConsumer", "inputs": {"clip": ["30", 0]}, "_meta": {"title": "Nested Remote Consumer"}, }, } 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 list(rewritten_prompt) == ["24:23"] assert summary.remote_node_ids == ["24:23", "30"] assert summary.remote_component_ids == ["24:23"] def test_rewrite_auto_expands_upstream_non_transportable_dependencies( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Marked remote nodes should absorb upstream non-transportable producers automatically.""" custom_nodes_dir = tmp_path / "custom_nodes" custom_nodes_dir.mkdir() (custom_nodes_dir / "__init__.py").write_text("NODE_CLASS_MAPPINGS = {}\n", encoding="utf-8") 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=custom_nodes_dir, ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "ModelSource": _FakeRemoteModelNode, "ConditioningSource": _FakeRemoteConditioningNode, "RemoteConsumer": _FakeRemoteSamplerNode, "LocalConsumer": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": False}}, {"id": 2, "properties": {"is_modal_remote": False}}, {"id": 3, "properties": {"is_modal_remote": True}}, {"id": 4, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "ModelSource", "inputs": {}, "_meta": {"title": "Model Source"}, }, "2": { "class_type": "ConditioningSource", "inputs": {}, "_meta": {"title": "Conditioning Source"}, }, "3": { "class_type": "RemoteConsumer", "inputs": { "model": ["1", 0], "conditioning": ["2", 0], }, "_meta": {"title": "Remote Consumer"}, }, "4": { "class_type": "LocalConsumer", "inputs": {"latent": ["3", 0]}, "_meta": {"title": "Local Consumer"}, }, } 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 set(rewritten_prompt) == {"1", "4"} assert summary.remote_node_ids == ["1", "2", "3"] assert summary.remote_component_ids == ["1"] assert summary.component_node_ids_by_representative == {"1": ["1", "2", "3"]} assert summary.rewritten_node_id_map == {"1": "1", "2": "1", "3": "1"} payload = rewritten_prompt["1"]["inputs"]["original_node_data"] assert payload["boundary_inputs"] == [] assert payload["execute_node_ids"] == ["3"] assert rewritten_prompt["4"]["inputs"]["latent"] == ["1", 0] def test_analyze_remote_node_selection_returns_nodes_to_mark_and_reasons( api_intercept_module: Any, settings_module: Any, ) -> None: """Dry-run analysis should surface the clicked node plus required upstream nodes.""" 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=Path("/tmp/storage"), remote_storage_root="/storage", custom_nodes_archive_name="custom_nodes_bundle.zip", comfyui_root=None, custom_nodes_dir=Path("/tmp/custom_nodes"), ) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "ModelSource": _FakeRemoteModelNode, "ConditioningSource": _FakeRemoteConditioningNode, "RemoteConsumer": _FakeRemoteSamplerNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": False}}, {"id": 2, "properties": {"is_modal_remote": False}}, {"id": 3, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": {"class_type": "ModelSource", "inputs": {}, "_meta": {"title": "Model"}}, "2": { "class_type": "ConditioningSource", "inputs": {}, "_meta": {"title": "Conditioning"}, }, "3": { "class_type": "RemoteConsumer", "inputs": {"model": ["1", 0], "conditioning": ["2", 0]}, "_meta": {"title": "Remote Consumer"}, }, } analysis = api_intercept_module.analyze_remote_node_selection( prompt=prompt, workflow=workflow, seed_workflow_node_paths=["3"], settings=settings, nodes_module=fake_nodes_module, ) assert analysis.requested_node_ids == ["3"] assert analysis.requested_workflow_node_paths == ["3"] assert analysis.current_remote_node_ids == [] assert analysis.current_remote_workflow_node_paths == [] assert analysis.resolved_remote_node_ids == ["1", "2", "3"] assert analysis.resolved_workflow_node_paths == ["1", "2", "3"] assert analysis.added_node_ids == ["1", "2", "3"] assert analysis.added_workflow_node_paths == ["1", "2", "3"] assert [(reason.node_id, reason.required_by_node_id) for reason in analysis.reasons] == [ ("1", "3"), ("2", "3"), ] def test_analyze_remote_node_selection_prefers_nested_workflow_paths( api_intercept_module: Any, settings_module: Any, ) -> None: """Nested prompt ids should map back to the specific inner workflow node path.""" 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=Path("/tmp/storage"), remote_storage_root="/storage", custom_nodes_archive_name="custom_nodes_bundle.zip", comfyui_root=None, custom_nodes_dir=Path("/tmp/custom_nodes"), ) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "ModelSource": _FakeRemoteModelNode, "RemoteConsumer": _FakeRemoteSamplerNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ { "id": 24, "properties": {"is_modal_remote": False}, "subgraph": { "nodes": [ {"id": 23, "properties": {"is_modal_remote": True}}, ] }, }, {"id": 30, "properties": {"is_modal_remote": False}}, ] } prompt = { "30": {"class_type": "ModelSource", "inputs": {}, "_meta": {"title": "Model"}}, "24:23": { "class_type": "RemoteConsumer", "inputs": {"model": ["30", 0]}, "_meta": {"title": "Nested Consumer"}, }, } analysis = api_intercept_module.analyze_remote_node_selection( prompt=prompt, workflow=workflow, seed_workflow_node_paths=["24:23"], settings=settings, nodes_module=fake_nodes_module, ) assert analysis.requested_node_ids == ["24:23"] assert analysis.current_remote_node_ids == ["24:23"] assert analysis.current_remote_workflow_node_paths == ["24:23"] assert analysis.resolved_remote_node_ids == ["24:23", "30"] assert analysis.resolved_workflow_node_paths == ["24:23", "30"] assert analysis.added_node_ids == ["30"] assert analysis.added_workflow_node_paths == ["30"] assert [(reason.node_id, reason.required_by_node_id) for reason in analysis.reasons] == [ ("30", "24:23"), ] def test_rewrite_rejects_non_transportable_remote_outputs( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Remote component boundaries should reject non-transportable local downstream edges.""" custom_nodes_dir = tmp_path / "custom_nodes" custom_nodes_dir.mkdir() (custom_nodes_dir / "__init__.py").write_text("NODE_CLASS_MAPPINGS = {}\n", encoding="utf-8") 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=custom_nodes_dir, ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteClip": _FakeRemoteClipNode, "LocalConsumer": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 1, "properties": {"is_modal_remote": True}}, {"id": 2, "properties": {"is_modal_remote": False}}, ] } prompt = { "1": { "class_type": "RemoteClip", "inputs": {}, "_meta": {"title": "Remote Clip"}, }, "2": { "class_type": "LocalConsumer", "inputs": {"clip": ["1", 0]}, "_meta": {"title": "Local Consumer"}, }, } try: api_intercept_module.rewrite_prompt_for_modal( prompt=prompt, workflow=workflow, sync_engine=sync_engine, settings=settings, nodes_module=fake_nodes_module, ) except api_intercept_module.ModalPromptValidationError as exc: message = str(exc) else: raise AssertionError("Expected ModalPromptValidationError to be raised.") assert "exports node 1 (RemoteClip) output index 0 of type 'CLIP'" in message assert "cannot cross the current local/remote boundary" in message def test_extract_remote_node_ids_prefers_nested_prompt_id_over_colliding_root_id( api_intercept_module: Any, settings_module: Any, ) -> None: """Nested Modal markers should resolve to their composed prompt ids when root ids collide.""" 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=Path("/tmp/storage"), remote_storage_root="/storage", custom_nodes_archive_name="custom_nodes_bundle.zip", comfyui_root=None, custom_nodes_dir=Path("/tmp/custom_nodes"), ) workflow = { "nodes": [ {"id": 27, "properties": {"is_modal_remote": False}}, { "id": 195, "properties": {"is_modal_remote": False}, "subgraph": { "nodes": [ {"id": 27, "properties": {"is_modal_remote": True}}, ] }, }, ] } assert api_intercept_module.extract_remote_node_ids( workflow, settings, prompt_node_ids={"27", "195:27", "222", "223"}, ) == {"195:27"} def test_rewrite_keeps_nested_remote_nodes_remote_when_root_ids_collide( api_intercept_module: Any, settings_module: Any, sync_engine_module: Any, tmp_path: Path, ) -> None: """Nested remote markers should survive prompt-id collisions with root workflow nodes.""" custom_nodes_dir = tmp_path / "custom_nodes" custom_nodes_dir.mkdir() (custom_nodes_dir / "__init__.py").write_text("NODE_CLASS_MAPPINGS = {}\n", encoding="utf-8") 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=custom_nodes_dir, ) sync_engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings) fake_nodes_module = type( "FakeNodesModule", (), { "NODE_CLASS_MAPPINGS": { "RemoteClip": _FakeRemoteClipNode, "RemoteConsumer": _FakeRemoteSamplerNode, "LocalConsumer": _FakeLocalSinkNode, }, "NODE_DISPLAY_NAME_MAPPINGS": {}, }, )() workflow = { "nodes": [ {"id": 27, "properties": {"is_modal_remote": False}}, {"id": 222, "properties": {"is_modal_remote": True}}, { "id": 195, "properties": {"is_modal_remote": False}, "subgraph": { "nodes": [ {"id": 27, "properties": {"is_modal_remote": True}}, ] }, }, {"id": 223, "properties": {"is_modal_remote": False}}, ] } prompt = { "27": { "class_type": "LocalConsumer", "inputs": {}, "_meta": {"title": "Root Local Consumer"}, }, "222": { "class_type": "RemoteClip", "inputs": {}, "_meta": {"title": "Remote Clip Source"}, }, "195:27": { "class_type": "RemoteConsumer", "inputs": {"clip": ["222", 0]}, "_meta": {"title": "Nested Remote Consumer"}, }, "223": { "class_type": "LocalConsumer", "inputs": {"latent": ["195:27", 0]}, "_meta": {"title": "Local Sink"}, }, } 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 set(rewritten_prompt) == {"27", "195:27", "223"} assert summary.remote_node_ids == ["195:27", "222"] assert summary.remote_component_ids == ["195:27"] def test_emit_modal_status_targets_prompt_client( api_intercept_module: Any, ) -> None: """Modal status events should preserve prompt and component metadata for the UI.""" api_intercept_module._MODAL_UI_EVENTS_BY_CLIENT.clear() class FakePromptServer: """Capture websocket events emitted by the queue route.""" def __init__(self) -> None: """Initialize the event sink.""" self.messages: list[tuple[str, dict[str, Any], str | None]] = [] def send_sync(self, event: str, data: dict[str, Any], sid: str | None) -> None: """Record an emitted websocket message.""" self.messages.append((event, data, sid)) prompt_server = FakePromptServer() api_intercept_module._emit_modal_status( prompt_server=prompt_server, phase="executing", client_id="client-1", prompt_id="prompt-1", node_ids=["4", "5"], component_node_ids_by_representative={"4": ["4", "5"]}, active_node_id="5", active_node_class_type="KSampler", active_node_role="sampling", ) assert prompt_server.messages == [ ( "modal_status", { "phase": "executing", "prompt_id": "prompt-1", "node_ids": ["4", "5"], "active_node_id": "5", "active_node_class_type": "KSampler", "active_node_role": "sampling", "components": [ { "representative_node_id": "4", "node_ids": ["4", "5"], } ], }, "client-1", ) ] replay_events = api_intercept_module.modal_ui_events_for_client("client-1") assert replay_events == [ { "event": "modal_status", "payload": { "phase": "executing", "prompt_id": "prompt-1", "node_ids": ["4", "5"], "active_node_id": "5", "active_node_class_type": "KSampler", "active_node_role": "sampling", "components": [ { "representative_node_id": "4", "node_ids": ["4", "5"], } ], }, "updated_at": replay_events[0]["updated_at"], } ] def test_modal_ui_event_replay_is_client_scoped(api_intercept_module: Any) -> None: """Refocus replay should only return events for the requesting ComfyUI client.""" api_intercept_module._MODAL_UI_EVENTS_BY_CLIENT.clear() api_intercept_module.record_modal_ui_event( "modal_progress", {"prompt_id": "prompt-1", "node_id": "4", "value": 2.0, "max": 10.0}, "client-1", ) api_intercept_module.record_modal_ui_event( "modal_status", {"prompt_id": "prompt-2", "phase": "executing", "node_ids": ["9"]}, "client-2", ) replay_events = api_intercept_module.modal_ui_events_for_client("client-1") assert len(replay_events) == 1 assert replay_events[0]["event"] == "modal_progress" assert replay_events[0]["payload"] == { "prompt_id": "prompt-1", "node_id": "4", "value": 2.0, "max": 10.0, } assert api_intercept_module.modal_ui_events_for_client(None) == [] def test_progress_state_route_is_queue_route_sibling(api_intercept_module: Any) -> None: """The frontend should have a stable sibling route for Modal UI replay.""" assert api_intercept_module._progress_state_route_path("/modal/queue_prompt") == ( "/modal/progress_state" ) assert api_intercept_module._progress_state_route_path("/custom/modal") == ( "/custom/modal/progress_state" ) def test_modal_interrupt_queue_bridge_exposes_active_remote_prompts( api_intercept_module: Any, remote_modal_app_module: Any, ) -> None: """Targeted ComfyUI interrupts should see prompts currently blocked on Modal work.""" class FakePromptQueue: """Minimal ComfyUI prompt queue with no native running prompts.""" def get_current_queue(self) -> tuple[list[Any], list[Any]]: """Return an empty running queue and one pending item.""" return [], ["queued"] prompt_queue = FakePromptQueue() prompt_server = SimpleNamespace(prompt_queue=prompt_queue) cancellation_event = remote_modal_app_module.threading.Event() api_intercept_module._install_modal_interrupt_queue_bridge(prompt_server) with remote_modal_app_module._registered_active_remote_invocation( {"prompt_id": "prompt-1", "component_id": "component-1"}, cancellation_event, None, ): running, queued = prompt_queue.get_current_queue() assert queued == ["queued"] assert [item[1] for item in running] == ["prompt-1"]