From d70bec6ac5be24250be027f5f9214f05f109b3de Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Wed, 6 May 2026 15:17:21 -0500 Subject: [PATCH] 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 " 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 --- crates/cli/src/lib.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index cc6a2af4..9e1156d7 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -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 " 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 " + // 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 = 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"]);