* fix: preserve and validate model controls across the fal catalog * fix: preserve enum constraints through nested normalization
100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
"""Validate the committed model registry — pure JSON, no heavy imports."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
|
|
REGISTRY = Path(__file__).resolve().parents[1] / "data" / "fal_registry.json"
|
|
|
|
VALID_INPUT_TYPES = {"string", "integer", "number", "boolean", "enum", "object", "array", "json"}
|
|
VALID_OUTPUT_KINDS = {"images", "image", "video", "audio", "text", "file", "json"}
|
|
VALID_MEDIA_KINDS = {None, "image", "video", "audio", "file"}
|
|
|
|
|
|
def _registry():
|
|
return json.loads(REGISTRY.read_text(encoding="utf-8"))
|
|
|
|
|
|
def test_top_level_shape():
|
|
reg = _registry()
|
|
assert reg["version"] == 1
|
|
assert reg["model_count"] == len(reg["models"])
|
|
deprecated = sum(bool(model.get("deprecated")) for model in reg["models"])
|
|
assert reg["live_model_count"] == reg["model_count"] - deprecated
|
|
assert reg["deprecated_model_count"] == deprecated
|
|
assert reg["model_count"] > 500
|
|
|
|
|
|
def test_models_well_formed():
|
|
reg = _registry()
|
|
seen_ids = set()
|
|
for model in reg["models"]:
|
|
eid = model["endpoint_id"]
|
|
assert eid and "/" in eid, f"bad endpoint_id: {eid!r}"
|
|
assert eid not in seen_ids, f"duplicate endpoint_id: {eid}"
|
|
seen_ids.add(eid)
|
|
assert model["title"], f"{eid}: missing title"
|
|
assert model["category"], f"{eid}: missing category"
|
|
assert model["output_kind"] in VALID_OUTPUT_KINDS, f"{eid}: {model['output_kind']}"
|
|
assert isinstance(model["inputs"], list)
|
|
|
|
|
|
def test_inputs_well_formed():
|
|
reg = _registry()
|
|
for model in reg["models"]:
|
|
eid = model["endpoint_id"]
|
|
names = set()
|
|
for inp in model["inputs"]:
|
|
name = inp["name"]
|
|
assert name not in names, f"{eid}: duplicate input {name}"
|
|
names.add(name)
|
|
assert inp["type"] in VALID_INPUT_TYPES, f"{eid}.{name}: {inp['type']}"
|
|
assert inp.get("media_kind") in VALID_MEDIA_KINDS, f"{eid}.{name}"
|
|
if inp["type"] == "enum":
|
|
assert inp.get("enum"), f"{eid}.{name}: enum without values"
|
|
|
|
|
|
def test_enum_defaults_are_members_or_custom_size():
|
|
reg = _registry()
|
|
for model in reg["models"]:
|
|
for inp in model["inputs"]:
|
|
if inp["type"] == "enum" and inp.get("default") is not None:
|
|
if inp["default"] in inp["enum"]:
|
|
continue
|
|
# two legitimate non-member shapes exist in the wild:
|
|
# 1. has_custom_size enums defaulting to an explicit
|
|
# {width, height} object (mapped to the custom_size preset)
|
|
# 2. multi-select enums (is_list) defaulting to a list of
|
|
# members (mapped to a comma-separated string widget)
|
|
if inp.get("has_custom_size") and isinstance(inp["default"], dict):
|
|
continue
|
|
if inp.get("is_list") and isinstance(inp["default"], list):
|
|
assert all(v in inp["enum"] for v in inp["default"]), (
|
|
f"{model['endpoint_id']}.{inp['name']}: list default "
|
|
f"contains non-members"
|
|
)
|
|
continue
|
|
raise AssertionError(
|
|
f"{model['endpoint_id']}.{inp['name']}: default "
|
|
f"{inp['default']!r} not in enum"
|
|
)
|
|
|
|
|
|
def test_every_shipped_input_can_build_a_widget_without_comfyui():
|
|
"""The nightly refresh must render every control, without torch/ComfyUI.
|
|
|
|
The translator has only stdlib dependencies. Load it directly so the
|
|
scheduled registry checks don't need to import the whole node pack.
|
|
"""
|
|
path = REGISTRY.parents[1] / "nodes" / "dynamic" / "schema_to_inputs.py"
|
|
spec = importlib.util.spec_from_file_location("registry_widget_check", path)
|
|
translator = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(translator)
|
|
for model in _registry()["models"]:
|
|
input_types = translator.build_input_types(model)
|
|
widgets = {**input_types["required"], **input_types["optional"]}
|
|
for inp in model["inputs"]:
|
|
assert inp["name"] in widgets, f"{model['endpoint_id']}: missing {inp['name']} widget"
|