diff --git a/comfy/cli_args.py b/comfy/cli_args.py index e7ee0d5eb..facb496a5 100644 --- a/comfy/cli_args.py +++ b/comfy/cli_args.py @@ -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, diff --git a/comfy_execution/validation.py b/comfy_execution/validation.py index ae9a2376c..00245f312 100644 --- a/comfy_execution/validation.py +++ b/comfy_execution/validation.py @@ -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: diff --git a/execution.py b/execution.py index 9e16e451d..4a8aa08da 100644 --- a/execution.py +++ b/execution.py @@ -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, diff --git a/tests-unit/execution_test/format_value_not_in_list_details_test.py b/tests-unit/execution_test/format_value_not_in_list_details_test.py new file mode 100644 index 000000000..085c8cd4c --- /dev/null +++ b/tests-unit/execution_test/format_value_not_in_list_details_test.py @@ -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