mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-03-14 21:47:37 +08:00
* feat(cli): expand --uv-compile to all node management commands with conflict attribution Add --uv-compile flag to reinstall, update, fix, restore-snapshot, restore-dependencies, and install-deps commands. Each skips per-node pip installs and runs batch uv pip compile after all operations. Change CollectedDeps.sources type to dict[str, list[tuple[str, str]]] to store (pack_path, pkg_spec) per requester. On resolution failure, _run_unified_resolve() cross-references conflict packages with sources using word-boundary regex and displays which node packs requested each conflicting package. Update EN/KO user docs and DESIGN/PRD developer docs to cover the expanded commands and conflict attribution output. Strengthen unit tests for sources tuple format and compile failure attribution. Bump version to 4.1b3. * refactor(cli): extract _finalize_resolve helper, add CNR nightly fallback and pydantic guard - Extract `_finalize_resolve()` to eliminate 7x duplicated uv-compile error handling blocks in cm_cli (~-85 lines) - Move conflict attribution regex to `attribute_conflicts()` in unified_dep_resolver.py for direct testability - Update 4 attribution tests to call production function instead of re-implementing regex - Add CNR nightly fallback: when node is absent from nightly manifest, fall back to cnr_map repository URL (glob + legacy) - Add pydantic Union guard: use getattr for is_unknown in uninstall and disable handlers to prevent Union type mismatch - Add E2E test suites for endpoint install/uninstall and uv-compile CLI commands (conflict + success cases) - Add nightly CNR fallback regression tests
255 lines
8.6 KiB
Python
255 lines
8.6 KiB
Python
"""E2E tests for cm-cli --uv-compile across all supported commands.
|
|
|
|
Requires a pre-built E2E environment (from setup_e2e_env.sh).
|
|
Set E2E_ROOT env var to point at it, or the tests will be skipped.
|
|
|
|
Supply-chain safety policy:
|
|
To prevent supply-chain attacks, E2E tests MUST only install node packs
|
|
from verified, controllable authors (ltdrdata, comfyanonymous, etc.).
|
|
Currently this suite uses only ltdrdata's dedicated test packs
|
|
(nodepack-test1-do-not-install, nodepack-test2-do-not-install) which
|
|
are intentionally designed for conflict testing and contain no
|
|
executable code. Adding packs from unverified sources is prohibited.
|
|
|
|
Usage:
|
|
E2E_ROOT=/tmp/e2e_full_test pytest tests/e2e/test_e2e_uv_compile.py -v
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
E2E_ROOT = os.environ.get("E2E_ROOT", "")
|
|
COMFYUI_PATH = os.path.join(E2E_ROOT, "comfyui") if E2E_ROOT else ""
|
|
CM_CLI = os.path.join(E2E_ROOT, "venv", "bin", "cm-cli") if E2E_ROOT else ""
|
|
CUSTOM_NODES = os.path.join(COMFYUI_PATH, "custom_nodes") if COMFYUI_PATH else ""
|
|
|
|
REPO_TEST1 = "https://github.com/ltdrdata/nodepack-test1-do-not-install"
|
|
REPO_TEST2 = "https://github.com/ltdrdata/nodepack-test2-do-not-install"
|
|
PACK_TEST1 = "nodepack-test1-do-not-install"
|
|
PACK_TEST2 = "nodepack-test2-do-not-install"
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not E2E_ROOT or not os.path.isfile(os.path.join(E2E_ROOT, ".e2e_setup_complete")),
|
|
reason="E2E_ROOT not set or E2E environment not ready (run setup_e2e_env.sh first)",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _run_cm_cli(*args: str, timeout: int = 180) -> subprocess.CompletedProcess:
|
|
"""Run cm-cli in the E2E environment."""
|
|
env = {**os.environ, "COMFYUI_PATH": COMFYUI_PATH}
|
|
return subprocess.run(
|
|
[CM_CLI, *args],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
env=env,
|
|
)
|
|
|
|
|
|
def _remove_pack(name: str) -> None:
|
|
"""Remove a node pack from custom_nodes (if it exists)."""
|
|
path = os.path.join(CUSTOM_NODES, name)
|
|
if os.path.islink(path):
|
|
os.unlink(path)
|
|
elif os.path.isdir(path):
|
|
shutil.rmtree(path, ignore_errors=True)
|
|
|
|
|
|
def _pack_exists(name: str) -> bool:
|
|
return os.path.isdir(os.path.join(CUSTOM_NODES, name))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_test_packs():
|
|
"""Ensure test node packs are removed before and after each test."""
|
|
_remove_pack(PACK_TEST1)
|
|
_remove_pack(PACK_TEST2)
|
|
yield
|
|
_remove_pack(PACK_TEST1)
|
|
_remove_pack(PACK_TEST2)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestInstall:
|
|
"""cm-cli install --uv-compile"""
|
|
|
|
def test_install_single_pack_resolves(self):
|
|
"""Install one test pack with --uv-compile → resolve succeeds."""
|
|
r = _run_cm_cli("install", "--uv-compile", REPO_TEST1)
|
|
combined = r.stdout + r.stderr
|
|
|
|
assert _pack_exists(PACK_TEST1)
|
|
assert "Installation was successful" in combined
|
|
assert "Resolved" in combined
|
|
|
|
def test_install_conflicting_packs_shows_attribution(self):
|
|
"""Install two conflicting packs → conflict attribution output."""
|
|
# Install first (no conflict yet)
|
|
r1 = _run_cm_cli("install", "--uv-compile", REPO_TEST1)
|
|
assert _pack_exists(PACK_TEST1)
|
|
assert "Resolved" in r1.stdout + r1.stderr
|
|
|
|
# Install second → conflict
|
|
r2 = _run_cm_cli("install", "--uv-compile", REPO_TEST2)
|
|
combined = r2.stdout + r2.stderr
|
|
|
|
assert _pack_exists(PACK_TEST2)
|
|
assert "Installation was successful" in combined
|
|
assert "Resolution failed" in combined
|
|
assert "Conflicting packages (by node pack):" in combined
|
|
assert PACK_TEST1 in combined
|
|
assert PACK_TEST2 in combined
|
|
assert "ansible" in combined.lower()
|
|
|
|
|
|
class TestReinstall:
|
|
"""cm-cli reinstall --uv-compile"""
|
|
|
|
def test_reinstall_with_uv_compile(self):
|
|
"""Reinstall an existing pack with --uv-compile."""
|
|
# Install first
|
|
_run_cm_cli("install", REPO_TEST1)
|
|
assert _pack_exists(PACK_TEST1)
|
|
|
|
# Reinstall with --uv-compile
|
|
r = _run_cm_cli("reinstall", "--uv-compile", REPO_TEST1)
|
|
combined = r.stdout + r.stderr
|
|
|
|
# uv-compile should run (resolve output present)
|
|
assert "Resolving dependencies" in combined
|
|
|
|
|
|
class TestUpdate:
|
|
"""cm-cli update --uv-compile"""
|
|
|
|
def test_update_single_with_uv_compile(self):
|
|
"""Update an installed pack with --uv-compile."""
|
|
_run_cm_cli("install", REPO_TEST1)
|
|
assert _pack_exists(PACK_TEST1)
|
|
|
|
r = _run_cm_cli("update", "--uv-compile", REPO_TEST1)
|
|
combined = r.stdout + r.stderr
|
|
|
|
assert "Resolving dependencies" in combined
|
|
|
|
def test_update_all_with_uv_compile(self):
|
|
"""update all --uv-compile runs uv-compile after updating."""
|
|
_run_cm_cli("install", REPO_TEST1)
|
|
assert _pack_exists(PACK_TEST1)
|
|
|
|
r = _run_cm_cli("update", "--uv-compile", "all")
|
|
combined = r.stdout + r.stderr
|
|
|
|
assert "Resolving dependencies" in combined
|
|
|
|
|
|
class TestFix:
|
|
"""cm-cli fix --uv-compile"""
|
|
|
|
def test_fix_single_with_uv_compile(self):
|
|
"""Fix an installed pack with --uv-compile."""
|
|
_run_cm_cli("install", REPO_TEST1)
|
|
assert _pack_exists(PACK_TEST1)
|
|
|
|
r = _run_cm_cli("fix", "--uv-compile", REPO_TEST1)
|
|
combined = r.stdout + r.stderr
|
|
|
|
assert "Resolving dependencies" in combined
|
|
|
|
def test_fix_all_with_uv_compile(self):
|
|
"""fix all --uv-compile runs uv-compile after fixing."""
|
|
_run_cm_cli("install", REPO_TEST1)
|
|
assert _pack_exists(PACK_TEST1)
|
|
|
|
r = _run_cm_cli("fix", "--uv-compile", "all")
|
|
combined = r.stdout + r.stderr
|
|
|
|
assert "Resolving dependencies" in combined
|
|
|
|
|
|
class TestUvCompileStandalone:
|
|
"""cm-cli uv-compile (standalone command)"""
|
|
|
|
def test_uv_compile_no_packs(self):
|
|
"""uv-compile with no node packs → 'No custom node packs found'."""
|
|
r = _run_cm_cli("uv-compile")
|
|
combined = r.stdout + r.stderr
|
|
|
|
# Only ComfyUI-Manager exists (no requirements.txt in it normally)
|
|
# so either "No custom node packs found" or resolves 0
|
|
assert r.returncode == 0 or "No custom node packs" in combined
|
|
|
|
def test_uv_compile_with_packs(self):
|
|
"""uv-compile after installing test pack → resolves."""
|
|
_run_cm_cli("install", REPO_TEST1)
|
|
assert _pack_exists(PACK_TEST1)
|
|
|
|
r = _run_cm_cli("uv-compile")
|
|
combined = r.stdout + r.stderr
|
|
|
|
assert "Resolving dependencies" in combined
|
|
assert "Resolved" in combined
|
|
|
|
def test_uv_compile_conflict_attribution(self):
|
|
"""uv-compile with conflicting packs → shows attribution."""
|
|
_run_cm_cli("install", REPO_TEST1)
|
|
_run_cm_cli("install", REPO_TEST2)
|
|
|
|
r = _run_cm_cli("uv-compile")
|
|
combined = r.stdout + r.stderr
|
|
|
|
assert r.returncode != 0
|
|
assert "Conflicting packages (by node pack):" in combined
|
|
assert PACK_TEST1 in combined
|
|
assert PACK_TEST2 in combined
|
|
|
|
|
|
class TestRestoreDependencies:
|
|
"""cm-cli restore-dependencies --uv-compile"""
|
|
|
|
def test_restore_dependencies_with_uv_compile(self):
|
|
"""restore-dependencies --uv-compile runs resolver after restore."""
|
|
_run_cm_cli("install", REPO_TEST1)
|
|
assert _pack_exists(PACK_TEST1)
|
|
|
|
r = _run_cm_cli("restore-dependencies", "--uv-compile")
|
|
combined = r.stdout + r.stderr
|
|
|
|
assert "Resolving dependencies" in combined
|
|
|
|
|
|
class TestConflictAttributionDetail:
|
|
"""Verify conflict attribution output details."""
|
|
|
|
def test_both_packs_and_specs_shown(self):
|
|
"""Conflict output shows pack names AND version specs."""
|
|
_run_cm_cli("install", REPO_TEST1)
|
|
_run_cm_cli("install", REPO_TEST2)
|
|
|
|
r = _run_cm_cli("uv-compile")
|
|
combined = r.stdout + r.stderr
|
|
|
|
# Processed attribution must show exact version specs (not raw uv error)
|
|
assert "Conflicting packages (by node pack):" in combined
|
|
assert "ansible==9.13.0" in combined
|
|
assert "ansible-core==2.14.0" in combined
|
|
# Both pack names present in attribution block
|
|
assert PACK_TEST1 in combined
|
|
assert PACK_TEST2 in combined
|