diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/crates/fabro-cli/tests/it/cmd/config.rs index b5b9b1ac4..42a6476d2 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/config.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/config.rs @@ -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!( diff --git a/lib/crates/fabro-config/src/error.rs b/lib/crates/fabro-config/src/error.rs index 01ba23d44..6061c8d62 100644 --- a/lib/crates/fabro-config/src/error.rs +++ b/lib/crates/fabro-config/src/error.rs @@ -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, #[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, diff --git a/lib/crates/fabro-config/src/load.rs b/lib/crates/fabro-config/src/load.rs index dcfd6059e..6ca272981 100644 --- a/lib/crates/fabro-config/src/load.rs +++ b/lib/crates/fabro-config/src/load.rs @@ -10,7 +10,7 @@ use crate::{Error, Result, project, user}; pub fn load_settings_path(path: &Path) -> Result { 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)