fix(cli): print full anyhow chain on error exit (#767)

The dispatcher's top-level error handler prints `"error: {err}"`,
which is anyhow's bare Display. anyhow's Display only renders the
top-level context message and drops every cause beneath it. Users hit
"failed to parse config at <path>" with zero hint about the actual
TOML error (line/column, expected token, missing quote, BOM, etc.).

This is the gap reported in #767: the OP got
`error: failed to parse config at C:\Users\y1547\.deepseek\config.toml`
with nothing else, while a separate code path that uses a different
formatter shows a rich `Caused by: TOML parse error at line 1, column
20 ...` chain. Maintainer was unable to triage without the underlying
parse error.

Print the full chain by iterating `err.chain().skip(1)` after the
top-level message. Output for the issue's case becomes:

  error: failed to parse config at C:\Users\y1547\.deepseek\config.toml
    caused by: TOML parse error at line N, column M
      | (snippet from toml-rs)

Tests: regression test pins the chain semantics so a future refactor
of the print path can't silently drop causes again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-05-06 15:17:21 -05:00
parent 88c2a06024
commit d70bec6ac5
+35
View File
@@ -391,7 +391,17 @@ pub fn run_cli() -> std::process::ExitCode {
match run() {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(err) => {
// Use the full anyhow chain so callers see the underlying
// cause (e.g. the actual TOML parse error with line/column)
// instead of just the top-level context message. The bare
// `{err}` Display impl drops the chain — see #767, where
// users hit "failed to parse config at <path>" with no
// hint that the real error was a stray BOM or unbalanced
// quote a few lines down.
eprintln!("error: {err}");
for cause in err.chain().skip(1) {
eprintln!(" caused by: {cause}");
}
std::process::ExitCode::FAILURE
}
}
@@ -1328,6 +1338,31 @@ mod tests {
Cli::command().debug_assert();
}
// Regression for #767: `run_cli` prints the full anyhow chain so users
// see the underlying TOML parser error (line/column, expected token)
// instead of just the top-level "failed to parse config at <path>"
// wrapper. anyhow's bare `Display` impl drops the chain — pin both
// pieces here so a future refactor of the printing path doesn't
// silently regress.
#[test]
fn anyhow_chain_surfaces_toml_parse_cause() {
use anyhow::Context;
let inner = anyhow::anyhow!("TOML parse error at line 1, column 20");
let err = Err::<(), _>(inner)
.context("failed to parse config at C:\\Users\\test\\.deepseek\\config.toml")
.unwrap_err();
// What `eprintln!("error: {err}")` prints (top context only).
assert_eq!(
err.to_string(),
"failed to parse config at C:\\Users\\test\\.deepseek\\config.toml",
);
// What the `for cause in err.chain().skip(1)` loop iterates over.
let causes: Vec<String> = err.chain().skip(1).map(ToString::to_string).collect();
assert_eq!(causes, vec!["TOML parse error at line 1, column 20"]);
}
#[test]
fn parses_config_command_matrix() {
let cli = parse_ok(&["deepseek", "config", "get", "provider"]);