feat: add --truncate-validation-error-lists to shorten combo validation errors

Combo validation errors include the full list of valid options whenever it
has 20 or fewer entries, which can dump an entire folder listing (e.g. a
model directory) into the error text. Add an opt-in CLI flag that always
replaces the list with the existing short length summary while keeping the
input name and the offending value, so errors stay debuggable.

Default off: behavior is unchanged unless the flag is set.
This commit is contained in:
Matt Miller 2026-06-12 12:58:59 -07:00
parent 28a40fb2b2
commit 6667a7a374
4 changed files with 108 additions and 11 deletions

View File

@ -227,6 +227,8 @@ parser.add_argument("--user-directory", type=is_valid_directory, default=None, h
parser.add_argument("--enable-compress-response-body", action="store_true", help="Enable compressing response body.")
parser.add_argument("--truncate-validation-error-lists", action="store_true", help="Always replace the list of valid options in 'value not in list' prompt validation errors with a short length summary. By default the full list is included in the error message when it has 20 or fewer entries, which can embed entire folder listings for file/model dropdowns.")
parser.add_argument(
"--comfy-api-base",
type=str,

View File

@ -1,6 +1,27 @@
from comfy_api.latest import IO
def format_value_not_in_list_details(input_name, invalid_vals, combo_options, force_truncate=False, max_items=20):
"""Build the ``details`` string for a ``value_not_in_list`` validation error.
Returns a ``(details, truncated)`` tuple. ``details`` always names the
offending input and value(s) so the error stays debuggable. The list of
valid options is replaced with a short length summary when it has more
than ``max_items`` entries (so errors don't embed entire folder listings)
or when ``force_truncate`` is set; ``truncated`` is True in that case and
callers should also omit the input config from the error, since it
contains the same options list.
"""
if force_truncate or len(combo_options) > max_items:
list_info = f"(list of length {len(combo_options)})"
truncated = True
else:
list_info = str(combo_options)
truncated = False
details = f"{input_name}: {', '.join(repr(v) for v in invalid_vals)} not in {list_info}"
return details, truncated
def validate_node_input(
received_type: str, input_type: str, strict: bool = False
) -> bool:

View File

@ -37,7 +37,7 @@ from comfy_execution.graph import (
get_input_info,
)
from comfy_execution.graph_utils import GraphBuilder, is_link
from comfy_execution.validation import validate_node_input
from comfy_execution.validation import format_value_not_in_list_details, validate_node_input
from comfy_execution.progress import get_progress_state, reset_progress_state, add_progress_handler, WebUIProgressHandler
from comfy_execution.utils import CurrentNodeContext
from comfy_execution.asset_enrichment import enrich_output_with_assets
@ -1043,21 +1043,19 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
else:
invalid_vals = [val] if val not in combo_options else []
if invalid_vals:
input_config = info
list_info = ""
# Don't send back gigantic lists like if they're lots of
# scanned model filepaths
if len(combo_options) > 20:
list_info = f"(list of length {len(combo_options)})"
input_config = None
else:
list_info = str(combo_options)
# scanned model filepaths. The truncated form also omits
# the input config, which contains the same options list.
details, truncated = format_value_not_in_list_details(
x, invalid_vals, combo_options,
force_truncate=args.truncate_validation_error_lists,
)
input_config = None if truncated else info
error = {
"type": "value_not_in_list",
"message": "Value not in list",
"details": f"{x}: {', '.join(repr(v) for v in invalid_vals)} not in {list_info}",
"details": details,
"extra_info": {
"input_name": x,
"input_config": input_config,

View File

@ -0,0 +1,76 @@
import pytest
from comfy.cli_args import parser
from comfy_execution.validation import format_value_not_in_list_details
def _legacy_details(input_name, invalid_vals, combo_options):
"""The historical details format for short lists, kept byte-identical."""
return f"{input_name}: {', '.join(repr(v) for v in invalid_vals)} not in {str(combo_options)}"
def test_short_list_includes_full_options_by_default():
options = ["a.safetensors", "b.safetensors"]
details, truncated = format_value_not_in_list_details("ckpt_name", ["missing.safetensors"], options)
assert not truncated
assert details == _legacy_details("ckpt_name", ["missing.safetensors"], options)
assert "a.safetensors" in details
assert "b.safetensors" in details
def test_long_list_is_summarized_by_default():
options = [f"model_{i}.safetensors" for i in range(21)]
details, truncated = format_value_not_in_list_details("ckpt_name", ["missing.safetensors"], options)
assert truncated
assert details == "ckpt_name: 'missing.safetensors' not in (list of length 21)"
assert "model_0.safetensors" not in details
def test_max_items_boundary():
at_limit = [f"m{i}" for i in range(20)]
details, truncated = format_value_not_in_list_details("ckpt_name", ["nope"], at_limit)
assert not truncated
assert details == _legacy_details("ckpt_name", ["nope"], at_limit)
over_limit = at_limit + ["m20"]
details, truncated = format_value_not_in_list_details("ckpt_name", ["nope"], over_limit)
assert truncated
assert "(list of length 21)" in details
def test_force_truncate_summarizes_short_lists():
options = ["a.safetensors", "b.safetensors"]
details, truncated = format_value_not_in_list_details(
"ckpt_name", ["missing.safetensors"], options, force_truncate=True
)
assert truncated
assert details == "ckpt_name: 'missing.safetensors' not in (list of length 2)"
# The input name and offending value stay in the error so it remains debuggable.
assert "ckpt_name" in details
assert "missing.safetensors" in details
# None of the valid options appear in the error text.
assert "a.safetensors" not in details
assert "b.safetensors" not in details
@pytest.mark.parametrize("force_truncate", [False, True])
def test_multiple_invalid_values_are_preserved(force_truncate):
options = ["x", "y"]
details, _ = format_value_not_in_list_details(
"values", ["bad1", "bad2"], options, force_truncate=force_truncate
)
assert "'bad1', 'bad2'" in details
def test_cli_flag_defaults_off():
args = parser.parse_args([])
assert args.truncate_validation_error_lists is False
def test_cli_flag_parses_on():
args = parser.parse_args(["--truncate-validation-error-lists"])
assert args.truncate_validation_error_lists is True