fix: preserve file path in config parse errors and fix test assertion

The error standardization lost the file path from parse error messages
when anyhow::Context was removed. Add path field to ParseSettings
variant so errors like "Failed to parse settings file at /path: ..."
include the file location. Also fix test that expected capitalized
"Workflow not found" to match the new lowercase error message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-11 12:20:17 -04:00
parent 1802a61963
commit dd44220bc3
No known key found for this signature in database
3 changed files with 36 additions and 13 deletions

View file

@ -436,10 +436,10 @@ fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() {
// checkpoint.exclude_globs is a security/policy list: replace by default.
let checkpoint = run_checkpoint(&cfg);
assert_eq!(checkpoint.exclude_globs, vec![
"run-only".to_string(),
"shared".to_string()
]);
assert_eq!(
checkpoint.exclude_globs,
vec!["run-only".to_string(), "shared".to_string()]
);
// Hooks: id-based replacement. The "shared" hook appears in both cli and
// workflow layers and resolves to the workflow entry; project and run-only
@ -529,9 +529,10 @@ fn settings_local_explicit_workflow_path_uses_workflow_project_layers() {
assert!(auto_approve_enabled(&cfg));
// v2 R30: run.prepare.steps replaces the whole ordered list across layers.
// The highest-precedence layer (workflow) wins.
assert_eq!(run_prepare_commands(&cfg), vec![
"workflow-setup".to_string()
]);
assert_eq!(
run_prepare_commands(&cfg),
vec!["workflow-setup".to_string()]
);
assert_eq!(run_sandbox(&cfg).preserve, Some(true));
}
@ -644,7 +645,7 @@ fn settings_missing_run_config_errors() {
assert!(String::from_utf8_lossy(&output.stdout).trim().is_empty());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("error: Workflow not found:"),
stderr.contains("workflow not found:"),
"stderr should report missing workflow path, got:\n{stderr}"
);
assert!(

View file

@ -1,5 +1,7 @@
use std::path::{Path, PathBuf};
use toml::de::Error as TomlError;
use crate::parse::ParseError;
use crate::resolve::ResolveError;
@ -11,6 +13,13 @@ fn format_resolve_errors(errors: &[ResolveError]) -> String {
.join("\n")
}
fn format_path_suffix(path: Option<&PathBuf>) -> String {
match path {
Some(p) => format!(" at {}", p.display()),
None => String::new(),
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("reading config file {path}: {source}")]
@ -20,9 +29,10 @@ pub enum Error {
source: std::io::Error,
},
#[error("{context}: {source}")]
#[error("{context}{}: {source}", format_path_suffix(.path.as_ref()))]
ParseSettings {
context: &'static str,
path: Option<PathBuf>,
#[source]
source: ParseError,
},
@ -31,7 +41,7 @@ pub enum Error {
TomlParse {
path: PathBuf,
#[source]
source: toml::de::Error,
source: TomlError,
},
#[error("{context}:\n{}", format_resolve_errors(.errors))]
@ -67,10 +77,22 @@ impl Error {
}
pub fn parse(context: &'static str, source: ParseError) -> Self {
Self::ParseSettings { context, source }
Self::ParseSettings {
context,
path: None,
source,
}
}
pub fn toml_parse(path: &Path, source: toml::de::Error) -> Self {
pub fn parse_file(context: &'static str, path: &Path, source: ParseError) -> Self {
Self::ParseSettings {
context,
path: Some(path.to_path_buf()),
source,
}
}
pub fn toml_parse(path: &Path, source: TomlError) -> Self {
Self::TomlParse {
path: path.to_path_buf(),
source,

View file

@ -10,7 +10,7 @@ use crate::{Error, Result, project, user};
pub fn load_settings_path(path: &Path) -> Result<SettingsLayer> {
let content = std::fs::read_to_string(path).map_err(|source| Error::read_file(path, source))?;
let mut layer = parse_settings_layer(&content)
.map_err(|err| Error::parse("Failed to parse settings file", err))?;
.map_err(|err| Error::parse_file("Failed to parse settings file", path, err))?;
let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
resolve_goal_file_paths(&mut layer, base_dir);
Ok(layer)