Files
wildminder-ComfyUI-KEEP/tests/test_dependency_decisions.py
T
WildAi 01a4924489 plan(P5): address review findings
The five review findings of 2026-10-23 for phase P5:

1. The P5.5 fallback branch (a checkpoint without flownet.* keys) died with
   a bare RuntimeError at the strict load: the arch always owns the flownet
   (keep_arch.py:882), so the state dict of the net always carries the
   flownet.* keys - and the GMFlow weights the arch had just loaded were
   discarded.  The flownet tensors of the built net (the trained ones - the
   arch was built with flownet_path) are now merged into the checkpoint
   state dict before load_state_dict(..., strict=True).  The load stays
   strict (an unexpected key still raises) and the trained flow weights
   survive.  The stub arches of the tests behave like nn.Module now - a
   no-op recorder had hidden the defect.

2. P5.4's acceptance is no longer unmet: the GPU acceptance run was
   executed (real weights, real ComfyUI modules, RTX 4070 Ti SUPER) and is
   green (5 passed, exit code 0; the run log is in the audit doc section 5).
   KEEP_MODEL_DIR now really decides where the weights come from - it is the
   models root of the run (folder_paths.models_dir), the harness fixture
   no longer isolates the gpu_acceptance tier, and the run needs
   KEEP_NO_COMFY_MOCKS=1 so the real ComfyUI modules are in charge.

3. The "the flow weights are not random" assertion of the acceptance test
   could not distinguish random weights from trained ones.  It now compares
   the flownet tensors with the tensors of their source (the KEEP checkpoint
   or the GMFlow file) bit for bit.

4. The superseded download helper load_file_from_url_comfy is gone
   (src/keep_paths.py only resolves paths; the download lives in
   src/keep_download.py since P5.2).  The stale docstrings, the stale
   comment in the vendored facelib and the two tests that called the dead
   helper are fixed.

5. The risk list mentioned a pending deletion of assets/keep-preview.png
   that does not exist - the asset is committed and present (93279 bytes).
   The rule in the implementation workflow now states the truth.

Checks: the gate (pytest -q -m "unit or mock_integration") is green (234
passed, 1 skipped, 1 xfailed); the opt-in acceptance run is green (5 passed);
the acceptance tier still skips cleanly without its environment variables.
2026-10-24 00:01:48 +03:00

218 lines
7.7 KiB
Python

