From c6ef1c66827914bcd4ca79e43bfd6affc8763989 Mon Sep 17 00:00:00 2001 From: Ken Simpson Date: Tue, 4 Aug 2026 14:08:34 -0700 Subject: [PATCH] feat: namespace Modal apps per ComfyUI instance --- .gitignore | 1 + README.md | 5 +- comfyui_modal_sync_cloud.py | 36 ++++----- docs/LEARNINGS.md | 1 + instance_identity.py | 123 ++++++++++++++++++++++++++++++ settings.py | 63 ++++++++++++++- tests/conftest.py | 9 +++ tests/test_instance_identity.py | 68 +++++++++++++++++ tests/test_modal_executor_node.py | 19 +++++ tests/test_settings.py | 44 +++++++++++ 10 files changed, 347 insertions(+), 22 deletions(-) create mode 100644 instance_identity.py create mode 100644 tests/test_instance_identity.py diff --git a/.gitignore b/.gitignore index 869cdba..0346d6e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ !/api_intercept.py !/comfyui_modal_sync_cloud.py !/durable_state.py +!/instance_identity.py !/modal_executor_node.py !/modal_sdk.py !/runtime_environment.py diff --git a/README.md b/README.md index dac0167..25e474f 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ When ComfyUI starts in remote mode, Modal-Sync checks whether the supported Moda Modal authentication remains user-managed. Run `/bin/python -m modal setup` when the current user does not already have working credentials. To install the SDK manually before startup, run `uv pip install --python /bin/python "modal==1.4.2"`. +Each ComfyUI environment receives its own Modal app name on first startup. Modal-Sync generates 64 random bits, persists them as lowercase hexadecimal in `/.comfy-modal-sync-instance-id`, and uses an unpadded URL-safe Base64 encoding in names such as `comfy-modal-sync-R7uqpQZ6S1A`. The identity file is published atomically, so concurrent first startups converge on one name. It lives in ComfyUI's effective user directory and therefore follows `--user-directory`; keep the file when upgrading if the deployment and its state should remain associated with the same ComfyUI environment. `COMFY_MODAL_APP_NAME` remains an authoritative override for deliberately shared or externally managed deployments. + For repository development, `uv sync --extra remote --group test` installs the same pinned SDK. Remote mode uses the stable cloud entrypoint in [`comfyui_modal_sync_cloud.py`](comfyui_modal_sync_cloud.py). On first use, Modal-Sync can auto-deploy the configured Modal app if it does not exist. The deployed image uses Python 3.11 plus an exact ComfyUI support and CUDA package set. Its local build context is limited to the ComfyUI source packages, top-level Python modules, and runtime configuration needed by the headless worker; model directories, custom nodes, caches, tests, virtual environments, user data, and unknown top-level directories stay out of the image snapshot. Before every process's first remote invocation, Modal-Sync compares the deployed worker's runtime fingerprint with the local source, ComfyUI source, custom-node requirements, and runtime-shaping settings. A missing or mismatched fingerprint is treated as stale and replaced automatically when `COMFY_MODAL_AUTO_DEPLOY=true`. @@ -218,7 +220,8 @@ Boolean values accept `1`, `true`, `yes`, `on`, `0`, `false`, `no`, and `off`. | Variable | Default | Purpose | | --- | --- | --- | | `COMFY_MODAL_EXECUTION_MODE` | `local` | Set to `remote` for Modal-backed execution. | -| `COMFY_MODAL_APP_NAME` | `comfy-modal-sync` | Modal app name. | +| `COMFY_MODAL_APP_NAME` | `comfy-modal-sync-` | Explicit Modal app name override; otherwise derived from the persistent per-ComfyUI identity. | +| `COMFY_MODAL_INSTANCE_ID_PATH` | `/.comfy-modal-sync-instance-id` | Override the persistent 64-bit identity file location. | | `COMFY_MODAL_VOLUME_NAME` | `comfy-universal-storage` | Modal volume name for synced assets and bundles. | | `COMFY_MODAL_AUTO_DEPLOY` | `true` | Deploy or replace the configured app when lookup fails or its runtime fingerprint is stale. | | `COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK` | `false` | Allow the older temporary `app.run()` fallback when deployed lookup fails. | diff --git a/comfyui_modal_sync_cloud.py b/comfyui_modal_sync_cloud.py index 1d47f80..0163fbc 100644 --- a/comfyui_modal_sync_cloud.py +++ b/comfyui_modal_sync_cloud.py @@ -6104,6 +6104,21 @@ def _execute_loader_prewarm_plans( ) +def _modal_image_environment(settings: Any, runtime_fingerprint: str) -> dict[str, str]: + """Return environment values that keep the worker aligned with local settings.""" + return { + "COMFY_MODAL_APP_NAME": settings.app_name, + "COMFY_MODAL_RUNTIME_FINGERPRINT": runtime_fingerprint, + "COMFY_MODAL_STREAM_EVENT_QUEUE_MAXSIZE": str(settings.stream_event_queue_maxsize), + "COMFY_MODAL_BRIDGE_INLINE_MAX_BYTES": str(settings.bridge_inline_max_bytes), + "COMFY_MODAL_INVOCATION_RESULT_INLINE_MAX_BYTES": str( + settings.invocation_result_inline_max_bytes + ), + "COMFY_MODAL_EXECUTION_TIMEOUT_SECONDS": str(settings.execution_timeout_seconds), + "COMFY_MODAL_STARTUP_TIMEOUT_SECONDS": str(settings.startup_timeout_seconds), + } + + if modal is not None: # pragma: no branch - remote entrypoint configuration. settings = get_settings() _guard_against_existing_modal_app(settings, modal) @@ -6145,26 +6160,7 @@ if modal is not None: # pragma: no branch - remote entrypoint configuration. image = ( modal.Image.debian_slim(python_version=REMOTE_PYTHON_VERSION) .apt_install(*_comfyui_apt_packages()) - .env( - { - "COMFY_MODAL_RUNTIME_FINGERPRINT": runtime_identity.fingerprint, - "COMFY_MODAL_STREAM_EVENT_QUEUE_MAXSIZE": str( - settings.stream_event_queue_maxsize - ), - "COMFY_MODAL_BRIDGE_INLINE_MAX_BYTES": str( - settings.bridge_inline_max_bytes - ), - "COMFY_MODAL_INVOCATION_RESULT_INLINE_MAX_BYTES": str( - settings.invocation_result_inline_max_bytes - ), - "COMFY_MODAL_EXECUTION_TIMEOUT_SECONDS": str( - settings.execution_timeout_seconds - ), - "COMFY_MODAL_STARTUP_TIMEOUT_SECONDS": str( - settings.startup_timeout_seconds - ), - } - ) + .env(_modal_image_environment(settings, runtime_identity.fingerprint)) .pip_install(*_comfyui_runtime_packages()) ) if custom_node_packages: diff --git a/docs/LEARNINGS.md b/docs/LEARNINGS.md index 4f56b83..5d3da34 100644 --- a/docs/LEARNINGS.md +++ b/docs/LEARNINGS.md @@ -305,3 +305,4 @@ - Pinning `opencv-python-headless` in the core Modal runtime does not guarantee that `cv2` stays headless. Custom-node requirements can install `opencv-python` directly, and dependencies such as RMBG's `transparent-background` can pull it transitively. The Modal image therefore needs `libgl1` and `libglib2.0-0` so GUI-enabled OpenCV remains importable for custom nodes such as RMBG and Inspire Pack. - Modal can retry a generator input after an internal delivery failure even while the original container's daemon compute thread is still running. A durable invocation guard should prevent duplicate GPU work, but a raw `already running` error turns that protection into a prompt failure. On stream abandonment, cancel the original compute and mark its record failed without committing pending Volume objects; overlapping delivery should briefly wait for the record to become completed or retryable before deciding whether to replay or start the next attempt. - A fresh uv-created ComfyUI virtual environment may intentionally have no `pip` module, so startup dependency repair cannot assume `sys.executable -m pip` works. For remote mode, detect the missing Modal SDK before importing the rest of the extension, prefer `uv pip install --python sys.executable` with the exact supported pin, fall back to the interpreter's pip module when available, and retry the import in the same process after invalidating import caches. +- A fixed default Modal app name makes independent ComfyUI installations collide inside one Modal workspace and also shares every app-derived Dict namespace. Persist 64 random bits in the effective ComfyUI user directory, publish the fully written identity atomically so concurrent first starts agree, encode it with unpadded URL-safe Base64 for the app suffix, preserve explicit name overrides, and inject the resolved app name into the remote image so workers never generate a second identity. diff --git a/instance_identity.py b/instance_identity.py new file mode 100644 index 0000000..66fb565 --- /dev/null +++ b/instance_identity.py @@ -0,0 +1,123 @@ +"""Persistent per-ComfyUI identity for Modal resource namespacing.""" + +from __future__ import annotations + +import base64 +import logging +import os +import secrets +import tempfile +from pathlib import Path + +logger = logging.getLogger(__name__) + +INSTANCE_ID_BYTES = 8 +INSTANCE_ID_ENTROPY_BITS = INSTANCE_ID_BYTES * 8 +INSTANCE_ID_FILENAME = ".comfy-modal-sync-instance-id" +MODAL_APP_PREFIX = "comfy-modal-sync" + + +class ComfyInstanceIdentityError(RuntimeError): + """Raised when a persistent ComfyUI instance identity cannot be used safely.""" + + +def _read_instance_id(identity_path: Path) -> bytes: + """Read and validate one persisted hexadecimal instance identifier.""" + try: + encoded_identity = identity_path.read_text(encoding="ascii").strip() + except FileNotFoundError: + raise + except OSError as exc: + raise ComfyInstanceIdentityError( + f"Unable to read ComfyUI instance identity from {identity_path}: {exc}" + ) from exc + + try: + instance_id = bytes.fromhex(encoded_identity) + except ValueError as exc: + raise ComfyInstanceIdentityError( + f"ComfyUI instance identity at {identity_path} is not valid hexadecimal data." + ) from exc + if len(instance_id) != INSTANCE_ID_BYTES or encoded_identity != instance_id.hex(): + raise ComfyInstanceIdentityError( + f"ComfyUI instance identity at {identity_path} must contain exactly " + f"{INSTANCE_ID_BYTES} bytes encoded as lowercase hexadecimal." + ) + return instance_id + + +def _remove_temporary_identity_file(temporary_path: Path) -> None: + """Remove an unpublished identity candidate without masking the primary result.""" + try: + temporary_path.unlink() + except FileNotFoundError: + return + except OSError as exc: + logger.warning("Unable to remove temporary instance identity %s: %s", temporary_path, exc) + + +def _publish_instance_id(identity_path: Path, instance_id: bytes) -> bool: + """Atomically publish an identifier and return whether this process won creation.""" + try: + identity_path.parent.mkdir(parents=True, exist_ok=True) + except OSError as exc: + raise ComfyInstanceIdentityError( + f"Unable to create instance identity directory {identity_path.parent}: {exc}" + ) from exc + + temporary_path: Path | None = None + try: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="ascii", + dir=identity_path.parent, + prefix=f".{identity_path.name}.", + suffix=".tmp", + delete=False, + ) as temporary_file: + temporary_path = Path(temporary_file.name) + temporary_file.write(f"{instance_id.hex()}\n") + temporary_file.flush() + os.fsync(temporary_file.fileno()) + try: + os.link(temporary_path, identity_path) + except FileExistsError: + return False + except OSError as exc: + raise ComfyInstanceIdentityError( + f"Unable to publish ComfyUI instance identity at {identity_path}: {exc}" + ) from exc + return True + finally: + if temporary_path is not None: + _remove_temporary_identity_file(temporary_path) + + +def load_or_create_instance_id(identity_path: Path) -> bytes: + """Return the stable 64-bit identifier stored at the requested path.""" + try: + return _read_instance_id(identity_path) + except FileNotFoundError: + pass + + candidate = secrets.token_bytes(INSTANCE_ID_BYTES) + if _publish_instance_id(identity_path, candidate): + logger.info( + "Created %d-bit ComfyUI instance identity at %s.", + INSTANCE_ID_ENTROPY_BITS, + identity_path, + ) + return candidate + return _read_instance_id(identity_path) + + +def instance_id_base64(instance_id: bytes) -> str: + """Encode a 64-bit instance identifier as unpadded URL-safe Base64.""" + if len(instance_id) != INSTANCE_ID_BYTES: + raise ValueError(f"Instance identifier must contain exactly {INSTANCE_ID_BYTES} bytes.") + return base64.urlsafe_b64encode(instance_id).decode("ascii").rstrip("=") + + +def modal_app_name_for_instance(instance_id: bytes) -> str: + """Return the Modal app name namespaced to one persistent ComfyUI instance.""" + return f"{MODAL_APP_PREFIX}-{instance_id_base64(instance_id)}" diff --git a/settings.py b/settings.py index 3f615a5..9a810fa 100644 --- a/settings.py +++ b/settings.py @@ -2,12 +2,26 @@ from __future__ import annotations +import importlib import logging import os from dataclasses import dataclass from functools import lru_cache from pathlib import Path +if __package__: + from .instance_identity import ( + INSTANCE_ID_FILENAME, + load_or_create_instance_id, + modal_app_name_for_instance, + ) +else: # pragma: no cover - the stable cloud entrypoint imports this module top-level. + from instance_identity import ( + INSTANCE_ID_FILENAME, + load_or_create_instance_id, + modal_app_name_for_instance, + ) + logger = logging.getLogger(__name__) _SETTINGS_ENV_KEYS = ( @@ -18,6 +32,7 @@ _SETTINGS_ENV_KEYS = ( "COMFY_MODAL_SYNC_CUSTOM_NODES", "COMFY_MODAL_LOCAL_STORAGE_ROOT", "COMFY_MODAL_APP_NAME", + "COMFY_MODAL_INSTANCE_ID_PATH", "COMFY_MODAL_AUTO_DEPLOY", "COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK", "COMFY_MODAL_ENABLE_MEMORY_SNAPSHOT", @@ -206,6 +221,51 @@ def _discover_custom_nodes_dir(repo_root: Path, comfyui_root: Path | None) -> Pa return None +def _discover_comfyui_user_directory(comfyui_root: Path | None) -> Path | None: + """Return ComfyUI's effective user directory when it can be resolved.""" + try: + folder_paths = importlib.import_module("folder_paths") + except ModuleNotFoundError as exc: + if exc.name != "folder_paths": + logger.debug("Unable to import folder_paths while resolving the user directory: %s", exc) + else: + get_user_directory = getattr(folder_paths, "get_user_directory", None) + if callable(get_user_directory): + return Path(get_user_directory()).expanduser().resolve() + if comfyui_root is None: + return None + return (comfyui_root / "user").resolve() + + +def _resolve_modal_app_name(comfyui_root: Path | None, execution_mode: str) -> str: + """Return the explicit or persistent per-ComfyUI Modal app name.""" + configured_name = os.getenv("COMFY_MODAL_APP_NAME") + if configured_name is not None: + resolved_name = configured_name.strip() + if not resolved_name: + raise ValueError("COMFY_MODAL_APP_NAME must not be empty.") + return resolved_name + + identity_path = _read_path_env("COMFY_MODAL_INSTANCE_ID_PATH") + if identity_path is None: + user_directory = _discover_comfyui_user_directory(comfyui_root) + if user_directory is not None: + identity_path = user_directory / INSTANCE_ID_FILENAME + if identity_path is None: + if execution_mode == "remote": + raise RuntimeError( + "Remote execution requires a persistent ComfyUI instance identity, but the " + "ComfyUI user directory could not be resolved. Set COMFY_MODAL_COMFYUI_ROOT, " + "COMFY_MODAL_INSTANCE_ID_PATH, or COMFY_MODAL_APP_NAME explicitly." + ) + logger.debug("Using the legacy local app name because no ComfyUI user directory was found.") + return "comfy-modal-sync" + + app_name = modal_app_name_for_instance(load_or_create_instance_id(identity_path)) + logger.info("Using per-ComfyUI Modal app %s from identity file %s.", app_name, identity_path) + return app_name + + def _settings_env_signature() -> tuple[tuple[str, str | None], ...]: """Return the environment values that affect resolved Modal-Sync settings.""" return tuple((key, os.getenv(key)) for key in _SETTINGS_ENV_KEYS) @@ -221,6 +281,7 @@ def _get_settings_cached( comfyui_root = _discover_comfyui_root(repo_root) custom_nodes_dir = _discover_custom_nodes_dir(repo_root, comfyui_root) execution_mode = os.getenv("COMFY_MODAL_EXECUTION_MODE", "local").strip().lower() + app_name = _resolve_modal_app_name(comfyui_root, execution_mode) sync_custom_nodes = _read_bool_env("COMFY_MODAL_SYNC_CUSTOM_NODES") if sync_custom_nodes is None: sync_custom_nodes = execution_mode != "local" @@ -230,7 +291,7 @@ def _get_settings_cached( ) settings = ModalSyncSettings( - app_name=(app_name := os.getenv("COMFY_MODAL_APP_NAME", "comfy-modal-sync")), + app_name=app_name, auto_deploy=_read_bool_env("COMFY_MODAL_AUTO_DEPLOY") is not False, allow_ephemeral_fallback=_read_bool_env("COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK") or False, enable_memory_snapshot=_read_bool_env("COMFY_MODAL_ENABLE_MEMORY_SNAPSHOT") is not False, diff --git a/tests/conftest.py b/tests/conftest.py index 5aa04db..bdc923e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ import pytest REPO_ROOT = Path(__file__).resolve().parents[1] PACKAGE_NAME = "comfyui_modal_sync_under_test" +os.environ["COMFY_MODAL_APP_NAME"] = "comfy-modal-sync" def _comfyui_root() -> Path: @@ -94,6 +95,12 @@ def modal_sdk_module(extension_package: object) -> object: return importlib.import_module(f"{PACKAGE_NAME}.modal_sdk") +@pytest.fixture(scope="session") +def instance_identity_module(extension_package: object) -> object: + """Return the persistent ComfyUI instance identity module.""" + return importlib.import_module(f"{PACKAGE_NAME}.instance_identity") + + @pytest.fixture(scope="session") def api_intercept_module(extension_package: object) -> object: """Return the prompt interception module.""" @@ -159,7 +166,9 @@ def modal_cloud_module() -> object: def reset_modal_environment(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """Isolate Modal-Sync environment variables between tests.""" monkeypatch.setenv("COMFY_MODAL_EXECUTION_MODE", "local") + monkeypatch.setenv("COMFY_MODAL_APP_NAME", "comfy-modal-sync") monkeypatch.setenv("COMFY_MODAL_LOCAL_STORAGE_ROOT", str(tmp_path / "storage")) + monkeypatch.delenv("COMFY_MODAL_INSTANCE_ID_PATH", raising=False) monkeypatch.delenv("COMFY_MODAL_AUTO_DEPLOY", raising=False) monkeypatch.delenv("COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK", raising=False) monkeypatch.delenv("COMFY_MODAL_CUSTOM_NODES_DIR", raising=False) diff --git a/tests/test_instance_identity.py b/tests/test_instance_identity.py new file mode 100644 index 0000000..38e1cd6 --- /dev/null +++ b/tests/test_instance_identity.py @@ -0,0 +1,68 @@ +"""Tests for persistent per-ComfyUI Modal app identities.""" + +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import Any + +import pytest + + +def test_instance_identity_is_persisted_and_reused( + instance_identity_module: Any, + tmp_path: Path, +) -> None: + """A generated 64-bit identifier should remain stable across later loads.""" + identity_path = tmp_path / "user" / instance_identity_module.INSTANCE_ID_FILENAME + + first_identity = instance_identity_module.load_or_create_instance_id(identity_path) + second_identity = instance_identity_module.load_or_create_instance_id(identity_path) + + assert first_identity == second_identity + assert len(first_identity) == instance_identity_module.INSTANCE_ID_BYTES + assert identity_path.read_text(encoding="ascii").strip() == first_identity.hex() + + +def test_instance_identity_uses_unpadded_url_safe_base64( + instance_identity_module: Any, +) -> None: + """The app suffix should encode all 64 bits into eleven URL-safe characters.""" + instance_id = bytes.fromhex("0001020304050607") + + app_name = instance_identity_module.modal_app_name_for_instance(instance_id) + + assert app_name == "comfy-modal-sync-AAECAwQFBgc" + + +def test_concurrent_identity_creation_publishes_one_winner( + instance_identity_module: Any, + tmp_path: Path, +) -> None: + """Concurrent first startups should all observe the same atomically published identity.""" + identity_path = tmp_path / "user" / instance_identity_module.INSTANCE_ID_FILENAME + + with ThreadPoolExecutor(max_workers=16) as executor: + identities = list( + executor.map( + lambda _: instance_identity_module.load_or_create_instance_id(identity_path), + range(64), + ) + ) + + assert len(set(identities)) == 1 + assert list(identity_path.parent.glob(f".{identity_path.name}.*.tmp")) == [] + + +def test_corrupt_instance_identity_is_not_silently_replaced( + instance_identity_module: Any, + tmp_path: Path, +) -> None: + """Malformed persistent state should fail rather than silently select a new Modal app.""" + identity_path = tmp_path / instance_identity_module.INSTANCE_ID_FILENAME + identity_path.write_text("not-an-identity\n", encoding="ascii") + + with pytest.raises(instance_identity_module.ComfyInstanceIdentityError): + instance_identity_module.load_or_create_instance_id(identity_path) + + assert identity_path.read_text(encoding="ascii") == "not-an-identity\n" diff --git a/tests/test_modal_executor_node.py b/tests/test_modal_executor_node.py index c302941..863d960 100644 --- a/tests/test_modal_executor_node.py +++ b/tests/test_modal_executor_node.py @@ -1321,6 +1321,25 @@ def test_stable_modal_cloud_entry_imports_without_modal_sdk( assert hasattr(modal_cloud_module, "RemoteEngine") +def test_modal_cloud_image_environment_preserves_unique_app_name( + modal_cloud_module: Any, +) -> None: + """Remote workers should receive the same per-ComfyUI app name resolved locally.""" + settings = types.SimpleNamespace( + app_name="comfy-modal-sync-AAECAwQFBgc", + stream_event_queue_maxsize=256, + bridge_inline_max_bytes=1024, + invocation_result_inline_max_bytes=2048, + execution_timeout_seconds=3600, + startup_timeout_seconds=900, + ) + + image_environment = modal_cloud_module._modal_image_environment(settings, "fingerprint-1") + + assert image_environment["COMFY_MODAL_APP_NAME"] == "comfy-modal-sync-AAECAwQFBgc" + assert image_environment["COMFY_MODAL_RUNTIME_FINGERPRINT"] == "fingerprint-1" + + def test_modal_cloud_installs_timestamped_logger_handler( modal_cloud_module: Any, ) -> None: diff --git a/tests/test_settings.py b/tests/test_settings.py index 712dcbb..b4cd633 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from pathlib import Path from typing import Any @@ -59,6 +60,49 @@ def test_settings_reads_modal_gpu_override( assert settings.modal_gpu == "L40S" +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,