ComfyUI/tests-unit/app_test/frontend_manager_test.py
DrJKL 672b4a255f
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Simplify auto-managed frontend cleanup with on-disk markers
Replace the .auto_managed.json sidecar with a .auto_managed/ marker
directory. Tracked version names now come from real single-component
dirents, removing the untrusted-input parsing, path-traversal checks,
and rmtree boundary guards that the JSON design required.

Also fix the dead Optional handling in _prune_auto_managed_versions,
extract _ensure_release_downloaded to drop the download_succeeded flag,
and reuse _provider_dir in init_frontend_unsafe.

Amp-Thread-ID: https://ampcode.com/threads/T-019eb879-1e6f-77aa-abb3-4d229d18061f
Co-authored-by: Amp <amp@ampcode.com>
2026-06-11 14:24:44 -07:00

445 lines
13 KiB
Python

import argparse
from pathlib import Path
import pytest
from requests.exceptions import HTTPError
from unittest.mock import patch, mock_open
from app.frontend_management import (
FrontendManager,
FrontEndProvider,
Release,
)
from comfy.cli_args import DEFAULT_VERSION_STRING
@pytest.fixture
def mock_releases():
return [
Release(
id=1,
tag_name="1.0.0",
name="Release 1.0.0",
prerelease=False,
created_at="2022-01-01T00:00:00Z",
published_at="2022-01-01T00:00:00Z",
body="Release notes for 1.0.0",
assets=[{"name": "dist.zip", "url": "https://example.com/dist.zip"}],
),
Release(
id=2,
tag_name="2.0.0",
name="Release 2.0.0",
prerelease=False,
created_at="2022-02-01T00:00:00Z",
published_at="2022-02-01T00:00:00Z",
body="Release notes for 2.0.0",
assets=[{"name": "dist.zip", "url": "https://example.com/dist.zip"}],
),
]
@pytest.fixture
def mock_provider(mock_releases):
provider = FrontEndProvider(
owner="test-owner",
repo="test-repo",
)
provider.all_releases = mock_releases
provider.latest_release = mock_releases[1]
FrontendManager.PROVIDERS = [provider]
return provider
@pytest.fixture(autouse=True)
def clear_cache():
import utils.install_util
import app.frontend_management
utils.install_util.PACKAGE_VERSIONS = {}
app.frontend_management.COMFY_PACKAGE_VERSIONS = []
def test_get_release(mock_provider, mock_releases):
version = "1.0.0"
release = mock_provider.get_release(version)
assert release == mock_releases[0]
def test_get_release_latest(mock_provider, mock_releases):
version = "latest"
release = mock_provider.get_release(version)
assert release == mock_releases[1]
def test_get_release_invalid_version(mock_provider):
version = "invalid"
with pytest.raises(ValueError):
mock_provider.get_release(version)
def test_init_frontend_default():
version_string = DEFAULT_VERSION_STRING
frontend_path = FrontendManager.init_frontend(version_string)
assert frontend_path == FrontendManager.default_frontend_path()
def test_init_frontend_invalid_version():
version_string = "test-owner/test-repo@1.100.99"
with pytest.raises(HTTPError):
FrontendManager.init_frontend_unsafe(version_string)
def test_init_frontend_invalid_provider():
version_string = "invalid/invalid@latest"
with pytest.raises(HTTPError):
FrontendManager.init_frontend_unsafe(version_string)
@pytest.fixture
def mock_os_functions():
with (
patch("app.frontend_management.os.makedirs") as mock_makedirs,
patch("app.frontend_management.os.listdir") as mock_listdir,
patch("app.frontend_management.os.rmdir") as mock_rmdir,
):
mock_listdir.return_value = [] # Simulate empty directory
yield mock_makedirs, mock_listdir, mock_rmdir
@pytest.fixture
def mock_download():
with patch("app.frontend_management.download_release_asset_zip") as mock:
mock.side_effect = Exception("Download failed") # Simulate download failure
yield mock
def test_finally_block(mock_os_functions, mock_download, mock_provider):
# Arrange
mock_makedirs, mock_listdir, mock_rmdir = mock_os_functions
version_string = "test-owner/test-repo@1.0.0"
# Act & Assert
with pytest.raises(Exception):
FrontendManager.init_frontend_unsafe(version_string, mock_provider)
# Assert
mock_makedirs.assert_called_once()
mock_download.assert_called_once()
mock_listdir.assert_called_once()
mock_rmdir.assert_called_once()
def test_parse_version_string():
version_string = "owner/repo@1.0.0"
repo_owner, repo_name, version = FrontendManager.parse_version_string(
version_string
)
assert repo_owner == "owner"
assert repo_name == "repo"
assert version == "1.0.0"
def test_parse_version_string_invalid():
version_string = "invalid"
with pytest.raises(argparse.ArgumentTypeError):
FrontendManager.parse_version_string(version_string)
def test_init_frontend_default_with_mocks():
# Arrange
version_string = DEFAULT_VERSION_STRING
# Act
with (
patch("app.frontend_management.check_comfy_packages_versions") as mock_check,
patch.object(
FrontendManager, "default_frontend_path", return_value="/mocked/path"
),
):
frontend_path = FrontendManager.init_frontend(version_string)
# Assert
assert frontend_path == "/mocked/path"
mock_check.assert_called_once()
def test_init_frontend_fallback_on_error():
# Arrange
version_string = "test-owner/test-repo@1.0.0"
# Act
with (
patch.object(
FrontendManager, "init_frontend_unsafe", side_effect=Exception("Test error")
),
patch("app.frontend_management.check_comfy_packages_versions") as mock_check,
patch.object(
FrontendManager, "default_frontend_path", return_value="/default/path"
),
):
frontend_path = FrontendManager.init_frontend(version_string)
# Assert
assert frontend_path == "/default/path"
mock_check.assert_called_once()
def test_get_frontend_version():
# Arrange
expected_version = "1.25.0"
mock_requirements_content = """torch
torchsde
comfyui-frontend-package==1.25.0
other-package==1.0.0
numpy"""
# Act
with patch("builtins.open", mock_open(read_data=mock_requirements_content)):
version = FrontendManager.get_required_frontend_version()
# Assert
assert version == expected_version
def test_get_frontend_version_invalid_semver():
# Arrange
mock_requirements_content = """torch
torchsde
comfyui-frontend-package==1.29.3.75
other-package==1.0.0
numpy"""
# Act
with patch("builtins.open", mock_open(read_data=mock_requirements_content)):
version = FrontendManager.get_required_frontend_version()
# Assert
assert version is None
def test_get_templates_version():
# Arrange
expected_version = "0.1.41"
mock_requirements_content = """torch
torchsde
comfyui-frontend-package==1.25.0
comfyui-workflow-templates==0.1.41
other-package==1.0.0
numpy"""
# Act
with patch("builtins.open", mock_open(read_data=mock_requirements_content)):
version = FrontendManager.get_required_templates_version()
# Assert
assert version == expected_version
def test_get_templates_version_not_found():
# Arrange
mock_requirements_content = """torch
torchsde
comfyui-frontend-package==1.25.0
other-package==1.0.0
numpy"""
# Act
with patch("builtins.open", mock_open(read_data=mock_requirements_content)):
version = FrontendManager.get_required_templates_version()
# Assert
assert version is None
def test_get_templates_version_invalid_semver():
# Arrange
mock_requirements_content = """torch
torchsde
comfyui-workflow-templates==1.0.0.beta
other-package==1.0.0
numpy"""
# Act
with patch("builtins.open", mock_open(read_data=mock_requirements_content)):
version = FrontendManager.get_required_templates_version()
# Assert
assert version is None
def test_get_installed_templates_version():
# Arrange
expected_version = "0.1.40"
# Act
with patch("app.frontend_management.version", return_value=expected_version):
version = FrontendManager.get_installed_templates_version()
# Assert
assert version == expected_version
def test_get_installed_templates_version_not_installed():
# Act
with patch(
"app.frontend_management.version", side_effect=Exception("Package not found")
):
version = FrontendManager.get_installed_templates_version()
# Assert
assert version is None
# Auto-managed @latest / @prerelease cleanup (CORE-285)
@pytest.fixture
def custom_frontends_root(tmp_path, monkeypatch):
root = tmp_path / "web_custom_versions"
root.mkdir()
monkeypatch.setattr(FrontendManager, "CUSTOM_FRONTENDS_ROOT", str(root))
return root
def _make_version_dir(root, owner, repo, version):
folder = root / f"{owner}_{repo}" / version
folder.mkdir(parents=True, exist_ok=True)
(folder / "index.html").write_text("<html></html>")
return folder
def test_auto_managed_markers_roundtrip(custom_frontends_root):
FrontendManager._mark_auto_managed("o", "r", "1.0.0")
FrontendManager._mark_auto_managed("o", "r", "1.1.0")
FrontendManager._mark_auto_managed("o", "r", "1.0.0") # idempotent
assert FrontendManager._read_auto_managed_versions("o", "r") == ["1.0.0", "1.1.0"]
def test_read_auto_managed_versions_missing(custom_frontends_root):
assert FrontendManager._read_auto_managed_versions("o", "r") == []
def test_mark_auto_managed_does_not_create_version_dir(custom_frontends_root):
FrontendManager._mark_auto_managed("o", "r", "1.0.0")
provider_dir = custom_frontends_root / "o_r"
# The marker is recorded, but no version directory is created as a side effect.
assert not (provider_dir / "1.0.0").exists()
assert FrontendManager._read_auto_managed_versions("o", "r") == ["1.0.0"]
def test_prune_only_touches_marked_siblings(custom_frontends_root):
# A sibling provider directory must never be affected by pruning.
sibling = _make_version_dir(custom_frontends_root, "other", "repo", "9.9.9")
_make_version_dir(custom_frontends_root, "o", "r", "1.0.0")
FrontendManager._mark_auto_managed("o", "r", "1.0.0")
FrontendManager._prune_auto_managed_versions("o", "r", keep_version="2.0.0")
assert sibling.exists()
assert not (custom_frontends_root / "o_r" / "1.0.0").exists()
def test_prune_auto_managed_versions_removes_stale_and_keeps_pinned(
custom_frontends_root,
):
_make_version_dir(custom_frontends_root, "o", "r", "1.0.0")
_make_version_dir(custom_frontends_root, "o", "r", "1.1.0")
pinned = _make_version_dir(custom_frontends_root, "o", "r", "1.2.0")
FrontendManager._mark_auto_managed("o", "r", "1.0.0")
FrontendManager._mark_auto_managed("o", "r", "1.1.0")
FrontendManager._prune_auto_managed_versions("o", "r", keep_version="1.1.0")
provider_dir = custom_frontends_root / "o_r"
assert not (provider_dir / "1.0.0").exists()
assert (provider_dir / "1.1.0").exists()
assert pinned.exists()
assert FrontendManager._read_auto_managed_versions("o", "r") == ["1.1.0"]
def test_untrack_auto_managed_version_does_not_delete_folder(custom_frontends_root):
version_dir = _make_version_dir(custom_frontends_root, "o", "r", "1.0.0")
FrontendManager._mark_auto_managed("o", "r", "1.0.0")
FrontendManager._mark_auto_managed("o", "r", "1.1.0")
FrontendManager._untrack_auto_managed_version("o", "r", "1.0.0")
assert version_dir.exists()
assert FrontendManager._read_auto_managed_versions("o", "r") == ["1.1.0"]
def test_untrack_auto_managed_version_missing_is_noop(custom_frontends_root):
FrontendManager._untrack_auto_managed_version("o", "r", "1.0.0")
assert FrontendManager._read_auto_managed_versions("o", "r") == []
def test_init_frontend_latest_prunes_previous_auto_managed_versions(
custom_frontends_root, mock_provider, mock_releases
):
_make_version_dir(custom_frontends_root, "test-owner", "test-repo", "1.0.0")
pinned = _make_version_dir(
custom_frontends_root, "test-owner", "test-repo", "1.1.5"
)
FrontendManager._mark_auto_managed("test-owner", "test-repo", "1.0.0")
def fake_download(release, destination_path):
Path(destination_path).mkdir(parents=True, exist_ok=True)
(Path(destination_path) / "index.html").write_text("<html></html>")
with patch(
"app.frontend_management.download_release_asset_zip",
side_effect=fake_download,
):
result = FrontendManager.init_frontend_unsafe(
"test-owner/test-repo@latest", mock_provider
)
provider_dir = custom_frontends_root / "test-owner_test-repo"
assert Path(result) == provider_dir / "2.0.0"
assert (provider_dir / "2.0.0").exists()
assert FrontendManager._read_auto_managed_versions(
"test-owner", "test-repo"
) == ["2.0.0"]
assert not (provider_dir / "1.0.0").exists()
assert pinned.exists()
def test_init_frontend_explicit_version_promotes_out_of_auto_managed(
custom_frontends_root, mock_provider
):
_make_version_dir(custom_frontends_root, "test-owner", "test-repo", "1.0.0")
FrontendManager._mark_auto_managed("test-owner", "test-repo", "1.0.0")
result = FrontendManager.init_frontend_unsafe(
"test-owner/test-repo@v1.0.0", mock_provider
)
provider_dir = custom_frontends_root / "test-owner_test-repo"
assert Path(result) == provider_dir / "1.0.0"
assert FrontendManager._read_auto_managed_versions(
"test-owner", "test-repo"
) == []
assert (provider_dir / "1.0.0").exists()
def test_init_frontend_explicit_no_v_prefix_promotes_out_of_auto_managed(
custom_frontends_root, mock_provider
):
_make_version_dir(custom_frontends_root, "test-owner", "test-repo", "1.0.0")
FrontendManager._mark_auto_managed("test-owner", "test-repo", "1.0.0")
with patch(
"app.frontend_management.download_release_asset_zip"
) as mock_download_zip:
FrontendManager.init_frontend_unsafe(
"test-owner/test-repo@1.0.0", mock_provider
)
mock_download_zip.assert_not_called()
assert FrontendManager._read_auto_managed_versions(
"test-owner", "test-repo"
) == []