mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-07-26 10:07:38 +08:00
feat(cli): expand --uv-compile to all node management commands with conflict attribution (#2682)
* 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
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
"""E2E tests for ComfyUI Manager HTTP API endpoints (install/uninstall).
|
||||
|
||||
Starts a real ComfyUI instance, exercises the task-queue-based install
|
||||
and uninstall endpoints, then verifies the results via the installed-list
|
||||
endpoint and filesystem checks.
|
||||
|
||||
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.
|
||||
|
||||
Install test methodology follows the main comfyui-manager test suite
|
||||
(tests/glob/test_queue_task_api.py):
|
||||
- Uses a CNR-registered package with proper version-based install
|
||||
- Verifies .tracking file for CNR installs
|
||||
- Checks installed-list API with cnr_id matching
|
||||
- Cleans up .disabled/ directory entries
|
||||
|
||||
Usage:
|
||||
E2E_ROOT=/tmp/e2e_full_test pytest tests/e2e/test_e2e_endpoint.py -v
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
E2E_ROOT = os.environ.get("E2E_ROOT", "")
|
||||
COMFYUI_PATH = os.path.join(E2E_ROOT, "comfyui") if E2E_ROOT else ""
|
||||
CUSTOM_NODES = os.path.join(COMFYUI_PATH, "custom_nodes") if COMFYUI_PATH else ""
|
||||
SCRIPTS_DIR = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "scripts"
|
||||
)
|
||||
|
||||
PORT = 8199
|
||||
BASE_URL = f"http://127.0.0.1:{PORT}"
|
||||
|
||||
# CNR-registered package with multiple versions, no heavy dependencies.
|
||||
# Same test package used by the main comfyui-manager test suite.
|
||||
PACK_ID = "ComfyUI_SigmoidOffsetScheduler"
|
||||
PACK_DIR_NAME = "ComfyUI_SigmoidOffsetScheduler"
|
||||
PACK_CNR_ID = "comfyui_sigmoidoffsetscheduler"
|
||||
PACK_VERSION = "1.0.1"
|
||||
|
||||
# Polling configuration for async task completion
|
||||
POLL_TIMEOUT = 30 # max seconds to wait for an operation
|
||||
POLL_INTERVAL = 0.5 # seconds between polls
|
||||
|
||||
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 _start_comfyui() -> int:
|
||||
"""Start ComfyUI and return its PID."""
|
||||
env = {**os.environ, "E2E_ROOT": E2E_ROOT, "PORT": str(PORT)}
|
||||
r = subprocess.run(
|
||||
["bash", os.path.join(SCRIPTS_DIR, "start_comfyui.sh")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=180,
|
||||
env=env,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"Failed to start ComfyUI:\n{r.stderr}")
|
||||
for part in r.stdout.strip().split():
|
||||
if part.startswith("COMFYUI_PID="):
|
||||
return int(part.split("=")[1])
|
||||
raise RuntimeError(f"Could not parse PID from start_comfyui output:\n{r.stdout}")
|
||||
|
||||
|
||||
def _stop_comfyui():
|
||||
"""Stop ComfyUI."""
|
||||
env = {**os.environ, "E2E_ROOT": E2E_ROOT, "PORT": str(PORT)}
|
||||
subprocess.run(
|
||||
["bash", os.path.join(SCRIPTS_DIR, "stop_comfyui.sh")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
def _queue_task(task: dict) -> None:
|
||||
"""Queue a task and start the worker."""
|
||||
resp = requests.post(
|
||||
f"{BASE_URL}/v2/manager/queue/task",
|
||||
json=task,
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
requests.get(f"{BASE_URL}/v2/manager/queue/start", timeout=10)
|
||||
|
||||
|
||||
def _remove_pack(name: str) -> None:
|
||||
"""Remove a node pack directory and any .disabled/ entries."""
|
||||
# Active directory
|
||||
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)
|
||||
# .disabled/ entries (CNR versioned + nightly)
|
||||
disabled_dir = os.path.join(CUSTOM_NODES, ".disabled")
|
||||
if os.path.isdir(disabled_dir):
|
||||
cnr_lower = name.lower().replace("_", "").replace("-", "")
|
||||
for entry in os.listdir(disabled_dir):
|
||||
entry_lower = entry.lower().replace("_", "").replace("-", "")
|
||||
if entry_lower.startswith(cnr_lower):
|
||||
entry_path = os.path.join(disabled_dir, entry)
|
||||
if os.path.isdir(entry_path):
|
||||
shutil.rmtree(entry_path, ignore_errors=True)
|
||||
|
||||
|
||||
def _pack_exists(name: str) -> bool:
|
||||
return os.path.isdir(os.path.join(CUSTOM_NODES, name))
|
||||
|
||||
|
||||
def _has_tracking(name: str) -> bool:
|
||||
"""Check if the pack has a .tracking file (CNR install marker)."""
|
||||
return os.path.isfile(os.path.join(CUSTOM_NODES, name, ".tracking"))
|
||||
|
||||
|
||||
def _wait_for(predicate, timeout=POLL_TIMEOUT, interval=POLL_INTERVAL):
|
||||
"""Poll *predicate* until it returns True or *timeout* seconds elapse.
|
||||
|
||||
Returns True if predicate was satisfied, False on timeout.
|
||||
"""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if predicate():
|
||||
return True
|
||||
time.sleep(interval)
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def comfyui():
|
||||
"""Start ComfyUI once for the module, stop after all tests."""
|
||||
_remove_pack(PACK_DIR_NAME)
|
||||
pid = _start_comfyui()
|
||||
yield pid
|
||||
_stop_comfyui()
|
||||
_remove_pack(PACK_DIR_NAME)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestEndpointInstallUninstall:
|
||||
"""Install and uninstall via HTTP endpoints on a running ComfyUI.
|
||||
|
||||
Follows the same methodology as tests/glob/test_queue_task_api.py in
|
||||
the main comfyui-manager repo: CNR version-based install, .tracking
|
||||
verification, installed-list API check.
|
||||
"""
|
||||
|
||||
def test_install_via_endpoint(self, comfyui):
|
||||
"""POST /v2/manager/queue/task (install) -> pack appears on disk with .tracking."""
|
||||
_remove_pack(PACK_DIR_NAME)
|
||||
|
||||
_queue_task({
|
||||
"ui_id": "e2e-install",
|
||||
"client_id": "e2e-install",
|
||||
"kind": "install",
|
||||
"params": {
|
||||
"id": PACK_ID,
|
||||
"version": PACK_VERSION,
|
||||
"selected_version": "latest",
|
||||
"mode": "remote",
|
||||
"channel": "default",
|
||||
},
|
||||
})
|
||||
assert _wait_for(
|
||||
lambda: _pack_exists(PACK_DIR_NAME),
|
||||
), f"{PACK_DIR_NAME} not found in custom_nodes within {POLL_TIMEOUT}s"
|
||||
assert _has_tracking(PACK_DIR_NAME), f"{PACK_DIR_NAME} missing .tracking (not a CNR install?)"
|
||||
|
||||
def test_installed_list_shows_pack(self, comfyui):
|
||||
"""GET /v2/customnode/installed includes the installed pack."""
|
||||
if not _pack_exists(PACK_DIR_NAME):
|
||||
pytest.skip("Pack not installed (previous test may have failed)")
|
||||
|
||||
resp = requests.get(f"{BASE_URL}/v2/customnode/installed", timeout=10)
|
||||
resp.raise_for_status()
|
||||
installed = resp.json()
|
||||
|
||||
# Match by cnr_id (case-insensitive) following main repo pattern
|
||||
package_found = any(
|
||||
pkg.get("cnr_id", "").lower() == PACK_CNR_ID.lower()
|
||||
for pkg in installed.values()
|
||||
if isinstance(pkg, dict) and pkg.get("cnr_id")
|
||||
)
|
||||
assert package_found, (
|
||||
f"{PACK_CNR_ID} not found in installed list: {list(installed.keys())}"
|
||||
)
|
||||
|
||||
def test_uninstall_via_endpoint(self, comfyui):
|
||||
"""POST /v2/manager/queue/task (uninstall) -> pack removed from disk."""
|
||||
if not _pack_exists(PACK_DIR_NAME):
|
||||
pytest.skip("Pack not installed (previous test may have failed)")
|
||||
|
||||
_queue_task({
|
||||
"ui_id": "e2e-uninstall",
|
||||
"client_id": "e2e-uninstall",
|
||||
"kind": "uninstall",
|
||||
"params": {
|
||||
"node_name": PACK_CNR_ID,
|
||||
},
|
||||
})
|
||||
assert _wait_for(
|
||||
lambda: not _pack_exists(PACK_DIR_NAME),
|
||||
), f"{PACK_DIR_NAME} still exists after uninstall ({POLL_TIMEOUT}s timeout)"
|
||||
|
||||
def test_installed_list_after_uninstall(self, comfyui):
|
||||
"""After uninstall, pack no longer appears in installed list."""
|
||||
if _pack_exists(PACK_DIR_NAME):
|
||||
pytest.skip("Pack still exists (previous test may have failed)")
|
||||
|
||||
resp = requests.get(f"{BASE_URL}/v2/customnode/installed", timeout=10)
|
||||
resp.raise_for_status()
|
||||
installed = resp.json()
|
||||
|
||||
package_found = any(
|
||||
pkg.get("cnr_id", "").lower() == PACK_CNR_ID.lower()
|
||||
for pkg in installed.values()
|
||||
if isinstance(pkg, dict) and pkg.get("cnr_id")
|
||||
)
|
||||
assert not package_found, f"{PACK_CNR_ID} still in installed list after uninstall"
|
||||
|
||||
def test_install_uninstall_cycle(self, comfyui):
|
||||
"""Complete install/uninstall cycle in a single test."""
|
||||
_remove_pack(PACK_DIR_NAME)
|
||||
|
||||
# Install
|
||||
_queue_task({
|
||||
"ui_id": "e2e-cycle-install",
|
||||
"client_id": "e2e-cycle",
|
||||
"kind": "install",
|
||||
"params": {
|
||||
"id": PACK_ID,
|
||||
"version": PACK_VERSION,
|
||||
"selected_version": "latest",
|
||||
"mode": "remote",
|
||||
"channel": "default",
|
||||
},
|
||||
})
|
||||
assert _wait_for(
|
||||
lambda: _pack_exists(PACK_DIR_NAME),
|
||||
), f"Pack not installed within {POLL_TIMEOUT}s"
|
||||
assert _has_tracking(PACK_DIR_NAME), "Pack missing .tracking"
|
||||
|
||||
# Verify in installed list
|
||||
resp = requests.get(f"{BASE_URL}/v2/customnode/installed", timeout=10)
|
||||
resp.raise_for_status()
|
||||
installed = resp.json()
|
||||
package_found = any(
|
||||
pkg.get("cnr_id", "").lower() == PACK_CNR_ID.lower()
|
||||
for pkg in installed.values()
|
||||
if isinstance(pkg, dict) and pkg.get("cnr_id")
|
||||
)
|
||||
assert package_found, f"{PACK_CNR_ID} not in installed list"
|
||||
|
||||
# Uninstall
|
||||
_queue_task({
|
||||
"ui_id": "e2e-cycle-uninstall",
|
||||
"client_id": "e2e-cycle",
|
||||
"kind": "uninstall",
|
||||
"params": {
|
||||
"node_name": PACK_CNR_ID,
|
||||
},
|
||||
})
|
||||
assert _wait_for(
|
||||
lambda: not _pack_exists(PACK_DIR_NAME),
|
||||
), f"Pack not uninstalled within {POLL_TIMEOUT}s"
|
||||
|
||||
|
||||
class TestEndpointStartup:
|
||||
"""Verify ComfyUI startup with unified resolver."""
|
||||
|
||||
def test_comfyui_started(self, comfyui):
|
||||
"""ComfyUI is running and responds to health check."""
|
||||
resp = requests.get(f"{BASE_URL}/system_stats", timeout=10)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_startup_resolver_ran(self, comfyui):
|
||||
"""Startup log contains unified resolver output."""
|
||||
log_path = os.path.join(E2E_ROOT, "logs", "comfyui.log")
|
||||
with open(log_path) as f:
|
||||
log = f.read()
|
||||
assert "[UnifiedDepResolver]" in log
|
||||
assert "startup batch resolution succeeded" in log
|
||||
@@ -0,0 +1,254 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user