"""P3.2 / P4.1 - the mock-integration execution of the node classes. The loader node is a V3 node (P4.1) - its ``execute`` is a classmethod that returns an ``io.NodeOutput``. The tests drive it with a **stubbed loader** (the model build is stubbed out - no weights are needed) and assert the contract of the node: the pack comes back, the widgets reach the loader unchanged and the second call is served from the loader's cache. """ from __future__ import annotations import importlib import numpy as np import pytest import torch @pytest.fixture(scope="module") def node_module(): return importlib.import_module("ComfyUI_KEEP.nodes.keep_face_upscale_image") @pytest.fixture(scope="module") def loader_module(): return importlib.import_module("ComfyUI_KEEP.src.keep_model_loader") @pytest.fixture(scope="module") def loader_node_module(): return importlib.import_module("ComfyUI_KEEP.nodes.keep_model_loader") @pytest.fixture def stub_pack(loader_module, monkeypatch): """A real ``KEEPModelPack`` (the node checks the type) with stubs.""" pack = loader_module.KEEPModelPack( keep_net=None, face_helper=None, bg_upscale_model=None, face_upscale_model=None, model_type_str="KEEP", ) calls: list[str] = [] monkeypatch.setattr(pack, "load_device", lambda: calls.append("load_device")) monkeypatch.setattr(pack, "offload", lambda: calls.append("offload")) pack.calls = calls return pack @pytest.fixture def stubbed_loader(loader_module, monkeypatch, tmp_path): """A real ``KEEPModelLoader`` whose model build is stubbed out. The stub replaces the four heavy parts of the build (the architecture, the checkpoint download, the checkpoint load and the face helper) - the cache logic of the loader itself stays **real**. """ class _StubArch: def __init__(self, **kwargs): self.kwargs = kwargs self.loaded = None def to(self, device): return self def load_state_dict(self, state_dict, strict=True): 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, "load_file_from_url_comfy", lambda **kwargs: str(tmp_path / "checkpoint.pth"), ) 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() # --------------------------------------------------------------------------- # P4.1 - the model loader node # --------------------------------------------------------------------------- @pytest.mark.mock_integration def test_the_loader_node_returns_the_pack(loader_node_module, stubbed_loader, monkeypatch): """The node returns the pack of the loader as a ``NodeOutput``.""" from comfy_api.latest import io monkeypatch.setattr(loader_node_module, "GLOBAL_KEEP_MODEL_LOADER", stubbed_loader) output = loader_node_module.KEEP_ModelLoaderNode.execute( model="KEEP", detection_model="retinaface_resnet50", ) assert isinstance(output, io.NodeOutput) assert len(output.result) == 1 pack = output.result[0] assert pack.model_type_str == "KEEP" assert pack.keep_net is not None @pytest.mark.mock_integration def test_the_loader_node_serves_the_second_call_from_the_cache( loader_node_module, stubbed_loader, monkeypatch ): """The second call with the same widgets reuses the cached models.""" monkeypatch.setattr(loader_node_module, "GLOBAL_KEEP_MODEL_LOADER", stubbed_loader) first = loader_node_module.KEEP_ModelLoaderNode.execute( model="KEEP", detection_model="retinaface_resnet50" ).result[0] second = loader_node_module.KEEP_ModelLoaderNode.execute( model="KEEP", detection_model="retinaface_resnet50" ).result[0] assert first is not second, "every call returns its own pack" assert first.keep_net is second.keep_net, "the models are cached" assert first.face_helper is second.face_helper, "the models are cached" assert len(stubbed_loader.loaded_models) == 1, stubbed_loader.loaded_models @pytest.mark.mock_integration def test_the_loader_node_passes_the_widgets_to_the_loader( loader_node_module, monkeypatch ): """The widget values reach the loader unchanged.""" received: list[dict] = [] class _RecordingLoader: def load_keep_model_pack(self, **kwargs): received.append(kwargs) return "pack" monkeypatch.setattr(loader_node_module, "GLOBAL_KEEP_MODEL_LOADER", _RecordingLoader()) output = loader_node_module.KEEP_ModelLoaderNode.execute( model="KEEP", detection_model="retinaface_mobile0.25", bg_upscale_model="bg", face_upscale_model="face", ) assert output.result == ("pack",) assert received == [ { "model_type_str": "KEEP", "detection_model_str": "retinaface_mobile0.25", "bg_upscale_model": "bg", "face_upscale_model": "face", } ] # --------------------------------------------------------------------------- # P3.2 - the single image node (V1 until P4.2) # --------------------------------------------------------------------------- @pytest.mark.mock_integration def test_upscale_face_image_returns_the_processed_image(node_module, stub_pack, monkeypatch): """The node returns the processed image and uses the pack once.""" processed = np.full((4, 6, 3), 128, dtype=np.uint8) processor_calls: list[dict] = [] class _StubProcessor: def __init__(self, model_pack): processor_calls.append({"pack": model_pack}) def process_image(self, **kwargs): processor_calls.append({"kwargs": kwargs}) return processed monkeypatch.setattr(node_module, "KEEPFaceProcessor", _StubProcessor) image = torch.rand((1, 4, 6, 3), dtype=torch.float32) result = node_module.KEEP_FaceUpscaleImageNode().upscale_face_image( image, stub_pack, final_upscale_factor=1.0, has_aligned_face=True, only_center_face=True, draw_bounding_box=False, ) assert len(result) == 1 output = result[0] assert tuple(output.shape) == (1, 4, 6, 3), output.shape # the stub's image (128/255) came back assert torch.allclose(output, torch.full((1, 4, 6, 3), 128 / 255), atol=1e-3), output # the processor was built once and called once assert len(processor_calls) == 2, processor_calls assert processor_calls[0]["pack"] is stub_pack assert processor_calls[1]["kwargs"]["final_upscale_factor"] == 1.0 # the pack was loaded and offloaded exactly once each assert stub_pack.calls == ["load_device", "offload"], stub_pack.calls @pytest.mark.mock_integration def test_upscale_face_image_rejects_a_foreign_pack(node_module): """A pack of the wrong type is refused (and the error is logged).""" result = node_module.KEEP_FaceUpscaleImageNode().upscale_face_image( torch.rand((1, 4, 4, 3), dtype=torch.float32), object(), final_upscale_factor=1.0, has_aligned_face=True, only_center_face=True, draw_bounding_box=False, ) assert result == (None,)