Files
wildminder-ComfyUI-KEEP/tests/test_keep_model_loader.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

695 lines
24 KiB
Python

"""P4.5 - the model residency of the pack (ModelPatcher) and ``ensure_flow``.
The pack wraps its three model components (the KEEP net with its flownet, the
face detector, the face parser) in ``ModelPatcher`` instances and lets ComfyUI
arbitrate the VRAM. The tests run against the **mocked**
``comfy.model_management`` of the conftest: every residency call is recorded,
so the tests can assert what the pack asks ComfyUI to do - and that it asks
exactly once per execute.
"""
from __future__ import annotations
import hashlib
import importlib
import os
import pytest
import torch
@pytest.fixture(scope="module")
def loader_module():
return importlib.import_module("ComfyUI_KEEP.src.keep_model_loader")
@pytest.fixture(scope="module")
def node_module():
return importlib.import_module("ComfyUI_KEEP.nodes.keep_face_upscale_image")
@pytest.fixture(scope="module")
def configs_module():
return importlib.import_module("ComfyUI_KEEP.src.keep_model_configs")
@pytest.fixture
def model_management():
"""The mocked ``comfy.model_management`` (the calls are reset per test)."""
module = importlib.import_module("comfy.model_management")
module.calls.clear()
return module
class _StubModule:
"""A minimal stand-in for a torch module (it only has to move)."""
def __init__(self, name: str = "module") -> None:
self.name = name
self.moved_to: list = []
def to(self, device):
self.moved_to.append(device)
return self
class _StubKeepNet(_StubModule):
"""The KEEP net - it owns the flownet as a submodule."""
def __init__(self) -> None:
super().__init__("keep_net")
self.flownet = _StubModule("flownet")
class _StubFaceHelper:
"""The facelib helper - it owns the detector and the parser."""
def __init__(self) -> None:
self.face_detector = _StubModule("detector")
self.face_parse = _StubModule("parser")
self.device = None
@pytest.fixture
def stub_models():
return _StubKeepNet(), _StubFaceHelper()
@pytest.fixture
def pack(loader_module, stub_models):
"""A real ``KEEPModelPack`` around the stub models."""
keep_net, face_helper = stub_models
return loader_module.KEEPModelPack(
keep_net=keep_net,
face_helper=face_helper,
bg_upscale_model=None,
face_upscale_model=None,
model_type_str="KEEP",
)
# ---------------------------------------------------------------------------
# the patchers
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_the_pack_builds_three_patchers(pack):
"""Three patchers: the KEEP net, the face detector and the face parser."""
assert len(pack.patchers) == 3, pack.patchers
assert pack.keep_patcher.model is pack.keep_net
assert pack.detector_patcher.model is pack.face_helper.face_detector
assert pack.parser_patcher.model is pack.face_helper.face_parse
@pytest.mark.unit
def test_exactly_one_patcher_contains_the_flownet(pack):
"""The flownet belongs to the KEEP net - it must not be patched twice.
A second patcher for the flownet would count its VRAM twice (the plan
P4.5).
"""
assert pack.flownet is pack.keep_net.flownet
assert pack.keep_patcher.model.flownet is pack.flownet
# the flownet is not the model of a patcher of its own
assert all(patcher.model is not pack.flownet for patcher in pack.patchers)
assert pack.detector_patcher.model is not pack.flownet
assert pack.parser_patcher.model is not pack.flownet
@pytest.mark.unit
def test_the_patchers_are_shared_by_the_packs_of_the_same_models(
loader_module, stub_models
):
"""The patchers belong to the models - they are built once per build."""
keep_net, face_helper = stub_models
first = loader_module.KEEPModelPack(keep_net, face_helper, None, None, "KEEP")
second = loader_module.KEEPModelPack(
keep_net, face_helper, None, None, "KEEP", patchers=first.patchers_by_role
)
assert second.keep_patcher is first.keep_patcher
assert second.patchers == first.patchers
# ---------------------------------------------------------------------------
# the residency
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_load_device_lets_comfyui_arbitrate_the_vram(pack, model_management):
"""The pack hands all its patchers to ``load_models_gpu`` - once."""
pack.load_device()
load_calls = [
call for call in model_management.calls if call[0] == "load_models_gpu"
]
assert len(load_calls) == 1, model_management.calls
_, models, memory_required = load_calls[0]
assert list(models) == list(pack.patchers)
assert memory_required == 0
@pytest.mark.unit
def test_offload_moves_the_models_back_and_frees_the_vram(pack, model_management):
"""The release path: the models go back to the offload device and ComfyUI
frees the VRAM."""
pack.offload()
names = [call[0] for call in model_management.calls]
assert "unload_all_models" in names, model_management.calls
assert "soft_empty_cache" in names, model_management.calls
# the models were moved back to the offload device
assert pack.keep_net.moved_to[-1] == pack.offload_device
assert pack.face_helper.face_detector.moved_to[-1] == pack.offload_device
assert pack.face_helper.face_parse.moved_to[-1] == pack.offload_device
@pytest.mark.unit
def test_the_cpu_mode_makes_the_same_calls(
loader_module, model_management, monkeypatch
):
"""``get_torch_device()`` -> cpu: the same residency calls are made.
The conftest mock answers ``cpu`` by default (``conftest.py``: the
``get_torch_device`` / ``unet_offload_device`` of the mocked
``comfy.model_management``), so patching the device to ``cpu`` alone
would exercise the default path and prove nothing. The CPU leg is
therefore compared with a **GPU leg**: both devices must produce the
same residency calls - the pack must not branch on the device. A
device dependent path is exactly the B7 CPU crash (the pack called
``torch.cuda.empty_cache()`` and crashed on a CPU-only host).
"""
def residency_calls(device):
monkeypatch.setattr(model_management, "get_torch_device", lambda: device)
monkeypatch.setattr(model_management, "unet_offload_device", lambda: device)
model_management.calls.clear()
pack = loader_module.KEEPModelPack(
_StubKeepNet(), _StubFaceHelper(), None, None, "KEEP"
)
# the patched device really reaches the pack - the leg is not a no-op
assert pack.device == device
assert pack.offload_device == device
pack.load_device()
pack.offload()
return [call[0] for call in model_management.calls]
gpu_calls = residency_calls(torch.device("cuda"))
cpu_calls = residency_calls(torch.device("cpu"))
assert cpu_calls == gpu_calls, (cpu_calls, gpu_calls)
assert cpu_calls.count("load_models_gpu") == 1, cpu_calls
assert cpu_calls.count("unload_all_models") == 1, cpu_calls
assert cpu_calls.count("soft_empty_cache") == 1, cpu_calls
# ---------------------------------------------------------------------------
# the node around the pack (mock-integration)
# ---------------------------------------------------------------------------
def _patch_the_node(monkeypatch, node_module, processor_cls):
"""Stub the heavy parts of the single image node."""
monkeypatch.setattr(node_module, "KEEPFaceProcessor", processor_cls)
monkeypatch.setattr(node_module, "comfy_image_to_cv2", lambda image: image)
monkeypatch.setattr(node_module, "cv2_to_comfy_image", lambda image: image)
monkeypatch.setattr(node_module, "report_progress", lambda *args: True)
class _StubProcessor:
"""A processor that returns a stub image."""
def __init__(self, pack) -> None:
self.pack = pack
def process_image(self, **kwargs):
return _StubModule("processed")
class _FailingProcessor:
"""A processor that fails (the node must give the VRAM back)."""
def __init__(self, pack) -> None:
self.pack = pack
def process_image(self, **kwargs):
raise RuntimeError("the processing failed")
@pytest.mark.mock_integration
def test_one_execute_asks_for_the_vram_once_and_gives_it_back(
node_module, loader_module, model_management, monkeypatch
):
"""One execute -> one ``load_models_gpu`` with all the pack patchers and
the release afterwards."""
pack = loader_module.KEEPModelPack(
_StubKeepNet(), _StubFaceHelper(), None, None, "KEEP"
)
_patch_the_node(monkeypatch, node_module, _StubProcessor)
node_module.KEEP_FaceUpscaleImageNode.execute(
image=torch.zeros((1, 8, 8, 3)),
keep_model=pack,
final_upscale_factor=1.0,
has_aligned_face=False,
only_center_face=True,
draw_bounding_box=False,
)
names = [call[0] for call in model_management.calls]
assert names.count("load_models_gpu") == 1, model_management.calls
assert names.count("unload_all_models") == 1, model_management.calls
assert names.count("soft_empty_cache") == 1, model_management.calls
load_call = [c for c in model_management.calls if c[0] == "load_models_gpu"][0]
assert list(load_call[1]) == list(pack.patchers)
@pytest.mark.mock_integration
def test_the_node_gives_the_vram_back_when_the_processing_fails(
node_module, loader_module, model_management, monkeypatch
):
"""The release happens in the ``finally`` path - also on a failure."""
pack = loader_module.KEEPModelPack(
_StubKeepNet(), _StubFaceHelper(), None, None, "KEEP"
)
_patch_the_node(monkeypatch, node_module, _FailingProcessor)
with pytest.raises(RuntimeError):
node_module.KEEP_FaceUpscaleImageNode.execute(
image=torch.zeros((1, 8, 8, 3)),
keep_model=pack,
final_upscale_factor=1.0,
has_aligned_face=False,
only_center_face=True,
draw_bounding_box=False,
)
names = [call[0] for call in model_management.calls]
assert names.count("load_models_gpu") == 1, model_management.calls
assert names.count("unload_all_models") == 1, model_management.calls
assert names.count("soft_empty_cache") == 1, model_management.calls
# ---------------------------------------------------------------------------
# ensure_flow
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_ensure_flow_is_idempotent(loader_module, stub_models):
"""``ensure_flow`` resolves once per pack - the second call is a no-op.
A checkpoint that carries the flownet weights (the shipped one, P0.4
section 2) needs no resolution at all.
"""
net, helper = stub_models
pack = loader_module.KEEPModelPack(
net, helper, None, None, "KEEP", flownet_from_checkpoint=True
)
assert pack.ensure_flow() is True, "the flownet comes with the KEEP net"
# the flownet is gone -> the cached result is still returned
pack.flownet = None
assert pack.ensure_flow() is True
@pytest.mark.unit
def test_ensure_flow_reports_a_missing_flownet(loader_module):
"""A pack without a flownet reports that nothing is available (yet).
P5.5 resolves the GMFlow checkpoint behind this hook.
"""
net = _StubKeepNet()
net.flownet = None
pack = loader_module.KEEPModelPack(net, _StubFaceHelper(), None, None, "KEEP")
assert pack.ensure_flow() is False
# ---------------------------------------------------------------------------
# P5.3 - the fail-fast weight validation
# ---------------------------------------------------------------------------
PAYLOAD = b"not a real checkpoint"
PAYLOAD_SHA256 = hashlib.sha256(PAYLOAD).hexdigest()
@pytest.fixture
def synthetic_manifest(loader_module, configs_module, monkeypatch):
"""The manifest with the hash of the test payload.
The real hashes cannot be reproduced by a test, so the manifest the
loader and the downloader see is replaced by entries that carry the hash
of the payload the transport double writes. Everything else - the
resolution order, the fail-fast rule, the error messages - is the
production code.
"""
entries = tuple(
configs_module.WeightEntry(
name=entry.name,
url=entry.url,
sha256=PAYLOAD_SHA256,
dest_folder=entry.dest_folder,
subdir=entry.subdir,
required=entry.required,
available=entry.available,
unverified=entry.unverified,
sha256_prefix=entry.sha256_prefix,
)
for entry in configs_module.WEIGHT_MANIFEST
)
by_name = {entry.name: entry for entry in entries}
monkeypatch.setattr(loader_module, "WEIGHT_MANIFEST", entries)
monkeypatch.setattr(loader_module, "WEIGHT_MANIFEST_BY_NAME", by_name)
monkeypatch.setattr(
loader_module,
"weight_entry_for",
lambda model_type: by_name[
"KEEP_Asian-4765ebe0.pth" if model_type == "Asian" else "KEEP-b76feb75.pth"
],
)
monkeypatch.setattr(
loader_module.keep_download, "WEIGHT_MANIFEST", entries
)
return by_name
@pytest.fixture
def transport(monkeypatch):
"""The transport double: it records the URLs and writes the payload."""
download_module = importlib.import_module("ComfyUI_KEEP.src.keep_download")
calls: list[str] = []
def _download(url, dst, hash_prefix=None, progress=True):
calls.append(url)
os.makedirs(os.path.dirname(dst), exist_ok=True)
with open(dst, "wb") as handle:
handle.write(PAYLOAD)
monkeypatch.setattr(download_module, "download_url_to_file", _download)
return calls
@pytest.fixture
def loader(loader_module, monkeypatch, tmp_path, synthetic_manifest, transport):
"""A loader whose model build is stubbed out (the weights are real)."""
class _StubArch:
"""A stand-in for the vendored arch that behaves like ``nn.Module``.
The real arch **owns** the flownet as a submodule
(``keep_arch.py:882``), so the state dict of the net always carries
the ``flownet.*`` keys - and a checkpoint without them fails the
**strict** load. The stub keeps that behaviour (a no-op recorder
would hide exactly that defect).
"""
#: The state dict of the stub net (the flownet is part of it).
STATE = {
"weight": torch.zeros(1),
"flownet.model.conv1.weight": torch.ones(1),
}
def __init__(self, **kwargs):
self.kwargs = kwargs
# the real arch owns the flownet as a submodule (keep_arch.py:882)
self.flownet = _StubModule("flownet")
self.state = dict(self.STATE)
def to(self, device):
return self
def state_dict(self):
return dict(self.state)
def load_state_dict(self, state_dict, strict=True):
missing = [key for key in self.state if key not in state_dict]
unexpected = [key for key in state_dict if key not in self.state]
if strict and (missing or unexpected):
raise RuntimeError(
"Error(s) in loading state_dict for KEEP: missing "
f"{missing}, unexpected {unexpected}"
)
self.loaded = state_dict
return self
def eval(self):
return self
class _StubHelper:
def __init__(self, **kwargs):
self.kwargs = kwargs
monkeypatch.setattr(loader_module, "ARCH_REGISTRY", {"KEEP": _StubArch})
monkeypatch.setattr(
loader_module.torch,
"load",
lambda *args, **kwargs: {"params_ema": {"weight": torch.zeros(1)}},
)
monkeypatch.setattr(loader_module, "_face_restore_helper", lambda: _StubHelper)
monkeypatch.setattr(
loader_module.keep_paths, "resolve_model_root", lambda *args, **kwargs: str(tmp_path)
)
return loader_module.KEEPModelLoader()
@pytest.mark.unit
def test_a_missing_required_weight_triggers_a_download(loader, transport):
"""The weights are resolved (and downloaded) before the net is built."""
pack = loader.load_keep_model_pack("KEEP", "retinaface_resnet50")
assert pack.keep_net is not None
urls = [url.rsplit("/", 1)[-1] for url in transport]
assert "KEEP-b76feb75.pth" in urls, urls
for name in (
"detection_Resnet50_Final.pth",
"detection_mobilenet0.25_Final.pth",
"yolov5n-face.pth",
"yolov5l-face.pth",
"parsing_parsenet.pth",
):
assert name in urls, (name, urls)
@pytest.mark.unit
def test_a_failed_download_raises_the_actionable_message(
loader_module, loader, monkeypatch, transport
):
"""A missing required weight names the file, the path and the URL."""
download_module = importlib.import_module("ComfyUI_KEEP.src.keep_download")
def _fail(url, dst, hash_prefix=None, progress=True):
raise RuntimeError("the server is not reachable")
monkeypatch.setattr(download_module, "download_url_to_file", _fail)
with pytest.raises(FileNotFoundError) as excinfo:
loader.load_keep_model_pack("KEEP", "retinaface_resnet50")
message = str(excinfo.value)
assert "KEEP-b76feb75.pth" in message
assert "https://github.com/jnjaby/KEEP/releases/download/v1.0.0/KEEP-b76feb75.pth" in message
assert "keep_models" in message and "KEEP" in message
@pytest.mark.unit
def test_an_unresolvable_optional_gmflow_weight_does_not_raise(
loader_module, loader, monkeypatch
):
"""The GMFlow checkpoint is optional - its absence never breaks the pack.
The flow weights are only *needed* when the checkpoint does not carry
them; the resolution is deferred to ``ensure_flow()`` (P5.5).
"""
monkeypatch.setattr(
loader_module.keep_download,
"ensure_optional",
lambda entry, progress=True: None,
)
pack = loader.load_keep_model_pack("KEEP", "retinaface_resnet50")
assert pack is not None
assert pack.keep_net.kwargs["flownet_path"] is None
@pytest.mark.unit
def test_the_unavailable_asian_entry_does_not_break_the_default_pack(loader, transport):
"""The Asian checkpoint is not required - the default pack still builds."""
pack = loader.load_keep_model_pack("KEEP", "retinaface_resnet50")
assert pack is not None
urls = [url.rsplit("/", 1)[-1] for url in transport]
assert "KEEP_Asian-4765ebe0.pth" not in urls, urls
@pytest.mark.unit
def test_selecting_the_asian_model_raises_an_actionable_error(loader):
"""The Asian checkpoint is not published upstream (HTTP 404)."""
with pytest.raises(FileNotFoundError) as excinfo:
loader.load_keep_model_pack("Asian", "retinaface_resnet50")
message = str(excinfo.value)
assert "KEEP_Asian-4765ebe0.pth" in message
assert "not published upstream" in message
assert "KEEP_Asian-4765ebe0.pth" in message
assert "keep_models" in message
# ---------------------------------------------------------------------------
# P5.5 - the flownet wiring
# ---------------------------------------------------------------------------
def _downloaded(transport) -> list[str]:
return [url.rsplit("/", 1)[-1] for url in transport]
@pytest.mark.unit
def test_the_arch_gets_a_resolved_flownet_path_without_flownet_weights(loader, transport):
"""A checkpoint **without** flownet weights -> the GMFlow file is resolved.
The resolved path is put into the architecture config **before** the net
is built, so the vendored arch loads the trained weights instead of
random ones.
"""
pack = loader.load_keep_model_pack("KEEP", "retinaface_resnet50")
flownet_path = pack.keep_net.kwargs["flownet_path"]
assert flownet_path is not None, "the GMFlow checkpoint was not resolved"
assert flownet_path.endswith("gmflow_sintel-0c07dcb3.pth"), flownet_path
assert "gmflow_sintel-0c07dcb3.pth" in _downloaded(transport)
@pytest.mark.unit
def test_a_checkpoint_without_flownet_weights_loads_strictly(loader):
"""The fallback branch must not break the strict load.
The real arch **always** owns the flownet (``keep_arch.py:882``), so the
state dict of the net always carries the ``flownet.*`` keys. A
checkpoint without them used to make the strict load raise
``RuntimeError: Missing key(s) in state_dict: flownet.*`` - and the
GMFlow weights the arch had just loaded were thrown away. The flownet
tensors of the built net fill the gap, so the load stays strict **and**
the trained flow weights survive.
"""
pack = loader.load_keep_model_pack("KEEP", "retinaface_resnet50")
# the strict load happened (the stub arch raises on a missing key)
assert pack.keep_net.loaded is not None
# the flownet of the built net (the trained GMFlow weights) is in the
# loaded state dict - it was not discarded
assert torch.equal(
pack.keep_net.loaded["flownet.model.conv1.weight"],
pack.keep_net.state_dict()["flownet.model.conv1.weight"],
)
@pytest.mark.unit
def test_the_strict_load_still_rejects_an_unexpected_key(loader_module, loader, monkeypatch):
"""The flownet merge does not weaken the strict load."""
monkeypatch.setattr(
loader_module.torch,
"load",
lambda *args, **kwargs: {
"params_ema": {"weight": torch.zeros(1), "unexpected.key": torch.zeros(1)}
},
)
with pytest.raises(RuntimeError):
loader.load_keep_model_pack("KEEP", "retinaface_resnet50")
@pytest.mark.unit
def test_a_checkpoint_with_flownet_weights_downloads_no_gmflow(
loader_module, loader, transport, monkeypatch
):
"""The shipped checkpoint carries the flownet -> nothing is downloaded."""
monkeypatch.setattr(
loader_module.torch,
"load",
lambda *args, **kwargs: {
"params_ema": {
"flownet.model.conv1.weight": torch.zeros(1),
"weight": torch.zeros(1),
}
},
)
pack = loader.load_keep_model_pack("KEEP", "retinaface_resnet50")
assert pack.keep_net.kwargs["flownet_path"] is None
assert "gmflow_sintel-0c07dcb3.pth" not in _downloaded(transport)
# the flownet is restored from the checkpoint (the strict load)
assert "flownet.model.conv1.weight" in pack.keep_net.loaded
assert pack.flownet_from_checkpoint is True
# the hook is a no-op - the weights are already there
assert pack.ensure_flow() is True
assert "gmflow_sintel-0c07dcb3.pth" not in _downloaded(transport)
class _RecordingInner:
"""The inner module of the flownet (it records the strict load)."""
def __init__(self) -> None:
self.loaded: tuple | None = None
def load_state_dict(self, weights, strict=True):
self.loaded = (weights, strict)
return self
class _RecordingFlownet:
"""A flownet that records the weights it receives."""
def __init__(self) -> None:
self.model = _RecordingInner()
@pytest.mark.unit
def test_ensure_flow_loads_the_gmflow_weights_strictly(
loader_module, stub_models, monkeypatch, tmp_path
):
"""A net without flow weights gets them from the GMFlow file."""
net, helper = stub_models
net.flownet = _RecordingFlownet()
pack = loader_module.KEEPModelPack(
net, helper, None, None, "KEEP",
flownet_from_checkpoint=False, flownet_path=None,
)
gmflow_path = tmp_path / "gmflow_sintel-0c07dcb3.pth"
gmflow_path.write_bytes(b"weights")
monkeypatch.setattr(
loader_module.keep_download,
"ensure_optional",
lambda entry, progress=True: str(gmflow_path),
)
monkeypatch.setattr(
loader_module.torch,
"load",
lambda *args, **kwargs: {"model": {"w": torch.zeros(1)}},
)
assert pack.ensure_flow() is True
weights, strict = net.flownet.model.loaded
assert strict is True, "the flow weights must be loaded with strict=True"
assert "w" in weights
# the resolution is cached - a second call does nothing
assert pack.ensure_flow() is True
@pytest.mark.mock_integration
def test_the_image_node_does_not_call_ensure_flow(
loader_module, node_module, monkeypatch
):
"""The image path leaves the flow resolution deferred."""
net = _StubKeepNet()
pack = loader_module.KEEPModelPack(
net, _StubFaceHelper(), None, None, "KEEP"
)
calls: list[str] = []
monkeypatch.setattr(pack, "ensure_flow", lambda: calls.append("ensure_flow"))
_patch_the_node(monkeypatch, node_module, _StubProcessor)
node_module.KEEP_FaceUpscaleImageNode.execute(
image=torch.zeros((1, 8, 8, 3)),
keep_model=pack,
final_upscale_factor=1.0,
has_aligned_face=False,
only_center_face=True,
draw_bounding_box=False,
)
assert calls == [], "the image path must not resolve the flow weights"