Files
ttulttul-ComfyUI-Modal/tests/test_sync_engine_coordinator.py

644 lines
23 KiB
Python

"""Tests for the sync engine coordinator boundary."""
from __future__ import annotations
from sync_engine_test_support import * # noqa: F401,F403
def test_sync_file_deduplicates_by_hash(
settings_module: Any,
sync_engine_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""Repeated file syncs should reuse the same remote path and sync-index record."""
monkeypatch.setattr(sync_engine_module._sync_backends, "modal", None)
asset_path = tmp_path / "model.safetensors"
asset_path.write_bytes(b"model-bytes")
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=None,
)
engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings)
first = engine.sync_file(asset_path)
second = engine.sync_file(asset_path)
assert first.remote_path == second.remote_path
assert first.sha256 == second.sha256
assert first.uploaded is True
assert second.uploaded is False
assert (settings.local_storage_root / first.remote_path.lstrip("/")).exists()
sync_index_path = settings.local_storage_root / "metadata" / "sync_index.json"
assert sync_index_path.exists()
assert first.remote_path in sync_index_path.read_text(encoding="utf-8")
def test_request_asset_cache_syncs_repeated_prompt_asset_once(
settings_module: Any,
sync_engine_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""Repeated references across remote nodes should share one request sync decision."""
monkeypatch.setattr(sync_engine_module._sync_backends, "modal", None)
asset_path = tmp_path / "model.safetensors"
asset_path.write_bytes(b"model-bytes")
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=None,
)
engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings)
sync_calls: list[Path] = []
original_sync_file = engine.sync_file
def recording_sync_file(local_path: Path, *args: Any, **kwargs: Any) -> Any:
"""Record actual request sync decisions before delegating."""
sync_calls.append(local_path.resolve())
return original_sync_file(local_path, *args, **kwargs)
monkeypatch.setattr(engine, "sync_file", recording_sync_file)
request_cache = engine.create_request_asset_cache(
({"model": str(asset_path)}, {"second_model": str(asset_path)})
)
first_inputs, first_assets = engine.sync_prompt_inputs(
{"model": str(asset_path)},
request_cache=request_cache,
)
second_inputs, second_assets = engine.sync_prompt_inputs(
{"second_model": str(asset_path)},
request_cache=request_cache,
)
assert sync_calls == [asset_path.resolve()]
assert first_inputs["model"] == second_inputs["second_model"]
assert first_assets == second_assets
assert request_cache.synced_assets() == (first_assets[0],)
def test_sync_file_emits_upload_status(
settings_module: Any,
sync_engine_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""Uploading a new asset should emit a status message naming the uploaded file."""
monkeypatch.setattr(sync_engine_module._sync_backends, "modal", None)
asset_path = tmp_path / "model.safetensors"
asset_path.write_bytes(b"model-bytes")
observed_statuses: list[tuple[str, int | None, int | None]] = []
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=None,
)
engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings)
engine.sync_file(
asset_path,
status_callback=lambda message, current, total: observed_statuses.append(
(message, current, total)
),
item_index=1,
total_items=2,
)
assert observed_statuses == [("Uploading asset 1/2 to Modal: model.safetensors", 1, 2)]
def test_sync_file_uses_self_hosted_destination_in_ssh_mode(
settings_module: Any,
sync_engine_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""SSH uploads should not describe the destination as Modal."""
monkeypatch.setattr(sync_engine_module._sync_backends, "modal", None)
asset_path = tmp_path / "model.safetensors"
asset_path.write_bytes(b"model-bytes")
observed_statuses: list[tuple[str, int | None, int | None]] = []
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="ssh_docker",
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,
)
engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings)
engine.sync_file(
asset_path,
status_callback=lambda message, current, total: observed_statuses.append(
(message, current, total)
),
)
assert observed_statuses == [
("Uploading asset to the self-hosted worker: model.safetensors", None, None)
]
def test_vast_sync_materializes_registered_huggingface_asset_before_upload(
settings_module: Any,
sync_engine_module: Any,
huggingface_assets_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""A registered Vast asset should be fetched remotely with the local hash contract."""
asset_path = tmp_path / "model.safetensors"
asset_path.write_bytes(b"hub-backed-model")
sha256 = huggingface_assets_module.sha256_file(asset_path)
source = huggingface_assets_module.HuggingFaceAssetSource(
repo_id="owner/model",
revision="a" * 40,
filename="weights/model.safetensors",
sha256=sha256,
size_bytes=asset_path.stat().st_size,
)
registry = huggingface_assets_module.HuggingFaceAssetRegistry(
tmp_path / "user" / "huggingface-assets.json"
)
registry.upsert(source)
observed_materializations: list[tuple[Any, str, str | None]] = []
observed_uploads: list[tuple[Path, str]] = []
observed_statuses: list[tuple[str, int | None, int | None]] = []
class MaterializingVolume:
"""Expose the optional remote acquisition capability under test."""
def exists(self, remote_path: str) -> bool:
"""Report an initially empty remote store."""
del remote_path
return False
def put_file(self, local_path: Path, remote_path: str) -> None:
"""Record a fallback upload that should not be needed."""
observed_uploads.append((local_path, remote_path))
def put_bytes(self, payload: bytes, remote_path: str) -> None:
"""Accept sync-index sentinel bytes for protocol completeness."""
del payload, remote_path
def materialize_huggingface_file(
self,
registered_source: Any,
remote_path: str,
*,
token: str | None,
) -> bool:
"""Record and accept one direct Hub materialization."""
observed_materializations.append((registered_source, remote_path, token))
return True
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="vast",
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,
)
monkeypatch.setenv("HF_TOKEN", "secret-hf-token")
engine = sync_engine_module.ModalAssetSyncEngine(
volume=MaterializingVolume(),
settings=settings,
huggingface_asset_registry=registry,
)
result = engine.sync_file(
asset_path,
status_callback=lambda message, current, total: observed_statuses.append(
(message, current, total)
),
item_index=1,
total_items=2,
)
assert observed_uploads == []
assert observed_materializations == [
(source, result.remote_path, "secret-hf-token")
]
assert observed_statuses == [
(
"Downloading asset 1/2 from Hugging Face on Vast.ai: model.safetensors",
1,
2,
)
]
def test_vast_sync_automatically_discovers_huggingface_asset(
settings_module: Any,
sync_engine_module: Any,
huggingface_assets_module: Any,
tmp_path: Path,
) -> None:
"""Vast sync should discover provenance without a user-run registration command."""
asset_path = tmp_path / "automatic.safetensors"
asset_path.write_bytes(b"automatically-discovered")
source = huggingface_assets_module.HuggingFaceAssetSource(
repo_id="owner/automatic",
revision="9" * 40,
filename="automatic.safetensors",
sha256=huggingface_assets_module.sha256_file(asset_path),
size_bytes=asset_path.stat().st_size,
)
statuses: list[str] = []
discoveries: list[tuple[Path, str]] = []
class DiscoveringVolume:
"""Accept direct Hub materialization and reject local upload use."""
def exists(self, remote_path: str) -> bool:
"""Report no preexisting remote asset."""
del remote_path
return False
def put_file(self, local_path: Path, remote_path: str) -> None:
"""Fail if automatic discovery unexpectedly falls back to upload."""
raise AssertionError(f"Unexpected upload of {local_path} to {remote_path}")
def put_bytes(self, payload: bytes, remote_path: str) -> None:
"""Accept protocol metadata writes."""
del payload, remote_path
def materialize_huggingface_file(
self,
registered_source: Any,
remote_path: str,
*,
token: str | None,
) -> bool:
"""Accept the automatically discovered immutable source."""
del remote_path, token
assert registered_source == source
return True
class FakeDiscovery:
"""Return an automatically verified source for the local asset."""
def discover(self, local_path: Path, *, sha256: str) -> Any:
"""Record discovery and return the expected source."""
discoveries.append((local_path, sha256))
return source
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="vast",
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,
)
engine = sync_engine_module.ModalAssetSyncEngine(
volume=DiscoveringVolume(),
settings=settings,
huggingface_asset_registry=huggingface_assets_module.HuggingFaceAssetRegistry(
tmp_path / "empty-registry.json"
),
huggingface_asset_discovery=FakeDiscovery(),
)
engine.sync_file(
asset_path,
status_callback=lambda message, current, total: statuses.append(message),
)
assert discoveries == [(asset_path, source.sha256)]
assert statuses == [
"Identifying Hugging Face source for Vast.ai: automatic.safetensors",
"Downloading asset from Hugging Face on Vast.ai: automatic.safetensors",
]
def test_vast_sync_falls_back_to_upload_after_huggingface_failure(
settings_module: Any,
sync_engine_module: Any,
huggingface_assets_module: Any,
tmp_path: Path,
) -> None:
"""A remote Hub miss should retain the existing content-addressed upload behavior."""
asset_path = tmp_path / "model.safetensors"
asset_path.write_bytes(b"fallback-model")
source = huggingface_assets_module.HuggingFaceAssetSource(
repo_id="owner/model",
revision="b" * 40,
filename="model.safetensors",
sha256=huggingface_assets_module.sha256_file(asset_path),
size_bytes=asset_path.stat().st_size,
)
registry = huggingface_assets_module.HuggingFaceAssetRegistry(
tmp_path / "user" / "huggingface-assets.json"
)
registry.upsert(source)
uploads: list[tuple[Path, str]] = []
statuses: list[str] = []
class FailingMaterializingVolume:
"""Decline remote acquisition and record the fallback upload."""
def exists(self, remote_path: str) -> bool:
"""Report no cached files."""
del remote_path
return False
def put_file(self, local_path: Path, remote_path: str) -> None:
"""Record the safe fallback path."""
uploads.append((local_path, remote_path))
def put_bytes(self, payload: bytes, remote_path: str) -> None:
"""Accept protocol metadata writes."""
del payload, remote_path
def materialize_huggingface_file(
self,
registered_source: Any,
remote_path: str,
*,
token: str | None,
) -> bool:
"""Simulate an unavailable or unauthorized Hub source."""
del registered_source, remote_path, token
return False
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="vast",
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,
)
engine = sync_engine_module.ModalAssetSyncEngine(
volume=FailingMaterializingVolume(),
settings=settings,
huggingface_asset_registry=registry,
)
result = engine.sync_file(
asset_path,
status_callback=lambda message, current, total: statuses.append(message),
)
assert uploads == [(asset_path.resolve(), result.remote_path)]
assert statuses == [
"Downloading asset from Hugging Face on Vast.ai: model.safetensors",
"Uploading asset to the Vast.ai instance: model.safetensors",
]
def test_sync_file_reuses_sync_index_record_for_existing_remote_payload(
settings_module: Any,
sync_engine_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""An indexed deterministic asset should be reused without a second upload."""
monkeypatch.setattr(sync_engine_module._sync_backends, "modal", None)
asset_path = tmp_path / "model.safetensors"
asset_path.write_bytes(b"model-bytes")
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=None,
)
class RecordingVolume:
"""Volume double that records any attempted uploads."""
def __init__(self) -> None:
"""Initialize captured writes."""
self.put_file_calls: list[tuple[Path, str]] = []
def put_file(self, local_path: Path, remote_path: str) -> None:
self.put_file_calls.append((local_path, remote_path))
def put_bytes(self, payload: bytes, remote_path: str) -> None:
raise AssertionError("Sync records should not be mirrored as marker files.")
def exists(self, remote_path: str) -> bool:
raise AssertionError("Sync should not probe volume metadata for indexed payloads.")
volume = RecordingVolume()
engine = sync_engine_module.ModalAssetSyncEngine(volume=volume, settings=settings)
engine.sync_index.put(
engine._asset_sync_index_key(engine._hash_file(asset_path)),
{"remote_path": "/assets/existing_model.safetensors", "source": "existing"},
)
synced_asset = engine.sync_file(asset_path)
assert synced_asset.uploaded is False
assert synced_asset.remote_path == "/assets/existing_model.safetensors"
assert volume.put_file_calls == []
def test_remote_sync_index_discards_stale_volume_epoch_and_reuploads_missing_payloads(
settings_module: Any,
sync_engine_module: Any,
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""A fresh volume should ignore stale sync-index records from an older volume epoch."""
class FakeBatch:
"""Capture files uploaded through Modal batch_upload."""
def __init__(self, volume: "FakeVolume") -> None:
"""Store the backing volume for uploaded-path tracking."""
self.volume = volume
def __enter__(self) -> "FakeBatch":
"""Return the active batch context."""
return self
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
"""Close the fake batch context."""
return None
def put_file(self, source: Any, remote_path: str) -> None:
"""Record the uploaded remote path as present in the fake volume."""
del source
self.volume.paths.add(remote_path)
self.volume.uploads.append(remote_path)
class FakeVolume:
"""Minimal Modal volume double with listdir accounting."""
def __init__(self) -> None:
"""Initialize fake storage state."""
self.paths: set[str] = set()
self.uploads: list[str] = []
def listdir(self, remote_path: str, recursive: bool = False) -> list[str]:
"""Return a listing for known paths."""
del recursive
return [remote_path] if remote_path in self.paths else []
def batch_upload(self) -> FakeBatch:
"""Return a fake uploader."""
return FakeBatch(self)
fake_volume = FakeVolume()
fake_dict: dict[str, Any] = {}
class FakeModal:
"""Minimal Modal SDK double exposing both Volume and Dict backends."""
exception = type(
"FakeExceptionNamespace",
(),
{
"NotFoundError": FileNotFoundError,
"ResourceExhaustedError": RuntimeError,
},
)
class Dict:
"""Namespace for sync-index lookups."""
@staticmethod
def from_name(name: str, create_if_missing: bool = False) -> dict[str, Any]:
"""Return the shared fake dict store for any lookup."""
del name, create_if_missing
return fake_dict
class Volume:
"""Namespace for volume lookups."""
@staticmethod
def from_name(name: str, create_if_missing: bool = False) -> FakeVolume:
"""Return the fake volume for any lookup."""
del name, create_if_missing
return fake_volume
monkeypatch.setattr(sync_engine_module._sync_backends, "modal", FakeModal)
custom_nodes_dir = tmp_path / "custom_nodes"
package_dir = custom_nodes_dir / "example"
package_dir.mkdir(parents=True)
(package_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="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=custom_nodes_dir,
)
engine = sync_engine_module.ModalAssetSyncEngine.from_environment(settings)
entry_hash = engine._hash_directory(package_dir)
fake_dict[f"{settings.volume_name}:current_volume_epoch"] = {
"epoch": "stale-epoch",
"remote_path": "/sync_index_epochs/stale-epoch.json",
"sentinel_path": "/sync_index_epochs/stale-epoch.json",
}
fake_dict[
f"{settings.volume_name}:epoch:stale-epoch:custom_nodes_entry:example:{entry_hash}"
] = {
"remote_path": engine._custom_nodes_archive_remote_path("example", entry_hash),
"source": str(package_dir),
}
bundle = engine.sync_custom_nodes_directory()
assert bundle is not None
assert bundle.uploaded is True
assert any(path.startswith("/sync_index_epochs/") for path in fake_volume.uploads)
assert any(path.startswith("/custom_nodes/entries/example/") for path in fake_volume.uploads)
assert fake_dict[f"{settings.volume_name}:current_volume_epoch"]["epoch"] != "stale-epoch"