Files
gokayfem-ComfyUI-fal-API/tests/test_registry_format.py
Gökay Aydoğan b75f731abf feat: v2.0.0 — full fal catalog coverage, dynamic nodes, core rewrite (#75)
Auto-generated nodes for the entire live fal catalog (1,391 models →
1,482 total nodes) built at startup from a committed registry, plus a
full rewrite of the core layer. All 87 v1 node keys, inputs and outputs
are preserved — existing workflows load and run unchanged.

New:
- data/fal_registry.json + scripts/build_registry.py: distills fal's
  catalog and per-endpoint OpenAPI schemas into a deterministic registry
- nodes/dynamic/: schema→node factory (typed widgets, tooltips, pricing
  in node help, IMAGE/VIDEO/AUDIO auto-upload, native VIDEO/AUDIO
  outputs, seed -1=random, force_rerun), plus a generic Fal Any Endpoint
  node for arbitrary endpoints
- nodes/utils/: rewritten core — FalApiError surfacing fal's real error
  payloads, queue progress logs, ComfyUI cancel support, retries and
  timeouts everywhere, parallel uploads/downloads, structured logging
- tests/ (45 tests incl. a legacy-key snapshot lock), CI lint+test
  workflow, weekly registry-refresh workflow that opens a PR on changes
- README regenerated from the registry (scripts/build_readme.py)

Fixed:
- GPTImage2 / GPTImage2Edit were defined but never registered
- Topaz video upscale sent a nonexistent API field (desired_increase)
- SeedVR video upscaler had its error handling commented out + wrong
  model label; ProRes enum value mismatch translated at argument level
- 10x `is ""` comparisons, bare excepts, missing HTTP timeouts,
  temp-file leaks, fps=0 crash in LoadVideoURL, duplicate enum entry
- opencv-python & friends missing from pyproject (Registry installs)
- API failures now raise visible errors instead of silently returning
  blank images or "Error:" strings that downstream nodes tried to load

BREAKING (behavioral only): failed API calls raise instead of returning
blank/black images or error strings. Node signatures are unchanged.
2026-07-02 12:46:22 +03:00

79 lines
3.1 KiB
Python

"""Validate the committed model registry — pure JSON, no heavy imports."""
from __future__ import annotations
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"])
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"
)