Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
234 lines
9.5 KiB
Python
234 lines
9.5 KiB
Python
"""Model family detection: a safetensors header read cold, then handed to
|
|
whichever model module claims it. Core never names a family -- these tests
|
|
prove that split holds, not any one architecture's signature (that belongs
|
|
to modules/models/minimax_h3's own tests).
|
|
"""
|
|
|
|
import json
|
|
import struct
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from core import probe
|
|
from core.contract import ModuleSpec
|
|
from core.registry import Registry
|
|
|
|
|
|
def _write_safetensors(path, keys):
|
|
header = {k: {"dtype": "F16", "shape": [1], "data_offsets": [0, 0]} for k in keys}
|
|
header["__metadata__"] = {"format": "pt"}
|
|
body = json.dumps(header).encode("utf-8")
|
|
path.write_bytes(struct.pack("<Q", len(body)) + body)
|
|
|
|
|
|
# --- reading the header -----------------------------------------------------
|
|
|
|
def test_keys_are_read_without_touching_anything_past_the_header(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["video_patch_proj.weight", "unrelated.bias"])
|
|
keys = probe.read_safetensors_keys(p)
|
|
assert set(keys) == {"video_patch_proj.weight", "unrelated.bias"}
|
|
|
|
|
|
def test_metadata_is_not_a_tensor_name(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["a.weight"])
|
|
assert "__metadata__" not in probe.read_safetensors_keys(p)
|
|
|
|
|
|
def test_a_file_too_short_to_hold_a_length_is_not_a_header(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
p.write_bytes(b"\x01\x02\x03")
|
|
assert probe.read_safetensors_keys(p) is None
|
|
|
|
|
|
def test_a_wild_length_is_refused_rather_than_allocated(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
# A length bigger than the ceiling, however the bytes got that way --
|
|
# some other file format's first eight bytes, not a real header.
|
|
p.write_bytes(struct.pack("<Q", probe.MAX_HEADER_BYTES + 1) + b"{}")
|
|
assert probe.read_safetensors_keys(p) is None
|
|
|
|
|
|
def test_a_header_that_is_not_json_is_not_a_header(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
body = b"not json"
|
|
p.write_bytes(struct.pack("<Q", len(body)) + body)
|
|
assert probe.read_safetensors_keys(p) is None
|
|
|
|
|
|
def test_a_header_that_is_valid_json_but_not_an_object_is_not_a_header(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
body = json.dumps(["a", "list"]).encode()
|
|
p.write_bytes(struct.pack("<Q", len(body)) + body)
|
|
assert probe.read_safetensors_keys(p) is None
|
|
|
|
|
|
# --- detect() -----------------------------------------------------------
|
|
|
|
def _registry(*claims):
|
|
"""claims: (id, title, keys_it_recognises) triples."""
|
|
reg = Registry()
|
|
for module_id, title, recognises in claims:
|
|
reg.add(ModuleSpec(id=module_id, title=title, mount="",
|
|
provides={"detect": lambda keyset, r=recognises: r <= keyset}))
|
|
return reg
|
|
|
|
|
|
def test_a_file_no_module_recognises_says_so_rather_than_guessing(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["some.unrelated.weight"])
|
|
out = probe.detect(p, registry=_registry(("m", "M", {"video_patch_proj.weight"})))
|
|
assert out == {"module": None, "title": None, "detected": False,
|
|
"reason": f"{p.name}: no installed model module recognises this file",
|
|
"traits": []}
|
|
|
|
|
|
def test_a_file_a_module_recognises_names_that_module(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["video_patch_proj.weight", "audio_patch_proj.weight"])
|
|
out = probe.detect(p, registry=_registry(("model_minimax_h3", "MiniMax H3",
|
|
{"video_patch_proj.weight", "audio_patch_proj.weight"})))
|
|
assert out["module"] == "model_minimax_h3"
|
|
assert out["title"] == "MiniMax H3"
|
|
assert out["detected"] is True
|
|
assert "MiniMax H3" in out["reason"]
|
|
|
|
|
|
# --- traits, only ever alongside a real detection ---------------------------
|
|
|
|
def test_an_undetected_file_carries_no_traits(tmp_path):
|
|
"""`detected: false` must mean "the caller should not filter", never
|
|
"filter to nothing" -- an empty list here is that contract, checked
|
|
explicitly so it cannot regress into `None` (which reads the same to a
|
|
human but not to `/api/modules?traits=`)."""
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["some.unrelated.weight"])
|
|
out = probe.detect(p, registry=_registry(("m", "M", {"video_patch_proj.weight"})))
|
|
assert out["traits"] == []
|
|
|
|
|
|
def test_a_matched_module_with_no_probe_traits_provider_says_unknown_not_none(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["shared.weight"])
|
|
out = probe.detect(p, registry=_registry(("m", "M", {"shared.weight"})))
|
|
assert out["traits"] is None # an empty list would hide every module needing a trait
|
|
|
|
|
|
def test_a_matched_modules_probe_traits_are_carried_through(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["shared.weight"])
|
|
reg = Registry()
|
|
reg.add(ModuleSpec(id="m", title="M", mount="",
|
|
provides={"detect": lambda keys: "shared.weight" in keys,
|
|
"probe_traits": lambda keys: ["audio_stream", "temporal_latent"]}))
|
|
out = probe.detect(p, registry=reg)
|
|
assert out["traits"] == ["audio_stream", "temporal_latent"]
|
|
|
|
|
|
def test_a_probe_traits_that_raises_is_skipped_not_fatal(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["shared.weight"])
|
|
def explodes(_keys):
|
|
raise RuntimeError("boom")
|
|
reg = Registry()
|
|
reg.add(ModuleSpec(id="m", title="M", mount="",
|
|
provides={"detect": lambda keys: "shared.weight" in keys,
|
|
"probe_traits": explodes}))
|
|
out = probe.detect(p, registry=reg)
|
|
assert out["module"] == "m"
|
|
assert out["detected"] is True
|
|
assert out["traits"] is None # unknown: it must not narrow what the UI shows
|
|
|
|
|
|
def test_a_missing_file_is_not_found_not_undetected(tmp_path):
|
|
out = probe.detect(tmp_path / "nope.safetensors", registry=_registry())
|
|
assert out["detected"] is False
|
|
assert "not found" in out["reason"]
|
|
|
|
|
|
def test_a_non_safetensors_extension_is_refused_without_reading_it(tmp_path):
|
|
p = tmp_path / "m.ckpt"
|
|
p.write_bytes(b"whatever a pickle looks like")
|
|
out = probe.detect(p, registry=_registry())
|
|
assert out["detected"] is False
|
|
assert "only .safetensors" in out["reason"]
|
|
|
|
|
|
def test_the_first_module_that_claims_it_wins_in_a_stable_order(tmp_path):
|
|
"""providers() is sorted by module id -- deterministic, not import order."""
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["shared.weight"])
|
|
out = probe.detect(p, registry=_registry(
|
|
("z_module", "Z", {"shared.weight"}),
|
|
("a_module", "A", {"shared.weight"}),
|
|
))
|
|
assert out["module"] == "a_module"
|
|
|
|
|
|
def test_a_detector_that_raises_is_skipped_not_fatal(tmp_path):
|
|
p = tmp_path / "m.safetensors"
|
|
_write_safetensors(p, ["real.weight"])
|
|
reg = Registry()
|
|
def explodes(_keys):
|
|
raise RuntimeError("boom")
|
|
reg.add(ModuleSpec(id="bad", title="Bad", mount="", provides={"detect": explodes}))
|
|
reg.add(ModuleSpec(id="good", title="Good", mount="",
|
|
provides={"detect": lambda keys: "real.weight" in keys}))
|
|
out = probe.detect(p, registry=reg)
|
|
assert out["module"] == "good"
|
|
|
|
|
|
# --- resolve_diffusion_model -------------------------------------------------
|
|
#
|
|
# `folder_paths` only exists inside a running ComfyUI, and whether it happens
|
|
# to be importable here depends on which OTHER tests already ran and pulled
|
|
# ComfyUI's source onto sys.path (core/tests/conftest.py's `comfyui` fixture
|
|
# does this session-wide, not just for the test that asked). Asserting on
|
|
# ambient absence would make this test's result depend on run order rather
|
|
# than on what resolve_diffusion_model() actually does -- so a fake module is
|
|
# installed instead of trusting whatever is or isn't already importable.
|
|
|
|
def _fake_folder_paths(monkeypatch, get_full_path):
|
|
fake = types.SimpleNamespace(get_full_path=get_full_path)
|
|
monkeypatch.setitem(sys.modules, "folder_paths", fake)
|
|
|
|
|
|
def test_resolve_finds_a_file_in_the_diffusion_models_folder(monkeypatch):
|
|
_fake_folder_paths(monkeypatch, lambda folder, name: (
|
|
"/models/diffusion_models/x.safetensors" if folder == "diffusion_models" else None))
|
|
assert probe.resolve_diffusion_model("x.safetensors") == Path("/models/diffusion_models/x.safetensors")
|
|
|
|
|
|
def test_resolve_falls_back_to_checkpoints(monkeypatch):
|
|
_fake_folder_paths(monkeypatch, lambda folder, name: (
|
|
"/models/checkpoints/x.safetensors" if folder == "checkpoints" else None))
|
|
assert probe.resolve_diffusion_model("x.safetensors") == Path("/models/checkpoints/x.safetensors")
|
|
|
|
|
|
def test_resolve_never_looks_in_a_unet_folder(monkeypatch):
|
|
"""No loader in this app reads a model from a "unet" folder --
|
|
modules/loaders/diffusion_model offers "diffusion_models" files,
|
|
modules/loaders/checkpoint offers "checkpoints" ones. Probing "unet" too
|
|
would risk answering for a file no loader here would ever actually load
|
|
under that name."""
|
|
asked = []
|
|
_fake_folder_paths(monkeypatch, lambda folder, name: (asked.append(folder), None)[1])
|
|
probe.resolve_diffusion_model("x.safetensors")
|
|
assert "unet" not in asked
|
|
|
|
|
|
def test_resolve_is_none_when_no_folder_has_it(monkeypatch):
|
|
_fake_folder_paths(monkeypatch, lambda folder, name: None)
|
|
assert probe.resolve_diffusion_model("nope.safetensors") is None
|
|
|
|
|
|
def test_resolve_is_none_when_folder_paths_itself_raises(monkeypatch):
|
|
def boom(folder, name):
|
|
raise KeyError(folder)
|
|
_fake_folder_paths(monkeypatch, boom)
|
|
assert probe.resolve_diffusion_model("x.safetensors") is None
|