"""P0.4 - dependency decision tests.
Two guards, both named by the plan:
* a **grep guard**: the package must contain exactly one diffusers import, and
it must import exactly ``FeedForward`` and ``AdaLayerNorm`` (the two symbols
that P2.2 vendors from diffusers 0.11.x);
* an **xfail guard** "flownet path is wired": the architecture config that is
handed to the KEEP arch must resolve a non-``None`` ``flownet_path``.
It is marked ``xfail(strict=False)`` because it is expected to fail until
**P5.5** wires the GMFlow resolution (the flip owner recorded in the
reason string and in the audit document).
"""
from __future__ import annotations
import ast
import logging
import sys
import types
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
PACKAGE_DIR = REPO_ROOT / "modules"
SRC_DIR = REPO_ROOT / "src"
NODES_DIR = REPO_ROOT / "nodes"
UTILS_FILE = SRC_DIR / "keep_model_configs.py"
AUDIT_DOC = REPO_ROOT / "docs" / "audit" / "2026-10-22-dependency-decisions.md"
EXPECTED_DIFFUSERS_SYMBOLS = {"FeedForward", "AdaLayerNorm"}
def _package_files() -> list[Path]:
"""Every production python file of the pack (P3.2 layout)."""
files: list[Path] = []
for directory in (PACKAGE_DIR, SRC_DIR, NODES_DIR):
files.extend(sorted(directory.rglob("*.py")))
return files
def _diffusers_imports() -> list[tuple[str, int, set[str]]]:
"""Every diffusers import in the package: (file, line, imported names)."""
found = []
for path in _package_files():
try:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
except (SyntaxError, UnicodeDecodeError):
continue
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
if node.module and node.module.split(".")[0] == "diffusers":
names = {alias.name for alias in node.names}
found.append((path.relative_to(REPO_ROOT).as_posix(), node.lineno, names))
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name.split(".")[0] == "diffusers":
found.append(
(path.relative_to(REPO_ROOT).as_posix(), node.lineno, {alias.name})
)
return found
@pytest.fixture(scope="module")
def utils_module():
"""The model configs of the package, imported the way the package does.
Since P3.2 the configs live in ``src/keep_model_configs.py``; the module
is imported through the package alias, so the *real* module is observed
(the ComfyUI seams - ``folder_paths`` and ``comfy.utils`` - are the ones
of the root conftest's mocks).
"""
import importlib
return importlib.import_module("ComfyUI_KEEP.src.keep_model_configs")
@pytest.mark.unit
def test_no_diffusers_import_remains_in_the_vendored_tree():
"""P2.2 flipped this guard: **no** diffusers import may remain.
The two symbols are vendored byte-for-byte in
``modules/deps/basicsr/archs/_diffusers_compat.py``; the vendored tree
must not depend on diffusers any more.
"""
imports = _diffusers_imports()
assert imports == [], f"no diffusers import may remain, got {imports}"
@pytest.mark.unit
def test_the_vendored_compat_module_provides_the_expected_symbols():
"""The vendored module must define exactly the two planned symbols."""
path = (
REPO_ROOT
/ "modules"
/ "deps"
/ "basicsr"
/ "archs"
/ "_diffusers_compat.py"
)
assert path.is_file(), f"the vendored compat module is missing: {path}"
source = path.read_text(encoding="utf-8")
for symbol in sorted(EXPECTED_DIFFUSERS_SYMBOLS):
assert f"class {symbol}(nn.Module):" in source, symbol
# the attribution header must name the upstream project and version
assert "diffusers 0.11.0" in source, (
"the attribution header must name diffusers 0.11.0"
)
assert "Apache License" in source, (
"the attribution header must name the Apache-2.0 license"
)
# and the architecture must use the vendored copy
keep_arch = (
REPO_ROOT / "modules" / "deps" / "basicsr" / "archs" / "keep_arch.py"
).read_text(encoding="utf-8")
assert "from ._diffusers_compat import FeedForward, AdaLayerNorm" in keep_arch, (
"keep_arch.py must import the vendored classes"
)
@pytest.mark.unit
def test_audit_doc_records_the_diffusers_decision():
text = AUDIT_DOC.read_text(encoding="utf-8")
assert "FeedForward" in text and "AdaLayerNorm" in text
assert "0.11" in text, "the diffusers version must be recorded"
@pytest.mark.unit
def test_flownet_path_is_wired(monkeypatch, tmp_path):
"""P5.5 flipped this guard: the flownet path is resolved before the arch.
The P0.4 guard asserted that the **config table** carries a non-``None``
``flownet_path``. That premise was disproved by P0.4 section 2: the
shipped KEEP checkpoint carries the trained flownet weights (123
``flownet.*`` tensors), so the GMFlow file is only needed when a
checkpoint does **not** provide them - the resolution is conditional.
The guard now asserts the guarantee that actually matters: a checkpoint
**without** flownet weights gets a **resolved** (non-``None``)
``flownet_path`` in the config that is handed to the arch - the flow net
must never keep its random weights.
"""
import importlib
import torch
loader_module = importlib.import_module("ComfyUI_KEEP.src.keep_model_loader")
built: list[dict] = []
class _StubArch:
def __init__(self, **kwargs):
built.append(kwargs)
def to(self, device):
return self
def state_dict(self):
# the real arch owns the flownet (keep_arch.py:882) - the stub
# has no flownet tensors, so nothing has to be merged into the
# checkpoint state dict before the strict load
return {}
def load_state_dict(self, state_dict, strict=True):
return self
def eval(self):
return self
class _StubHelper:
def __init__(self, **kwargs):
pass
gmflow = str(tmp_path / "gmflow_sintel-0c07dcb3.pth")
monkeypatch.setattr(loader_module, "ARCH_REGISTRY", {"KEEP": _StubArch})
monkeypatch.setattr(
loader_module.torch, "load", lambda *a, **k: {"params_ema": {"w": torch.zeros(1)}}
)
monkeypatch.setattr(loader_module, "_face_restore_helper", lambda: _StubHelper)
monkeypatch.setattr(
loader_module.keep_paths, "resolve_model_root", lambda *a, **k: str(tmp_path)
)
monkeypatch.setattr(
loader_module.keep_download,
"ensure_weight",
lambda entry, progress=True: str(tmp_path / "checkpoint.pth"),
)
monkeypatch.setattr(
loader_module.keep_download,
"ensure_required",
lambda entry, progress=True: str(tmp_path / "weight.pth"),
)
monkeypatch.setattr(
loader_module.keep_download,
"ensure_optional",
lambda entry, progress=True: gmflow,
)
loader_module.KEEPModelLoader().load_keep_model_pack(
"KEEP", "retinaface_resnet50"
)
assert built, "the architecture was never built"
assert built[0]["flownet_path"] is not None, (
"flownet_path is None - the GMFlow net would keep its random weights"
)
assert built[0]["flownet_path"] == gmflow
@pytest.mark.unit
def test_flownet_is_built_by_the_arch(utils_module):
"""The vendored arch builds the flownet from the config (code read check)."""
arch = REPO_ROOT / "modules" / "deps" / "basicsr" / "archs" / "keep_arch.py"
source = arch.read_text(encoding="utf-8")
assert "self.flownet = FlowGenerator(path=flownet_path)" in source
assert "flows = self.get_flow(x)" in source