| """Guards on which modules ``dev-sync`` ships, so the loader can never point |
| at a file the install lacks. |
| |
| The #183 incident (root cause behind #198): a module that ``__init__.py`` |
| imported but ``scripts/sync_to_dev.py``'s ``RUNTIME_PATHS`` never copied raised |
| ``ImportError`` at startup. After the #198 refactor the blast radius of such a |
| gap depends on *where* the import sits — a per-group node import degrades to |
| "that node absent", but the context probe (``koolook_versioning``) and the |
| install guard gate *all* registration. So the real invariant is: every module |
| ``__init__.py`` imports at load must be shipped by ``RUNTIME_PATHS``. This test |
| derives those imports from ``__init__.py`` itself (rather than a ``k_*.py`` |
| glob, which misses the non-``k_`` gates) so it can't drift. |
| """ |
| from __future__ import annotations |
|
|
| import ast |
| import importlib.util |
| from pathlib import Path |
|
|
| REPO_ROOT = Path(__file__).resolve().parents[2] |
| _SCRIPT = REPO_ROOT / "scripts" / "sync_to_dev.py" |
| _spec = importlib.util.spec_from_file_location("koolook_sync_to_dev_under_test", _SCRIPT) |
| assert _spec is not None and _spec.loader is not None |
| sync_to_dev = importlib.util.module_from_spec(_spec) |
| _spec.loader.exec_module(sync_to_dev) |
|
|
|
|
| def _init_load_time_module_deps() -> set[str]: |
| """Top-level package modules/packages ``__init__.py`` imports at load. |
| |
| Covers relative ``from .X import ...`` / ``from . import X`` statements and |
| the ``_merge_node_group(label, ".X")`` node-group imports. Returns the |
| first path segment of each (e.g. ``forks.radiance_koolook`` -> ``forks``), |
| the granularity ``RUNTIME_PATHS`` ships at. Absolute imports (the install |
| guard's out-of-package fallback) are level 0 and intentionally ignored. |
| """ |
| tree = ast.parse((REPO_ROOT / "__init__.py").read_text(encoding="utf-8")) |
| deps: set[str] = set() |
| for node in ast.walk(tree): |
| if isinstance(node, ast.ImportFrom) and (node.level or 0) >= 1: |
| if node.module: |
| deps.add(node.module.split(".")[0]) |
| else: |
| deps.update(alias.name.split(".")[0] for alias in node.names) |
| elif ( |
| isinstance(node, ast.Call) |
| and getattr(node.func, "id", None) == "_merge_node_group" |
| ): |
| for arg in node.args: |
| if ( |
| isinstance(arg, ast.Constant) |
| and isinstance(arg.value, str) |
| and arg.value.startswith(".") |
| ): |
| deps.add(arg.value.lstrip(".").split(".")[0]) |
| return deps |
|
|
|
|
| def _route_load_time_module_deps() -> set[str]: |
| tree = ast.parse((REPO_ROOT / "koolook_routes.py").read_text(encoding="utf-8")) |
| deps: set[str] = set() |
| for node in ast.walk(tree): |
| if isinstance(node, ast.ImportFrom): |
| if (node.level or 0) >= 1 and node.module: |
| deps.add(node.module.split(".")[0]) |
| elif node.module and node.module.startswith("koolook_"): |
| deps.add(node.module.split(".")[0]) |
| return deps |
|
|
|
|
| def test_init_load_deps_are_shipped_by_runtime_paths(): |
| runtime_paths = set(sync_to_dev.RUNTIME_PATHS) |
| deps = _init_load_time_module_deps() | _route_load_time_module_deps() |
| |
| |
| assert "koolook_versioning" in deps and "k_ai_pipeline" in deps, ( |
| f"__init__.py import parse looks broken — only found: {sorted(deps)}" |
| ) |
| |
| |
| missing = sorted( |
| dep |
| for dep in deps |
| if f"{dep}.py" not in runtime_paths and dep not in runtime_paths |
| ) |
| assert not missing, ( |
| "__init__.py imports these at load but dev-sync RUNTIME_PATHS does not " |
| f"ship them: {missing}. Add them to RUNTIME_PATHS in " |
| "scripts/sync_to_dev.py so a plain dev-sync can't 404 the loader." |
| ) |
|
|
|
|
| def test_dev_sync_ships_manager_visible_package_metadata(): |
| """The live dev install should report the same pack version as the source. |
| |
| ComfyUI Manager reads package metadata from ``pyproject.toml``. If dev-sync |
| skips it, the code can be current while the UI still reports an older |
| installed version. |
| """ |
| assert "pyproject.toml" in sync_to_dev.RUNTIME_PATHS |
|
|