Files
Gökay Aydoğan 647a38f265 Fix schema controls and prevent registry regressions across all models (#85)
* fix: preserve and validate model controls across the fal catalog

* fix: preserve enum constraints through nested normalization
2026-09-17 19:11:50 +03:00

157 lines
6.0 KiB
Python

"""Unit tests for the schema→INPUT_TYPES converter."""
from __future__ import annotations
from helpers import _input, _model
def test_required_and_optional_buckets(schema_to_inputs):
model = _model([
_input("prompt", "string", required=True, multiline=True),
_input("guidance", "number", default=3.5, min=1, max=20),
])
it = schema_to_inputs.build_input_types(model)
assert "prompt" in it["required"]
assert "guidance" in it["optional"]
assert it["required"]["prompt"][0] == "STRING"
assert it["required"]["prompt"][1]["multiline"] is True
def test_enum_becomes_dropdown(schema_to_inputs):
model = _model([_input("style", "enum", enum=["a", "b"], default="b")])
it = schema_to_inputs.build_input_types(model)
spec = it["optional"]["style"]
assert spec[0] == ["a", "b"]
assert spec[1]["default"] == "b"
def test_int_range_and_default_clamp(schema_to_inputs):
model = _model([_input("steps", "integer", default=28, min=1, max=50)])
it = schema_to_inputs.build_input_types(model)
typ, opts = it["optional"]["steps"]
assert typ == "INT"
assert opts["min"] == 1 and opts["max"] == 50 and opts["default"] == 28
def test_seed_spec(schema_to_inputs):
model = _model([_input("seed", "integer", required=True)])
it = schema_to_inputs.build_input_types(model)
# seed is always optional regardless of the API marking it required
typ, opts = it["optional"]["seed"]
assert typ == "INT"
assert opts["default"] == -1
assert opts["min"] == -1
assert opts.get("control_after_generate") is True
def test_media_inputs(schema_to_inputs):
model = _model([
_input("image_url", "string", required=True, media_kind="image"),
_input("video_url", "string", media_kind="video"),
_input("audio_url", "string", media_kind="audio"),
])
it = schema_to_inputs.build_input_types(model)
assert it["required"]["image_url"][0] == "IMAGE"
assert it["optional"]["video_url"][0] == "VIDEO"
assert it["optional"]["audio_url"][0] == "AUDIO"
def test_custom_size_companions(schema_to_inputs):
model = _model([
_input(
"image_size", "enum",
enum=["square", "landscape_4_3", "custom_size"],
default="landscape_4_3", has_custom_size=True,
)
])
it = schema_to_inputs.build_input_types(model)
assert "width" in it["optional"] and "height" in it["optional"]
assert it["optional"]["width"][0] == "INT"
def test_dict_default_maps_to_custom_size(schema_to_inputs):
model = _model([
_input(
"image_size", "enum",
enum=["square", "custom_size"],
default={"width": 2048, "height": 1536},
has_custom_size=True,
)
])
it = schema_to_inputs.build_input_types(model)
assert it["optional"]["image_size"][1]["default"] == "custom_size"
assert it["optional"]["width"][1]["default"] == 2048
assert it["optional"]["height"][1]["default"] == 1536
def test_multi_select_enum_is_comma_string(schema_to_inputs):
model = _model([
_input("stems", "enum", enum=["vocals", "drums", "bass"],
default=["vocals", "drums"], is_list=True)
])
it = schema_to_inputs.build_input_types(model)
typ, opts = it["optional"]["stems"]
assert typ == "STRING"
assert opts["default"] == "vocals, drums"
assert "vocals, drums, bass" in opts["tooltip"]
def test_numeric_multi_select_enum_keeps_api_value_types(schema_to_inputs, arguments_mod):
model = _model([_input("layers", "enum", enum=[1, 2, 4], default=[1, 4], is_list=True)])
typ, opts = schema_to_inputs.build_input_types(model)["optional"]["layers"]
assert typ == "STRING"
assert opts["default"] == "1, 4"
assert arguments_mod.build_arguments(model, {"layers": "1, 4"}) == {"layers": [1, 4]}
def test_json_field_is_multiline_string(schema_to_inputs):
model = _model([_input("loras", "json")])
it = schema_to_inputs.build_input_types(model)
typ, opts = it["optional"]["loras"]
assert typ == "STRING"
assert opts["multiline"] is True
def test_force_rerun_always_present(schema_to_inputs):
it = schema_to_inputs.build_input_types(_model([]))
typ, opts = it["optional"]["force_rerun"]
assert typ == "BOOLEAN"
assert opts["default"] is False
def test_every_input_has_tooltip_when_description_given(schema_to_inputs):
model = _model([_input("prompt", "string", required=True, description="What to draw")])
it = schema_to_inputs.build_input_types(model)
assert it["required"]["prompt"][1]["tooltip"] == "What to draw"
def test_suggestions_keep_string_socket_and_default(schema_to_inputs, arguments_mod):
model = _model([_input("voice", "string", default="my-voice-id", suggestions=["Aria", "Rachel"], multiline=True)])
spec = schema_to_inputs.build_input_types(model)["optional"]["voice"]
assert spec[0] == "STRING"
assert spec[1]["default"] == "my-voice-id"
assert spec[1]["fal_suggestions"] == ["Aria", "Rachel"]
assert spec[1]["multiline"] is False
assert arguments_mod.build_arguments(model, {"voice": "new-custom-voice"}) == {"voice": "new-custom-voice"}
def test_all_catalog_suggestions_preserve_input_names_and_arbitrary_values(schema_to_inputs, arguments_mod):
import json
from pathlib import Path
registry = json.loads((Path(__file__).resolve().parents[1] / "data" / "fal_registry.json").read_text())
count = 0
for model in registry["models"]:
inputs = schema_to_inputs.build_input_types(model)
widgets = {**inputs["required"], **inputs["optional"]}
for inp in model["inputs"]:
if not inp.get("suggestions"):
continue
count += 1
assert widgets[inp["name"]][0] == "STRING", model["endpoint_id"]
assert widgets[inp["name"]][1]["fal_suggestions"] == inp["suggestions"]
assert arguments_mod.build_arguments(model, {inp["name"]: "arbitrary-future-value"}) == {
inp["name"]: "arbitrary-future-value",
}
assert count > 20