Files
gregory-richard-ComfyUI-War…/tests/conftest.py
T
Gregory RichardandClaude Opus 5 56cc87fd92 fix: correctness and cleanup from the project review
Bugs found reading the pack end to end.

V3 mode registered five of the seven nodes: Save Image (Civitai) and
Prompt + LoRAs had no schema, so a workflow using either failed to open
with WARPPIPE_ENABLE_V3=1 and nothing said why. Both registration paths
are now built from one NODE_DEFINITIONS table, a missing V3 schema is
reported at startup, and a test asserts the two paths agree.

The built wheel shipped no JavaScript at all - package-data listed only
web/docs/*.md - so a registry install had a frontend directory holding
documentation and nothing else. A test now reads pyproject.toml and
checks every file under web/ is matched by a glob.

Also fixed:

- ruff format failed on two files, so CI was red on every push
- the nearest-model tie-break compared whole tuples, breaking ties on the
  filename rather than on the order the graph listed them
- SAFE_SAMPLERS and SAFE_SCHEDULERS referenced ComfyUI's own lists rather
  than copying them, so anything another pack appended later walked
  through coerce_scheduler untouched
- the hash and thumbnail caches wrote straight to their final paths,
  leaving a truncated file behind if anything interrupted them
- the Prompt + LoRAs node left a 100ms timer, a MutationObserver and its
  strip running after the node was removed
- model names and base models from downloaded sidecars were written into
  the browser's rows as markup without escaping
- a branch in the save node preferring "LoRAs recorded in the warp" could
  never run, because nothing ever wrote them there
- indexing the library read every .civitai.info twice
- the two views each held their own copy of the index and fetched it
  separately

The frontend had no automated checks at all, which is how two unused
arguments and an unescaped innerHTML survived. eslint and prettier now
run over web/ in their own CI job, with the lockfile committed for the
same reason the Python tools are pinned.

Five regression tests, each checked against the unfixed code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 21:46:47 +02:00

200 lines
5.6 KiB
Python

import importlib.util
import sys
import types
import uuid
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
class FakeTensor:
def __init__(self, shape):
self.shape = tuple(shape)
class _Field:
io_type = None
def __init__(self, field_id=None, display_name=None, options=None, **kwargs):
self.id = field_id
self.display_name = display_name or field_id
self.options = list(options or ())
self.metadata = kwargs
def _type_factory(io_type):
input_type = type("Input", (_Field,), {"io_type": io_type})
output_type = type("Output", (_Field,), {"io_type": io_type})
return type(
"FakeComfyType",
(),
{"io_type": io_type, "Input": input_type, "Output": output_type},
)
class _Schema:
def __init__(
self,
node_id,
display_name=None,
category=None,
description=None,
inputs=None,
outputs=None,
hidden=None,
**kwargs,
):
self.node_id = node_id
self.display_name = display_name
self.category = category
self.description = description
self.inputs = list(inputs or ())
self.outputs = list(outputs or ())
self.hidden = list(hidden or ())
self.metadata = kwargs
class _ComfyNode:
hidden = types.SimpleNamespace(
unique_id="test-node",
prompt=None,
extra_pnginfo=None,
)
@classmethod
def GET_SCHEMA(cls):
schema = cls.define_schema()
field_ids = [field.id for field in schema.inputs + schema.outputs if field.id]
if len(field_ids) != len(set(field_ids)):
raise ValueError("Schema field IDs must be unique")
return schema
class _NodeOutput(tuple):
def __new__(cls, *values, **kwargs):
instance = super().__new__(cls, values)
instance.metadata = kwargs
return instance
class _ComfyExtension:
pass
def _fake_io_module():
io = types.SimpleNamespace()
io.ComfyNode = _ComfyNode
io.Schema = _Schema
io.NodeOutput = _NodeOutput
io.Hidden = types.SimpleNamespace(unique_id=object(), prompt=object(), extra_pnginfo=object())
io.Custom = _type_factory
for name, io_type in (
("AnyType", "*"),
("Boolean", "BOOLEAN"),
("Clip", "CLIP"),
("ClipVision", "CLIP_VISION"),
("Combo", "COMBO"),
("Conditioning", "CONDITIONING"),
("Float", "FLOAT"),
("Image", "IMAGE"),
("Int", "INT"),
("Latent", "LATENT"),
("Mask", "MASK"),
("Model", "MODEL"),
("String", "STRING"),
("Vae", "VAE"),
):
setattr(io, name, _type_factory(io_type))
return io
def _install_fake_runtime(monkeypatch):
torch_module = types.ModuleType("torch")
torch_module.zeros = lambda shape: FakeTensor(shape)
monkeypatch.setitem(sys.modules, "torch", torch_module)
samplers_module = types.ModuleType("comfy.samplers")
samplers_module.SAMPLER_NAMES = ["euler", "heun"]
samplers_module.SCHEDULER_HANDLERS = {
"normal": object(),
"karras": object(),
"simple": object(),
}
samplers_module.SCHEDULER_NAMES = list(samplers_module.SCHEDULER_HANDLERS)
samplers_module.KSampler = type(
"KSampler",
(),
{
"SAMPLERS": samplers_module.SAMPLER_NAMES,
"SCHEDULERS": samplers_module.SCHEDULER_NAMES,
},
)
comfy_module = types.ModuleType("comfy")
comfy_module.__path__ = []
comfy_module.samplers = samplers_module
monkeypatch.setitem(sys.modules, "comfy", comfy_module)
monkeypatch.setitem(sys.modules, "comfy.samplers", samplers_module)
latest_module = types.ModuleType("comfy_api.latest")
latest_module.ComfyExtension = _ComfyExtension
latest_module.io = _fake_io_module()
comfy_api_module = types.ModuleType("comfy_api")
comfy_api_module.__path__ = []
comfy_api_module.latest = latest_module
monkeypatch.setitem(sys.modules, "comfy_api", comfy_api_module)
monkeypatch.setitem(sys.modules, "comfy_api.latest", latest_module)
def load_warp_pipe(monkeypatch, enable_v3=False):
_install_fake_runtime(monkeypatch)
if enable_v3:
monkeypatch.setenv("WARPPIPE_ENABLE_V3", "1")
else:
monkeypatch.delenv("WARPPIPE_ENABLE_V3", raising=False)
module_name = "_warppipe_test_" + uuid.uuid4().hex
spec = importlib.util.spec_from_file_location(module_name, PROJECT_ROOT / "warp_pipe.py")
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, module_name, module)
spec.loader.exec_module(module)
return module
def load_package(monkeypatch, enable_v3=False):
_install_fake_runtime(monkeypatch)
if enable_v3:
monkeypatch.setenv("WARPPIPE_ENABLE_V3", "1")
else:
monkeypatch.delenv("WARPPIPE_ENABLE_V3", raising=False)
module_name = "_warppipe_package_test_" + uuid.uuid4().hex
spec = importlib.util.spec_from_file_location(
module_name,
PROJECT_ROOT / "__init__.py",
submodule_search_locations=[str(PROJECT_ROOT)],
)
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, module_name, module)
spec.loader.exec_module(module)
return module
@pytest.fixture
def warp_pipe(monkeypatch):
return load_warp_pipe(monkeypatch)
@pytest.fixture
def warp_pipe_loader(monkeypatch):
return lambda enable_v3=False: load_warp_pipe(monkeypatch, enable_v3=enable_v3)
@pytest.fixture
def package_loader(monkeypatch):
return lambda enable_v3=False: load_package(monkeypatch, enable_v3=enable_v3)