fix(nodes,jobs): render a where_invalid envelope for a bad --where instead of a traceback or a silent misroute - #841
Conversation
… of a traceback `nodes._resolved_where` recovers from a corrupt persisted `where_default` by dropping the config and re-resolving. When the flag/env/project value is the bad one, that retry re-parses the same value and `where._parse` raises again, uncaught: `comfy --json nodes show --where bogus` printed a raw Python traceback on stderr with nothing on stdout, so a machine consumer got no `error.code`. Catch the second ValueError and emit the same `where_invalid` envelope every other routed command gets. The emit-and-exit tail of `resolve_default_or_exit` is split into `where.emit_where_invalid_or_exit`, and its hint into the `where.WHERE_INVALID_HINT` constant, so the two call sites cannot drift. The config-recovery branch is unchanged: a corrupt `where_default` still drops to the next precedence source instead of failing the command.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (5)
Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change standardizes invalid ChangesInvalid
Merge Risk: ⚪ Minimal · up to Invalid routing values for nodes and jobs now return a structured error and exit code instead of tracebacks or unintended local fallback. Coverage includes invalid routing sources and persisted-default recovery, with no current merge-readiness risk identified. 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 3 finding(s).
| Severity | Count |
|---|---|
| 🟢 Low | 1 |
| ⚪ Nit | 2 |
Panel: 6/6 reviewers contributed findings.
…ural
Three review findings from the cursor-review panel, all valid.
1. `where.py`'s docstring claimed both recover-first commands (`nodes`,
`jobs`) end on the shared envelope once their fallback is exhausted, but
only `nodes` did. `jobs._is_cloud` swallowed the `ValueError` and returned
False on the assumption that `cmdline.py`'s top-level `--where` had already
rejected the value. That only covers `comfy --where X jobs ls`. Verified
against the branch as it stood: `comfy --json jobs ls --where bogus` and
`COMFY_WHERE=bogus comfy --json jobs ls` both printed
`{"ok": true, "where": "local", ...}` and exited 0 — a machine consumer got
a successful *local* answer to a question about a target it never named,
which is worse than the traceback the `nodes` half of this branch removed.
`jobs` now mirrors `nodes` exactly: recover from a corrupt persisted
`where_default`, emit the envelope and exit 1 when the flag/env/project
value is the bad one. The docstring is now true rather than narrowed, and
`--where cloud` still routes cloud (`cloud_not_configured`, not a silent
local answer).
2. Neither `except ValueError` branch ended in an explicit `raise`, so both
were correct only because the helper was annotated `NoReturn` — a contract
nothing in CI enforces. `emit_where_invalid_or_exit` becomes
`where_invalid_exit`, which *returns* the `typer.Exit` for the caller to
`raise ... from exc`. The control flow is now structural at every call
site instead of load-bearing on an unchecked annotation.
3. The constant's comment claimed two call sites emit the hint while several
other `where_invalid` emitters hard-coded their own strings. The three that
fail the same *multi-source* way (`run`, `upload`, `download` all call
`resolve(flag=..., config_value=...)`) now use `WHERE_INVALID_HINT`. The
sites that validate a *single explicit* value keep the shorter hint on
purpose — the top-level `--where`, `set-default --where` and `setup` call
`_parse` on exactly the string the user just typed, and `logs` is
local-only, so pointing any of them at COMFY_WHERE/comfy.yaml would send
the user hunting in a file that had nothing to do with the failure. The
comment now says which sites share it and why the others don't.
Adds `test_jobs_where_invalid.py`, the sibling of the `nodes` file: all five
routed verbs for a bad flag, a bad `COMFY_WHERE`, a bad `defaults.where`, the
shared-hint pin, and the corrupt-config recovery branch this must not break.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ELI-5
comfy nodes show --where bogusused to crash with a raw Python stack trace and print nothing on stdout.comfy jobs ls --where boguswas worse — it printed a successful local listing and exited 0. Both now print the same one-line JSON error envelope (error.code == "where_invalid") that every other routed command already prints, so a script or agent reading the output gets an error code instead of a traceback or a wrong answer.What was wrong
nodes._resolved_where(comfy_cli/command/nodes.py) is written to survive a corrupt persistedwhere_default: it catches theValueErrorand retries with the config dropped. But when the flag /COMFY_WHERE/ projectdefaults.wherevalue is the bad one, that retry re-parses the exact same value,where._parseraises again, and nothing catches it. Reproduced onorigin/main(3fddc3e1):jobs._is_cloudhad the same hole with a different symptom: it swallowed theValueErrorand returnedFalse, on the commented assumption thatcmdline.py's top-level--wherehad already rejected the value. That only coverscomfy --where X jobs ls; a per-command flag or an exportedCOMFY_WHEREreaches no such validator:Every other routed command reaches
where.resolve_default_or_exit, which renders awhere_invalidenvelope. Thenodesandjobsverbs were deliberately kept on their own recovery fallbacks, but those fallbacks only ever recovered the config case.What changed
comfy_cli/where.py: split the emit-and-exit tail ofresolve_default_or_exitinto a reusablewhere_invalid_exit(exc) -> typer.Exit, and lifted its hint text into aWHERE_INVALID_HINTmodule constant, so the call sites that render this envelope cannot drift apart. The helper returns thetyper.Exitfor the caller toraise ... from excrather than being annotatedNoReturn— nothing in CI enforces aNoReturncontract, so theraiseis made structural at each call site instead.comfy_cli/command/nodes.py: the config-recovery retry is now wrapped in its ownexcept ValueError, which raiseswhere_invalid_exit. The config-recovery branch itself is untouched — a corruptwhere_defaultstill drops to the next precedence source rather than failing the command.comfy_cli/command/jobs.py:_is_cloudnow mirrorsnodesexactly — recover from a corrupt persistedwhere_default, emit the envelope and exit 1 when the flag/env/project value is the bad one.comfy_cli/cmdline.py:run,uploadanddownloadfail the same multi-source way (they all callresolve(flag=..., config_value=...)), so they now useWHERE_INVALID_HINTinstead of three copies of a shorter string. The sites that validate a single explicit value keep the shorter hint on purpose — the top-level--where,set-default --whereandsetupcall_parseon exactly the string the user just typed, andlogsis local-only, so pointing any of them atCOMFY_WHERE/comfy.yamlwould send the user hunting in a file that had nothing to do with the failure.tests/comfy_cli/command/test_nodes_where_invalid.py(17 tests) andtests/comfy_cli/command/test_jobs_where_invalid.py(13 tests).Verification
Reproduced both bugs first, then verified the fixes by running the real CLI (not only the test harness):
nodes show KSampler --where bogus --input FXwhere_invalidenvelope, exit 1COMFY_WHERE=bogus comfy nodes show KSampler --input FXwhere_invalidenvelope, exit 1defaults.where: bogusin aschema: project/1comfy.yamlwhere_invalidenvelope, exit 1jobs ls --where bogusok: true,where: "local", exit 0where_invalidenvelope, exit 1COMFY_WHERE=bogus comfy jobs lsok: true,where: "local", exit 0where_invalidenvelope, exit 1where_default+--where localjobs ls --where cloudcloud_not_configured,where: "cloud")All 9 graph-loading
nodesverbs were swept live with--where bogus(ls,show,search,upstream,downstream,path,types,categories,widget-catalog) — every one now returns the envelope with exit 1.nodes refreshis the tenth verb; it accepts--whereas a documented legacy no-op and never resolves routing, so it never had the defect. All 5 routedjobsverbs (ls,status,wait,cancel,watch) are covered too — each calls_stamp_whereas its first statement, so a bad value exits before any host/port resolution or network call.Only the genuinely invalid value is rejected:
--where localand--where cloudboth still resolve and route on every touched verb, confirmed live.Red→green proof: with the source files stashed, 14 of the 17
nodestests fail. The 3 that pass without the fix are exactly the regression controls (the two corrupt-config recovery cases and the valid-flag unit test) — i.e. they test pre-existing behavior rather than asserting my own premise. Thejobsfile'sTestBadFlag/TestBadEnvAndProjectcases all asserted exit 1 against a path that returned exit 0 before the change.Judgment calls
Exit code is 1, not 2. The plan this PR implements asked for "exit 2, exactly as
resolve_default_or_exitdoes for the other verbs" — those two clauses contradict each other.resolve_default_or_exitexits 1, as doworkflow.py's andlaunch.py'swhere_invalidpaths and the top-levelcomfy --where bogusflag (verified live:rc=1). I honored the "exactly as the other verbs" half, because an agent runner keying onwhere_invalidacross commands would otherwise see this one verb family disagree with all the rest. Say the word if 2 was actually intended and I'll flip it — but then it should be flipped everywhere at once.jobsis now in scope. It was originally left out because the plan's precondition ("apply the identical fix if it re-raises the same way") is false —jobsswallowed rather than re-raised. Review pointed out that this madewhere.py's own docstring untrue, and that silently answering local when the user named an unparseable target is a worse failure than the traceback. Folded in rather than deferred: it is the same five-line shape asnodes, on a resolver this PR already had to document.Residual
Not fixed here; each is actionable on its own.
assets librarystill emits a raw traceback for the same input, via a different resolver. Verified live on this branch:comfy --json assets library ls --where bogus→ rich traceback, exit 1. It routes throughtarget.resolve_target/cloud_target_or_local_error(comfy_cli/target.py~L86), which callswhere.resolve()with noValueErrorhandling at all — a separate code path from theresolve_defaultfamily this PR touches. Sizing the half not fixed: 22 call sites across 18 modules go throughresolve_target/cloud_target_or_local_error; each would need its own check to know whether a bad--wherethere tracebacks, silently misroutes, or is unreachable. I did not sweep all 22 — only the surfaces I could drive offline. Fixing that family centrally (haveresolve_targetrender the shared envelope) is the natural follow-up and would subsume this.Unexercised artifacts. The plan this PR implements was written from an upstream investigation whose write-up lives on a tracker I have no access to; I did not read it, so this PR rests entirely on the reproductions I ran myself rather than on that upstream evidence. The GitHub half of the duplicate check I could re-run:
gh pr list --repo Comfy-Org/comfy-cli --state open --search '_resolved_where OR where_invalid'returns nothing overlapping.One pre-existing test failure, unrelated to this change.
tests/comfy_cli/test_http.py::test_an_unloadable_supplement_falls_through_to_the_platform_rootsfails on this machine (a platform trust-store root count). Confirmed it fails identically on the unmodified branch head with these changes stashed, and it is green in CI, so it is environment-dependent and not introduced here. It is left untouched.Provenance
uv run --extra dev pytest -q: 7410 passed, 38 skipped, with the pre-existingtest_http.pytrust-store failure deselected (see Residual [New Feature] Web interface? #3);ruff check .: all checks passed;ruff format --diff .: 447 files already formatted; the two new files alone: 30 passed; live CLI repro + fix confirmed for bothnodesandjobsacross the flag/env/project routing sources, plus a live check that--where localand--where cloudstill route correctlyresolve_default_or_exitdoes" clause, and every existingwhere_invalidsite in the repo, use 1 — see Judgment calls); the plan's step 2jobsfix, initially skipped because its stated precondition was false, is now applied after review —jobssilently misrouted rather than re-raising, which is a worse shape than the one the precondition described