Files
olivv-cs-ComfyUI-FunPack/core/tests/test_routes_probe.py
T
digital-garbageandClaude Sonnet 5 93bdd27b32 app, core: a node's outputs are visible, and model family detection
Every node's own OUTPUTS were invisible on its own panel in the pipeline
editor -- wiring an input already let you pick a source, but opening the
node that PRODUCES something told you nothing about what it makes or
whether anything reads it. Found live, by a real user just looking at a
node, after two full review rounds that only ever checked the consuming
side. Fixed with a read-only "Outputs" section per node, naming each
output, its type, and who (if anyone) currently wires from it.

Also: model family detection. core/probe.py reads a safetensors header
(tensor names only, no weights) and asks every installed model module,
through the same capability-lookup mechanism already used for
empty_latent/decode, whether the file is its own -- core never names a
family itself. A real, adversarially-found bug in the ported signature:
one of MiniMax H3's three candidate prefixes doesn't correspond to any
prefix ComfyUI's own loader actually resolves to, traced through
comfy/model_detection.py and comfy/sd.py -- the probe could have called a
file H3 that ComfyUI's real loader would refuse. Removed, and pinned with
a dedicated test module. A second finding narrowed the folder search to
only the folders FunPack's own loaders actually read from.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 10:54:28 +03:00

125 lines
4.1 KiB
Python

"""Model family detection, over HTTP -- against the REAL registry, so this
proves modules/models/minimax_h3's own detect() is actually reachable
through core/registry.py's provider lookup, not just correct in isolation
(core/tests/test_probe.py) or in the module's own tests.
"""
import json
import socket
import struct
import threading
import pytest
pytest.importorskip("aiohttp")
from aiohttp import web # noqa: E402
from core import config, routes # noqa: E402
@pytest.fixture(scope="module")
def registered(comfyui):
"""FunPack's modules importable the way the real registry scan needs --
modules/models/minimax_h3 imports `comfy.*` at module level."""
import asyncio
import nodes as comfy_nodes
async def load():
await comfy_nodes.init_extra_nodes(init_custom_nodes=False)
asyncio.run(load())
return comfy_nodes
@pytest.fixture(scope="module")
def server(registered, tmp_path_factory):
from aiohttp import web as aioweb
models_dir = tmp_path_factory.mktemp("models")
import folder_paths
folder_paths.folder_names_and_paths["diffusion_models"] = ([str(models_dir)], {".safetensors"})
app = aioweb.Application()
table = aioweb.RouteTableDef()
routes.register(table, prefix="/funpack")
app.add_routes(table)
holder = {}
def run():
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
runner = aioweb.AppRunner(app)
loop.run_until_complete(runner.setup())
site = aioweb.TCPSite(runner, "127.0.0.1", 0)
loop.run_until_complete(site.start())
holder["port"] = site._server.sockets[0].getsockname()[1]
holder["loop"] = loop
holder["ready"].set()
loop.run_forever()
holder["ready"] = threading.Event()
holder["models_dir"] = models_dir
thread = threading.Thread(target=run, daemon=True)
thread.start()
assert holder["ready"].wait(20), "the test server did not start"
yield holder
holder["loop"].call_soon_threadsafe(holder["loop"].stop)
def _request(port, method, path):
head = (f"{method} {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n").encode()
with socket.create_connection(("127.0.0.1", port), timeout=30) as sock:
sock.sendall(head)
chunks = []
while True:
got = sock.recv(65536)
if not got:
break
chunks.append(got)
header, _, rest = b"".join(chunks).partition(b"\r\n\r\n")
status = int(header.split()[1])
try:
return status, json.loads(rest.decode() or "{}")
except ValueError:
return status, {"raw": rest.decode(errors="replace")}
def _write_safetensors(path, keys):
header = {k: {"dtype": "F16", "shape": [1], "data_offsets": [0, 0]} for k in keys}
body = json.dumps(header).encode("utf-8")
path.write_bytes(struct.pack("<Q", len(body)) + body)
def test_a_real_h3_signature_is_recognised_through_the_real_registry(server):
_write_safetensors(server["models_dir"] / "h3.safetensors",
["video_patch_proj.weight", "audio_patch_proj.weight"])
status, body = _request(server["port"], "GET", "/funpack/api/probe?file=h3.safetensors")
assert status == 200, body
assert body["module"] == "model_minimax_h3"
assert body["detected"] is True
def test_a_file_matching_no_known_signature_says_so(server):
_write_safetensors(server["models_dir"] / "unknown.safetensors", ["some.other.weight"])
status, body = _request(server["port"], "GET", "/funpack/api/probe?file=unknown.safetensors")
assert status == 200, body
assert body["module"] is None
assert body["detected"] is False
def test_a_file_that_does_not_exist_says_not_found(server):
status, body = _request(server["port"], "GET", "/funpack/api/probe?file=nope.safetensors")
assert status == 200
assert body["detected"] is False
assert "not found" in body["reason"]
def test_no_file_named_is_a_400_not_a_guess(server):
status, body = _request(server["port"], "GET", "/funpack/api/probe")
assert status == 400
assert body["problems"]