Give local JSON errors the same detail and hints as human output - #633
Open
sdairs wants to merge 2 commits into
Open
Give local JSON errors the same detail and hints as human output#633sdairs wants to merge 2 commits into
sdairs wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9786ed4. Configure here.
sdairs
force-pushed
the
fix/608-local-json-error-parity
branch
from
August 28, 2026 14:46
9786ed4 to
c06b9ab
Compare
`local --json` errors were thinner than the human `Error: ...` line they replace, which is backwards: JSON is the surface agents read. - `server remove <running>` now points at `local server stop <name>`, the step that actually recovers, instead of `local server list`. - Messages that clickhousectl composes itself are rendered verbatim, so a missing `--config` name lists the configs dir and the available files in JSON too, rather than collapsing to `local_error`/"Local command failed". - `local remove <not-installed-version>` reports the new `version_not_installed` code with `local list`, no longer conflating a local miss with an unresolvable download (`version_unavailable` and `local list --remote`). The mapping is now an exhaustive match over `Error`: every variant picks either parity (its own `Display` text) or redaction (a curated summary), so a new variant must be classified rather than silently collapsing. Only text that interpolates foreign output stays summarized -- subprocess stderr, Docker daemon strings, download bodies, OS/serde sources -- and new codes replace the fallback for config, server-name, version-state, platform, Docker and network failures. Docker unavailability, whose diagnostic is built by `local::docker` from a classified failure kind and never renders the daemon's text, now reaches JSON in full. Also splits the `--config` passthrough guard out of `Error::Exec` into `Error::UnsupportedArgument` so its self-composed guidance is rendered, and adds "no server was removed" to the bare `server remove` selection error in human mode, where JSON already said it. Tests: exact-payload unit tests for the three reported cases, a parity test asserting `message == error.to_string()` for every self-composed error, a redaction test asserting foreign output stays out, and subprocess coverage for the missing-config, escaping-config and passthrough-argument errors. README documents the two message rules and the extended code table. Fixes #608 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rors The JSON error contract states that error.message carries the human text whenever clickhousectl composes it itself, with redaction reserved for foreign output. Two variants violated that: the container name-conflict guidance was collapsed into the redacted docker_error summary, and every Error::Postgres message — all but one of which are self-composed validation or state guidance ending in a recovery hint — fell into the local_error fallback. Following the structural-contract rule (a failure a caller must tell apart gets its own variant, never message sniffing), split them: - Error::ContainerNameConflict renders parity as container_name_conflict, leaving Error::DockerError as the redacted daemon-text bucket. - Error::PostgresUsage renders parity as postgres_error; Error::Postgres keeps only the foreign-text psql exec failure and stays redacted. Human output is byte-identical for every migrated site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sdairs
force-pushed
the
fix/608-local-json-error-parity
branch
from
August 28, 2026 19:42
c06b9ab to
2157d67
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Why
local --jsonerrors were consistently thinner than the humanError: ...line they replace, which is backwards: JSON is the surface coding agents read. Issue #608 reported three cases; this fixes them and the rest of the mapping they were symptoms of.What
local server remove <running>suggestedclickhousectl local server list; it now suggestsclickhousectl local server stop <name>— the step that actually recovers — and carries the human message'sstop it first with ...clause.local server start --config does-not-existreportsconfig 'does-not-exist' not found in <dir> (available: analytics.xml)in JSON instead of{"code":"local_error","message":"Local command failed"}.local remove <not-installed-version>now reports a newversion_not_installedcode with aclickhousectl local listhint, instead ofversion_unavailable/clickhousectl local list --remote, which described an unresolvable download.The mapping became an exhaustive match over
Error. Every variant picks one of two documented rules: parity (render the error's ownDisplay) or redaction (a curated summary). A newErrorvariant now has to be classified rather than silently collapsing into the fallback. Only text interpolating output we do not control stays summarized — subprocess stderr and log tails (startup_exit,startup_timeout), Docker daemon strings (docker_error), download bodies (download_failed), OS/serde sources (io_error,local_error).New codes replace the fallback where it was hiding real classifications:
version_not_installed,version_selection_required,version_already_installed,unsupported_client_version,unsupported_platform,config_not_found,invalid_config_name,invalid_server_name,unsupported_argument,docker_unavailable,docker_error,network_error. Docker unavailability is built bylocal::dockerfrom a classified failure kind and platform guidance and never renders the daemon's own text, so it now reaches JSON in full.Two small adjacent fixes the audit turned up:
--configpassthrough guard usedError::Exec, whose payload is subprocess text and therefore redacted, so a pure usage mistake produced an opaquelocal_error. It now uses a dedicatedError::UnsupportedArgument.server removeselection error gained "no server was removed" in human mode, which JSON already stated.error.commandis nowOption<String>so a hint can name the concrete server or version.Tests
cargo fmt --all,cargo clippy --workspace --all-targets -- -D warnings,cargo test -p clickhousectl(all green), pluscargo check --workspace --all-features.self_composed_errors_serialize_their_human_message_verbatimassertsmessage == error.to_string()across every parity-classified error, so the two surfaces cannot drift again.errors_carrying_foreign_output_stay_summarizedasserts the complement, including that no injected secret or path is serialized.local_structured_errors_test.rsfor missing config, escaping config name, passthrough argument, the not-installed version, and the Docker diagnostic (still asserting the socket path and metadata contents never leak).README documents the two message rules and the extended error-code table.
Stacking
Part of a stacked chain: based on
fix/600-local-remove-global-guard, notmain. Keeps JSON parity with thelocal removeguards added there (version_is_default,server_runningforVersionInUse).Fixes #608
🤖 Generated with Claude Code