ComfyUI-Manager/tests/_install_flags_testutil.py
Dr.Lt.Data fca7ef149d
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
feat(security): dedicated install flags decouple git_url/pip install from security_level (#2962)
* feat(security): dedicated install flags decouple git_url/pip from security_level

Install via git URL and pip install are no longer gated by
security_level. Each surface gets a dedicated config.ini flag —
allow_git_url_install / allow_pip_install (both default false, secure
by default) — that fully REPLACES the security-level term for these two
features. The network-position invariant is retained: a non-local
listener stays denied regardless of the flags unless
network_mode = personal_cloud.

- New pure predicate is_dedicated_install_allowed() in
  common/manager_security (no config access; callers resolve config)
- Legacy endpoints /v2/customnode/install/git_url and .../pip switch
  from is_allowed_security_level('high+') to the flag gate; batch
  installs of unknown git URLs likewise (middle+ entry gate unchanged,
  unknown-pip 'block' stays unconditional; response shapes preserved)
- Config readers/writers (glob + legacy) parse and persist the flags;
  denial logs and frontend 403 messages name the responsible flag and
  note the non-local-listener requirement (network_mode=personal_cloud)
- No auto-seed from security_level — users previously on weak/normal-
  must opt in explicitly (see CHANGELOG migration notes; README
  documents the new contract)
- Update the pre-existing permissive E2E harness
  (start_comfyui_permissive.sh + test_e2e_legacy_real_ops.py) to the
  new contract: it now also sets allow_git_url_install /
  allow_pip_install = true, since security_level = normal- alone no
  longer opens the git_url/pip endpoints

Tests: predicate truth table proving security_level independence in
both directions, dual-reader config contract, security-level-matrix
freeze guards, legacy gate regression guards (121 unit), plus 22
real-server E2E tests incl. URL-form pip install with self-clean.

* test(e2e): fix fresh-env failures in customnode_info and git_clone harnesses

Two pre-existing harness defects that fail deterministically on a fresh
E2E environment (unrelated to the dedicated-install-flags change):

- test_e2e_customnode_info: TestInstalledPacks asserted the seed pack
  ComfyUI_SigmoidOffsetScheduler is installed, but nothing seeded it —
  the installing module (test_e2e_endpoint) runs alphabetically later
  and uninstalls it at the end. Add a module-scoped autouse fixture
  that installs the pack via cm-cli BEFORE the server starts (the
  imported-mode test asserts against the startup-frozen snapshot, so
  API-based seeding after boot cannot work) and removes it on teardown
  only if the fixture installed it.
- test_e2e_git_clone: _ensure_cache ran cm-cli update-cache with a
  120s timeout; the full DB download routinely exceeds that on slow
  links, erroring the whole module at setup. Raise to 600s.

Verified from a fresh state (seed pack absent): both modules pass
(13 tests, incl. previously-failing TestInstalledPacks 2 and
TestNightlyInstallCycle 3).
2026-06-11 01:44:12 +09:00

178 lines
7.6 KiB
Python

"""Shared test-support for the goal265 dedicated-install-flag test modules.
NOT a test module (no ``test_`` prefix — pytest does not collect it).
Provides minimal runtime stubs so ``comfyui_manager`` package modules can be
imported in a plain dev venv (outside a real ComfyUI runtime):
- ``comfy.cli_args.args`` — consumed by ``comfyui_manager/__init__.py`` at
package import time (``from comfy.cli_args import args``). The stub pins
``listen='127.0.0.1'`` (loopback) which matches the default precondition of
every scenario row (goal265-scenarios.md §0 preamble).
- ``folder_paths`` — consumed by ``comfyui_manager/common/context.py`` at
import time to derive the manager user directory. The stub redirects it to
a throwaway temp dir so importing the package NEVER creates directories
inside the repository checkout.
SUITE-ORDER INDEPENDENCE (goal265 FU, task #293): every public ``import_*``
entry point is self-sufficient — it repairs whatever ``sys.modules`` state
earlier test modules left behind, instead of assuming a pristine interpreter:
- Other test modules legitimately install FAKE ``comfyui_manager`` lineage
entries at module level (e.g. tests/test_unified_dep_resolver.py:63-73
registers a plain ``types.ModuleType("comfyui_manager")`` — NOT a package,
no ``__path__`` — to host a file-loaded module under a dotted name). Those
modules are imported at pytest COLLECTION time, so under full-suite
ordering the fake is already in ``sys.modules`` before any fixture here
runs, and ``import comfyui_manager.glob.manager_core`` dies with
``ModuleNotFoundError: 'comfyui_manager' is not a package``.
- ``_purge_fake_comfyui_manager()`` therefore validates the cached lineage
against the REAL package directory on disk and drops fake/broken entries
before importing. Dropping ``sys.modules`` entries is safe for the other
test modules: they hold direct object references to what they loaded;
they do not re-resolve through ``sys.modules``.
- ``ensure_comfy_stubs()`` likewise repairs half-formed ``comfy`` /
``folder_paths`` entries (present but missing the attributes we need)
rather than only handling the absent case.
Used by:
- tests/test_install_flags_config.py (dual-reader config tests)
- tests/test_install_flags_guards.py (out-of-scope guard tests)
Spec SoT: ~/.claude/pair-cowork/scratch/gm-team/goal265-spec.md §4
(test surface contract — binding for Step-4 test authoring).
"""
from __future__ import annotations
import importlib
import os
import sys
import tempfile
import types
#: repo root (parent of tests/)
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
#: the real on-disk package directory the cached lineage must resolve to
_REAL_PKG_DIR = os.path.join(REPO_ROOT, "comfyui_manager")
def ensure_comfy_stubs() -> None:
"""Install or REPAIR minimal ``comfy``/``folder_paths`` stubs (idempotent).
Handles three prior states per module:
- absent -> install a fresh stub
- present but inadequate -> patch the missing attributes in place
(another test's stub, or a partially-initialized real module)
- present and adequate -> leave untouched (e.g. real ComfyUI runtime)
"""
# --- comfy.cli_args.args ------------------------------------------------
comfy = sys.modules.get("comfy")
if comfy is None:
comfy = types.ModuleType("comfy")
sys.modules["comfy"] = comfy
cli_args = sys.modules.get("comfy.cli_args")
if cli_args is None:
cli_args = getattr(comfy, "cli_args", None)
if not isinstance(cli_args, types.ModuleType):
cli_args = types.ModuleType("comfy.cli_args")
sys.modules["comfy.cli_args"] = cli_args
args = getattr(cli_args, "args", None)
if args is None or not hasattr(args, "listen"):
setattr(cli_args, "args", types.SimpleNamespace(
listen="127.0.0.1",
enable_manager=True,
enable_manager_legacy_ui=False,
))
# re-link parent attr idempotently (a bare ``comfy`` stub from another
# test may lack the submodule attribute even when both entries exist)
setattr(comfy, "cli_args", cli_args)
# --- folder_paths -------------------------------------------------------
fp = sys.modules.get("folder_paths")
if fp is None:
fp = types.ModuleType("folder_paths")
sys.modules["folder_paths"] = fp
if not hasattr(fp, "get_system_user_directory"):
base = tempfile.mkdtemp(prefix="cm-flags-test-user-")
setattr(fp, "get_system_user_directory",
lambda name: os.path.join(base, name))
def _module_is_real(mod: types.ModuleType) -> bool:
"""True when a cached ``comfyui_manager*`` module resolves to the real
on-disk package tree (by ``__path__`` for packages, ``__file__`` for
leaf modules)."""
real_root = os.path.abspath(_REAL_PKG_DIR)
paths = getattr(mod, "__path__", None)
if paths is not None:
return any(os.path.abspath(p).startswith(real_root) for p in paths)
file = getattr(mod, "__file__", None)
if file is not None:
return os.path.abspath(file).startswith(real_root)
# neither __path__ nor __file__: a bare ModuleType fake
return False
def _purge_fake_comfyui_manager() -> None:
"""Drop fake/broken ``comfyui_manager`` lineage entries from
``sys.modules`` so a subsequent real-package import succeeds.
Uniform rule — an entry is dropped iff it does NOT resolve to the real
on-disk package tree:
- A fake top-level entry (bare ``ModuleType`` without ``__path__`` —
'not a package') is dropped so the real package can import.
- Fake SUBentries injected under dotted names (e.g.
``comfyui_manager.common.manager_util`` replaced by a stub) are
dropped; the import machinery re-imports the real module on next
access.
- REAL-file modules registered under dotted names by other test
modules (e.g. ``comfyui_manager.common.unified_dep_resolver`` loaded
via spec_from_file_location by tests/test_unified_dep_resolver.py)
are KEPT: those tests later resolve the SAME cached entry through
``mock.patch("comfyui_manager.common.unified_dep_resolver.X")``;
evicting it would make mock.patch import a fresh second instance and
silently patch the wrong module object.
"""
lineage = [n for n in list(sys.modules)
if n == "comfyui_manager" or n.startswith("comfyui_manager.")]
for name in lineage:
mod = sys.modules.get(name)
if mod is None or not _module_is_real(mod):
del sys.modules[name]
def _import_real(dotted: str):
"""ensure stubs -> purge fakes -> import; shared by all entry points."""
ensure_comfy_stubs()
_purge_fake_comfyui_manager()
return importlib.import_module(dotted)
def import_reader(reader: str):
"""Import and return ``comfyui_manager.<reader>.manager_core``.
``reader`` is ``"glob"`` or ``"legacy"`` — the two independent config
reader implementations sharing one config.ini (dual-reader rule,
goal265-mm.md §1.1 / goal265-spec.md §3).
"""
assert reader in ("glob", "legacy"), reader
return _import_real(f"comfyui_manager.{reader}.manager_core")
def import_context():
"""Import and return ``comfyui_manager.common.context`` (holds
``manager_config_path``, consumed by both readers at call time)."""
return _import_real("comfyui_manager.common.context")
def import_glob_security_utils():
"""Import and return ``comfyui_manager.glob.utils.security_utils``
(the glob copy of the security_level gate matrix — guard rows
SC-25C / SC-28)."""
return _import_real("comfyui_manager.glob.utils.security_utils")