feat(validate): enum-check COMFY_DYNAMICCOMBO_V3 selections + presence-check dotted sub-inputs (BE-3358) - #573
Conversation
…e-check dotted sub-inputs (BE-3358)
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 50 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 5 finding(s).
| Severity | Count |
|---|---|
| 🟠 High | 1 |
| 🟡 Medium | 2 |
| 🟢 Low | 1 |
| ⚪ Nit | 1 |
Panel: 6/8 reviewers contributed findings.
Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)
…l (BE-3358) Address all five cursor-review panel findings on PR #573: - Autogrow sub-inputs under a dynamic-combo option (High): slot keys (model.images.image0, ...) now register as valid instead of warning unknown_input; a required autogrow subtree with zero wired slots errors (autogrow_no_slots) and a bare single connection errors (autogrow_bare_input) — mirroring the top-level autogrow path. - Recursion DoS (Medium): the _parse_inputs <-> _parse_dynamic_options mutual recursion is depth-bounded by _MAX_SUBGRAPH_DEPTH; a hostile object_info with pathologically nested combos degrades leniently instead of crashing with RecursionError. - Stale sub-key false positives (Medium): _check_dynamic_combo now runs before the generic edge checks and exports valid/unresolved key sets, so a stale link-valued sub-key from a previous selection (which the server ignores) no longer hard-errors dangling_edge/ output_index_out_of_range — it keeps its unknown_input warning. - required_input_missing hint (Low): selection keys truncate to the first 8 plus a count, like the unknown_enum_value branch. - Stray-key attribution (Nit): unknown dotted keys are attributed to the deepest RESOLVED combo prefix (model.mode='fast'), not the top-level base, and the hint lists that level's sub-keys. Also repair two tests that are red on main itself (semantic conflict between BE-3357's prompt_no_outputs check and BE-3359's validate tests, merged independently): test_api_format_unchanged gains an output node; test_empty_dict_payload_unchanged now expects the correct prompt_no_outputs rejection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Pushed 153a5ab addressing all five cursor-review panel findings (autogrow sub-ports under dynamic options, recursion-depth cap, stale sub-key edge-check false positives, hint truncation, deepest-prefix stray-key attribution) — each thread has details and dedicated tests. Note on the |
|
Heads-up: #576 just shipped the two test_validate_command.py fixture fixes standalone (they're what's breaking |
…combo-validation # Conflicts: # tests/comfy_cli/command/test_validate_command.py
|
Self-review passed at |
|
This PR currently conflicts with Please rebase (or merge |
…combo-validation # Conflicts: # comfy_cli/cql/engine.py
|
Rebased and reconciled a semantic conflict: Kept Ported this PR's genuinely additive pieces onto
Test suites from both PRs are reconciled in Full suite: 3669 passed, 37 skipped, 0 failed. |
skishore23
left a comment
There was a problem hiding this comment.
Strong PR overall — both BE-3349 repros behave exactly as specified, and it merges cleanly with 195 passing. But I found a case where the code doesn't implement a guarantee the description makes, and it's a forward-compatibility hazard. Small fix.
The stated leniency guard isn't implemented
From the description:
Unparseable options degrade to the old lenient behavior (no selection check at all) rather than false-erroring on a schema we can't read — the validator only becomes strict where the option schema genuinely parsed.
That's the right design. But when zero options parse, the empty key set is treated as authoritative and the selection is hard-rejected. Run against the PR's own fixture with a valid selection ("Opus 4.6"):
options = "garbage" -> unknown_enum_value: 'Opus 4.6' not in 0 known options for model
options = [1, 2, 3] -> unknown_enum_value: 'Opus 4.6' not in 0 known options for model
options = [] -> unknown_enum_value: 'Opus 4.6' not in 0 known options for model
"not in 0 known options" is self-refuting — it's the validator saying it had no basis to judge and rejecting anyway. Contrast with a partially malformed schema, where leniency does work correctly:
options = [{"key": "A", "inputs": "not-a-dict"}], sel="A" -> [] ✓ lenient
So the guard holds for a malformed individual option but not for a malformed options container.
Why it matters beyond the synthetic case: this guard exists for forward compatibility — a newer ComfyUI shipping a COMFY_DYNAMICCOMBO_V3 option shape this parser doesn't recognize. When that happens, options parses to zero entries and every workflow using that node hard-fails validation, blocking comfy run preflight on a graph the server would happily execute. That's precisely the failure mode the paragraph above promises to avoid, and it's the one that shows up on a server upgrade rather than in tests.
Fix — guard before the option is None branch (engine.py:1353):
if not keys:
# No option schema parsed (absent/unreadable `options`) — we have no basis to
# judge the selection, so stay lenient rather than hard-erroring on a schema we
# can't read. Matches the "only strict where the schema genuinely parsed" rule.
return errors, warnings, set(), {f"{name}."}Worth a test pinning options=[] / options="garbage" → valid: True, since the current suite only covers malformed entries.
Secondary: valid_options can contain null
An option dict missing key is counted but contributes None:
options = [{"inputs": {}}] -> "not in 1 known options"
valid_options=[None] suggestions=[None]
valid_options / suggestions are agent-facing "here's what to pick" lists, so a JSON null in them is worse than an empty list — an agent may well try to set the field to null. The count (1) also disagrees with the number of matchable keys (0). Filtering to k is not None when building keys fixes both, and folds into the guard above.
What's verified and good
- Clean merge with
origin/main; 195 passed (cql/+command/test_validate_command.py). - Both BE-3349 repros are exactly right, run against the real validator:
The second confirms your judgment call about suppressing sub-key noise when the base selection is unresolved — that's the right call, and it's genuinely implemented rather than just described.
{"model": "Opus 4.6"} -> required_input_missing on model.max_tokens, model.mode {"model": "NotARealModel", "model.bogus_key":5} -> unknown_enum_value on model, and NO warning on model.bogus_key - No malformed input crashes. I threw non-list
options, non-dict entries, missingkey, and non-dictinputsat it — every one returns a structured result rather than raising. - The description's "2 pre-existing main failures" caveat is stale in your favour —
test_api_format_unchangedandtest_empty_dict_payload_unchangedboth pass on the merged branch now (#565 landed in that area). Drop that section from the squash message.
Happy to approve once the empty-keys guard is in.
…combo-validation # Conflicts: # comfy_cli/cql/engine.py # tests/comfy_cli/cql/test_engine.py
…ils to parse skishore23 review: when zero options parse (unreadable/absent `options`, not just a malformed individual entry), the empty key set was treated as authoritative and the selection hard-rejected with a self-refuting "not in 0 known options" error — a forward-compat hazard on a newer ComfyUI option shape this parser doesn't yet recognize. Stay lenient in that case, same as an unparseable individual option entry already does. Also drop `None` (from an option dict missing `key`) out of the matchable key set so it can't leak into the agent-facing valid_options/suggestions lists.
|
Pushed 033e5fe addressing the changes-requested review: the empty-keys guard now returns lenient (no error) when the options container itself doesn't parse (absent/garbage/non-list Also merged Ready for re-review. |
ELI-5
Some cloud nodes (ClaudeNode, ReveImageCreateNode, …) have a "pick one" input like
modelwhere the pick brings its own extra settings (model.max_tokens,model.temperature). Until nowcomfy validateignored all of that completely: you could pick a model that doesn't exist and pass garbage settings, and validate would say everything is fine — then the server would reject it. Now validate checks the pick is real, checks the picked option's required settings are present and in range, and warns about settings the server would ignore.What
COMFY_DYNAMICCOMBO_V3inputs validated end-to-end incomfy_cli/cql/engine.py, mirroring the server (_io.pyDynamicCombo.Option.as_dict/_expand_schema_for_dynamic→ therequired_input_missingpresence check inexecution.py:884-900):{"key", "inputs": {"required", "optional"}}sub-schema is parsed into newPort.selection_keys/Port.dynamic_optionsvia the existing_parse_inputsmachinery, so nested dynamic combos (model.mode.budget) recurse naturally. Malformed options (non-dict, missing/non-stringkey, non-dictinputs) are skipped._check_dynamic_combo/_expand_dynamic_port):unknown_enum_valuecarrying the fullvalid_optionslist (same shape as the existing enum error);required_input_missingper key (same shape as the phase-1 path);validate_shape/validate_catalogexactly like top-level ports (shape_mismatch,unknown_enum_value,below_min/above_maxroute to errors per BE-3357);required_input_missing(phase 1 deliberately skips dynamic types, so this pass owns it);unknown_inputwarning (server ignores extra keys);nodes show/ describe output now includesselection_keysfor dynamic-combo ports;choicesstays[](selection keys are not flat enum choices —_is_scalar_choicesemantics untouched).Both BE-3349 repros now fail correctly:
{"model": "Opus 4.6"}→required_input_missingonmodel.max_tokens/model.mode;{"model": "NotARealModel", "model.bogus_key": 5}→unknown_enum_valueonmodel.Judgment calls
model.bogus_keywhile also erroringmodelitself would be pile-on noise. Once the selection is fixed, a re-validate flags the bogus key._check_required_present's exclusions (autogrow,COMFY_DYNAMICSLOT) to avoid double-error shapes.Not touched (pre-existing on main)
tests/comfy_cli/command/test_validate_command.py::test_api_format_unchangedand::test_empty_dict_payload_unchangedfail onorigin/mainitself (verified on a pristine checkout) — a semantic conflict between #551 (BE-3357prompt_no_outputshard error) and #553 (BE-3359 tests that assume outputless workflows exit 0). #565 (BE-3406) is already working in exactly that area, so this PR leaves it alone.Tests
New fixture
tests/comfy_cli/fixtures/dynamic_combo_object_info.json(BE-3349-shaped synthetic node: two options with different required sub-inputs incl. INT min/max and an enum, one option nesting a second dynamic combo) + 15 tests inTestDynamicComboInputscovering every case above, both BE-3349 repros, nested selections, malformed options, link-valued selections, and describe output. Existing autogrow tests unchanged.pytest: 2653 passed on this branch minus the 2 pre-existing main failures above;ruff check+ruff format --checkclean.