From 6667a7a37411ab4140dc0f58e465b93cfa0371a0 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Fri, 12 Jun 2026 12:58:59 -0700 Subject: [PATCH] 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. --- comfy/cli_args.py | 2 + comfy_execution/validation.py | 21 +++++ execution.py | 20 +++-- .../format_value_not_in_list_details_test.py | 76 +++++++++++++++++++ 4 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 tests-unit/execution_test/format_value_not_in_list_details_test.py 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