Files
ttulttul-ComfyUI-Modal/tests/test_settings.py
T

602 lines
22 KiB
Python

"""Tests for Modal-Sync settings discovery."""
from __future__ import annotations
from dataclasses import replace
import re
from pathlib import Path
from typing import Any
def test_settings_discovers_comfyui_root_from_custom_nodes_install_path(
settings_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""The extension should infer the ComfyUI root from its install location."""
comfyui_root = tmp_path / "ComfyUI"
custom_node_repo = comfyui_root / "custom_nodes" / "ComfyUI-Modal"
custom_node_repo.mkdir(parents=True)
(comfyui_root / "main.py").write_text("print('main')\n", encoding="utf-8")
(comfyui_root / "nodes.py").write_text("NODE_CLASS_MAPPINGS = {}\n", encoding="utf-8")
monkeypatch.setattr(settings_module, "__file__", str(custom_node_repo / "settings.py"))
monkeypatch.delenv("COMFYUI_ROOT", raising=False)
monkeypatch.delenv("COMFY_MODAL_COMFYUI_ROOT", raising=False)
resolved = settings_module._discover_comfyui_root(custom_node_repo)
assert resolved == comfyui_root.resolve()
def test_settings_prefers_modal_specific_comfyui_root_env(
settings_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""The Modal-specific ComfyUI root env var should override path inference."""
env_root = tmp_path / "alt-comfyui"
env_root.mkdir()
monkeypatch.setenv("COMFY_MODAL_COMFYUI_ROOT", str(env_root))
monkeypatch.delenv("COMFYUI_ROOT", raising=False)
resolved = settings_module._discover_comfyui_root(tmp_path / "repo")
assert resolved == env_root.resolve()
def test_settings_discovers_latest_comfyui_standalone_checkout(
settings_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""Standalone development should recognize the locally used checkout name."""
comfyui_root = tmp_path / "git" / "Latest_ComfyUI"
comfyui_root.mkdir(parents=True)
(comfyui_root / "main.py").write_text("print('main')\n", encoding="utf-8")
(comfyui_root / "nodes.py").write_text(
"NODE_CLASS_MAPPINGS = {}\n",
encoding="utf-8",
)
monkeypatch.delenv("COMFYUI_ROOT", raising=False)
monkeypatch.delenv("COMFY_MODAL_COMFYUI_ROOT", raising=False)
monkeypatch.setattr(settings_module.Path, "home", lambda: tmp_path)
resolved = settings_module._discover_comfyui_root(tmp_path / "standalone")
assert resolved == comfyui_root.resolve()
def test_settings_reads_modal_gpu_override(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The Modal GPU type should be configurable via environment variable."""
monkeypatch.setenv("COMFY_MODAL_GPU", "L40S")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.modal_gpu == "L40S"
def test_settings_defaults_modal_gpu_to_rtx_pro_6000(
settings_module: Any,
) -> None:
"""Backend settings should use the same RTX default as the workflow UI."""
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings_module.DEFAULT_MODAL_GPU == "RTX-PRO-6000"
assert settings.modal_gpu == "RTX-PRO-6000"
def test_settings_reads_modal_secret_name_override(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The named Modal secret collection should be configurable at local startup."""
monkeypatch.setenv("COMFY_MODAL_SECRET_NAME", " workflow-credentials ")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.modal_secret_name == "workflow-credentials"
def test_settings_defaults_modal_secret_name_to_comfy(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Remote workers should use the conventional comfy secret collection by default."""
monkeypatch.delenv("COMFY_MODAL_SECRET_NAME", raising=False)
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings_module.DEFAULT_MODAL_SECRET_NAME == "comfy"
assert settings.modal_secret_name == "comfy"
def test_modal_gpu_from_workflow_prefers_saved_selection(settings_module: Any) -> None:
"""A saved workflow GPU should override the process-level fallback."""
workflow = {"extra": {"comfy_modal": {"gpu": "B300"}}}
selected_gpu = settings_module.modal_gpu_from_workflow(workflow, "L40S")
assert selected_gpu == "B300"
def test_modal_gpu_from_workflow_uses_fallback_when_unsaved(settings_module: Any) -> None:
"""Older workflows without Modal metadata should retain the configured fallback."""
assert settings_module.modal_gpu_from_workflow({"nodes": []}, "L40S") == "L40S"
def test_modal_gpu_from_workflow_rejects_unknown_selection(settings_module: Any) -> None:
"""Queue handling should reject edited workflow GPU values outside Modal's supported list."""
workflow = {"extra": {"comfy_modal": {"gpu": "V100"}}}
try:
settings_module.modal_gpu_from_workflow(workflow, "A100")
except ValueError as exc:
assert "V100" in str(exc)
else:
raise AssertionError("Expected an unsupported workflow GPU to be rejected.")
def test_settings_for_modal_gpu_preserves_other_runtime_settings(settings_module: Any) -> None:
"""A workflow GPU override should change only the deploy-time GPU target."""
base_settings = settings_module.get_settings()
overridden_settings = settings_module.settings_for_modal_gpu(base_settings, "h200")
assert overridden_settings.modal_gpu == "H200"
assert overridden_settings.app_name == base_settings.app_name
assert overridden_settings.volume_name == base_settings.volume_name
def test_modal_deployment_app_name_isolates_gpu_targets_from_legacy_a100_app(
settings_module: Any,
) -> None:
"""The RTX default should not probe the legacy A100 app retained at the base name."""
base_settings = settings_module.get_settings()
rtx_settings = settings_module.settings_for_modal_gpu(base_settings, "RTX-PRO-6000")
a100_settings = settings_module.settings_for_modal_gpu(base_settings, "A100")
b300_settings = settings_module.settings_for_modal_gpu(base_settings, "B300")
h100_settings = settings_module.settings_for_modal_gpu(base_settings, "H100")
priority_h100_settings = settings_module.settings_for_modal_gpu(base_settings, "H100!")
assert settings_module.modal_deployment_app_name(a100_settings) == base_settings.app_name
assert settings_module.modal_deployment_app_name(rtx_settings) == (
f"{base_settings.app_name}-gpu-rtx-pro-6000"
)
assert settings_module.modal_deployment_app_name(b300_settings) == (
f"{base_settings.app_name}-gpu-b300"
)
assert settings_module.modal_deployment_app_name(h100_settings).endswith("-gpu-h100")
assert settings_module.modal_deployment_app_name(priority_h100_settings).endswith(
"-gpu-h100-priority"
)
def test_modal_deployment_app_name_stays_within_modal_object_name_limit(
settings_module: Any,
) -> None:
"""GPU-specific app identities should remain valid for a 64-character base app name."""
base_settings = settings_module.get_settings()
long_name_settings = replace(
base_settings,
app_name="a" * 64,
modal_gpu="RTX-PRO-6000",
)
deployment_app_name = settings_module.modal_deployment_app_name(long_name_settings)
assert len(deployment_app_name) <= 64
assert deployment_app_name.endswith("-gpu-rtx-pro-6000")
assert re.fullmatch(r"[a-zA-Z0-9-_.]+", deployment_app_name)
def test_settings_generates_stable_per_comfyui_app_name(
settings_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""Default remote app names should be stable and namespaced by a persisted 64-bit identity."""
identity_path = tmp_path / "instance-id"
monkeypatch.delenv("COMFY_MODAL_APP_NAME", raising=False)
monkeypatch.setenv("COMFY_MODAL_INSTANCE_ID_PATH", str(identity_path))
settings_module.get_settings.cache_clear()
try:
first_settings = settings_module.get_settings()
settings_module.get_settings.cache_clear()
second_settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert first_settings.app_name == second_settings.app_name
assert re.fullmatch(r"comfy-modal-sync-[A-Za-z0-9_-]{11}", first_settings.app_name)
assert len(bytes.fromhex(identity_path.read_text(encoding="ascii").strip())) == 8
assert first_settings.interrupt_dict_name == f"{first_settings.app_name}-interrupts"
assert first_settings.invocation_dict_name == f"{first_settings.app_name}-invocations"
def test_settings_app_name_override_bypasses_instance_identity(
settings_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""An explicit app name should remain authoritative and require no identity file."""
identity_path = tmp_path / "instance-id"
monkeypatch.setenv("COMFY_MODAL_APP_NAME", "shared-explicit-app")
monkeypatch.setenv("COMFY_MODAL_INSTANCE_ID_PATH", str(identity_path))
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.app_name == "shared-explicit-app"
assert not identity_path.exists()
def test_settings_cache_tracks_execution_mode_env_changes(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Changing Modal execution mode should not leave callers with stale cached settings."""
settings_module.get_settings.cache_clear()
try:
monkeypatch.setenv("COMFY_MODAL_EXECUTION_MODE", "local")
local_settings = settings_module.get_settings()
monkeypatch.setenv("COMFY_MODAL_EXECUTION_MODE", "remote")
remote_settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert local_settings.execution_mode == "local"
assert local_settings.sync_custom_nodes is False
assert remote_settings.execution_mode == "remote"
assert remote_settings.sync_custom_nodes is True
def test_settings_enable_gpu_memory_snapshot_defaults_true(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""GPU memory snapshots should be enabled by default."""
monkeypatch.delenv("COMFY_MODAL_ENABLE_GPU_MEMORY_SNAPSHOT", raising=False)
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.enable_gpu_memory_snapshot is True
def test_settings_reads_modal_container_scaling_overrides(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Modal autoscaling knobs should be configurable via environment variables."""
monkeypatch.setenv("COMFY_MODAL_MIN_CONTAINERS", "1")
monkeypatch.setenv("COMFY_MODAL_MAX_CONTAINERS", "6")
monkeypatch.setenv("COMFY_MODAL_BUFFER_CONTAINERS", "2")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.min_containers == 1
assert settings.max_containers == 6
assert settings.buffer_containers == 2
def test_settings_reads_remote_invocation_runtime_controls(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Remote call concurrency and Modal deadlines should be configurable."""
monkeypatch.setenv("COMFY_MODAL_MAX_INFLIGHT_CALLS", "7")
monkeypatch.setenv("COMFY_MODAL_EXECUTION_TIMEOUT_SECONDS", "1800")
monkeypatch.setenv("COMFY_MODAL_STARTUP_TIMEOUT_SECONDS", "600")
monkeypatch.setenv("COMFY_MODAL_STREAM_EVENT_QUEUE_MAXSIZE", "32")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.max_inflight_calls == 7
assert settings.execution_timeout_seconds == 1800
assert settings.startup_timeout_seconds == 600
assert settings.stream_event_queue_maxsize == 32
def test_settings_reads_resident_llm_memory_controls(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Resident model count and ComfyUI VRAM reserve should be configurable."""
monkeypatch.setenv("COMFY_MODAL_LLM_MAX_RESIDENT_MODELS", "3")
monkeypatch.setenv("COMFY_MODAL_LLM_RESERVE_FREE_GB", "48.5")
monkeypatch.setenv("COMFY_MODAL_LLM_VLLM_EXECUTION_MODE", "throughput")
monkeypatch.setenv("COMFY_MODAL_LLM_MEMORY_RECOVERY_TIMEOUT_SECONDS", "7.5")
monkeypatch.setenv(
"COMFY_MODAL_LLM_COMPILE_CACHE_VOLUME_NAME",
"benchmark-vllm-cache",
)
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.llm_max_resident_models == 3
assert settings.llm_reserve_free_vram_gb == 48.5
assert settings.llm_vllm_execution_mode == "throughput"
assert settings.llm_memory_recovery_timeout_seconds == 7.5
assert settings.llm_compile_cache_volume_name == "benchmark-vllm-cache"
def test_settings_defaults_vllm_execution_mode_to_auto(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Unconfigured deployments should promote only after repeat container use."""
monkeypatch.delenv("COMFY_MODAL_LLM_VLLM_EXECUTION_MODE", raising=False)
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.llm_vllm_execution_mode == "auto"
def test_settings_reads_proactive_warmup_override(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Proactive remote warmup should be configurable via environment variable."""
monkeypatch.setenv("COMFY_MODAL_ENABLE_PROACTIVE_WARMUP", "false")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.enable_proactive_warmup is False
def test_settings_reads_loader_prewarm_worker_limit(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Heavy loader overlap should have a bounded environment-configured limit."""
monkeypatch.setenv("COMFY_MODAL_LOADER_PREWARM_WORKERS", "3")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.loader_prewarm_workers == 3
def test_settings_reads_local_gap_keepalive_overrides(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Remote retention across local workflow gaps should be configurable."""
monkeypatch.setenv("COMFY_MODAL_LOCAL_GAP_KEEPALIVE_SECONDS", "1200")
monkeypatch.setenv("COMFY_MODAL_LOCAL_GAP_KEEPALIVE_INTERVAL_SECONDS", "12.5")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.local_gap_keepalive_seconds == 1200.0
assert settings.local_gap_keepalive_interval_seconds == 12.5
def test_settings_reads_remote_cancel_grace_override(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The post-interrupt remote wait should be configurable."""
monkeypatch.setenv("COMFY_MODAL_REMOTE_CANCEL_GRACE_SECONDS", "0.25")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.remote_cancel_grace_seconds == 0.25
def test_settings_reads_remote_cancel_restart_override(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The remote post-cancel restart timeout should be configurable."""
monkeypatch.setenv("COMFY_MODAL_REMOTE_CANCEL_RESTART_SECONDS", "0.75")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.remote_cancel_restart_seconds == 0.75
def test_settings_defaults_interrupt_dict_name_from_app_name(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The shared interrupt dict name should default to one derived from the app name."""
monkeypatch.setenv("COMFY_MODAL_APP_NAME", "my-modal-app")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.interrupt_dict_name == "my-modal-app-interrupts"
def test_settings_reads_interrupt_dict_name_override(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The shared interrupt dict name should be overridable explicitly."""
monkeypatch.setenv("COMFY_MODAL_INTERRUPT_DICT_NAME", "custom-interrupt-store")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.interrupt_dict_name == "custom-interrupt-store"
def test_settings_defaults_node_cache_dict_name_from_app_name(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The shared node-cache dict should default to one derived from the app name."""
monkeypatch.setenv("COMFY_MODAL_APP_NAME", "my-modal-app")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.node_output_cache_dict_name == "my-modal-app-node-cache"
def test_settings_defaults_session_bridge_dict_name_from_app_name(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The durable session-bridge dict should default to one derived from the app name."""
monkeypatch.setenv("COMFY_MODAL_APP_NAME", "my-modal-app")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.session_bridge_dict_name == "my-modal-app-session-bridges"
def test_settings_defaults_invocation_dict_name_from_app_name(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The durable invocation dict should default to one derived from the app name."""
monkeypatch.setenv("COMFY_MODAL_APP_NAME", "my-modal-app")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.invocation_dict_name == "my-modal-app-invocations"
def test_settings_defaults_node_cache_max_bytes_to_five_mib(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The distributed node-cache size cap should default to 5 MiB."""
monkeypatch.delenv("COMFY_MODAL_NODE_CACHE_MAX_BYTES", raising=False)
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.node_output_cache_max_bytes == 5 * 1024 * 1024
def test_settings_reads_node_cache_overrides(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The shared node-cache dict name and size limit should be configurable."""
monkeypatch.setenv("COMFY_MODAL_NODE_CACHE_DICT_NAME", "custom-node-cache")
monkeypatch.setenv("COMFY_MODAL_NODE_CACHE_MAX_BYTES", "123456")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.node_output_cache_dict_name == "custom-node-cache"
assert settings.node_output_cache_max_bytes == 123456
def test_settings_reads_session_bridge_dict_name_override(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""The durable session-bridge dict name should be configurable."""
monkeypatch.setenv("COMFY_MODAL_SESSION_BRIDGE_DICT_NAME", "custom-session-bridges")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.session_bridge_dict_name == "custom-session-bridges"
def test_settings_reads_durable_invocation_and_bridge_limits(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Durable state metadata stores and inline byte limits should be configurable."""
monkeypatch.setenv("COMFY_MODAL_INVOCATION_DICT_NAME", "custom-invocations")
monkeypatch.setenv("COMFY_MODAL_BRIDGE_INLINE_MAX_BYTES", "1234")
monkeypatch.setenv("COMFY_MODAL_INVOCATION_RESULT_INLINE_MAX_BYTES", "5678")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.invocation_dict_name == "custom-invocations"
assert settings.bridge_inline_max_bytes == 1234
assert settings.invocation_result_inline_max_bytes == 5678
def test_settings_reads_terminate_container_on_error_override(
settings_module: Any,
monkeypatch: Any,
) -> None:
"""Remote crash teardown should be configurable via environment variable."""
monkeypatch.setenv("COMFY_MODAL_TERMINATE_CONTAINER_ON_ERROR", "false")
settings_module.get_settings.cache_clear()
try:
settings = settings_module.get_settings()
finally:
settings_module.get_settings.cache_clear()
assert settings.terminate_container_on_error is False