diff --git a/AGENTS.md b/AGENTS.md index 52d2a03f8..c8fd5fafb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `cargo nextest run -p fabro-workflow -- test_name` — run a single test - `set -a && source .env && set +a && cargo nextest run --workspace --profile e2e --run-ignored only` — run all E2E live tests (requires credentials in `.env`, see `.env.example`) - `set -a && source .env && set +a && cargo nextest run -p fabro-llm --profile e2e --run-ignored only` — run E2E tests for a single crate -- `cargo fmt --check --all` — check formatting +- `cargo +nightly fmt --check --all` — check formatting (nightly required for rustfmt config) +- `cargo +nightly fmt --all` — auto-format - `cargo clippy --workspace -- -D warnings` — lint macOS note: if `cargo nextest run` fails with `Too many open files (os error 24)` / `EMFILE`, raise the shell's soft FD limit before running tests, for example `ulimit -n 4096 && cargo nextest run --workspace`. Some terminals and inherited agent sessions start with `ulimit -n 256`, which is too low for the shared CLI test daemon under parallel nextest load. diff --git a/lib/crates/fabro-agent/src/error.rs b/lib/crates/fabro-agent/src/error.rs index cacba1f5c..892b451f1 100644 --- a/lib/crates/fabro-agent/src/error.rs +++ b/lib/crates/fabro-agent/src/error.rs @@ -49,7 +49,7 @@ mod tests { fn agent_error_from_sdk_error() { let sdk_err = LlmError::Network { message: "connection refused".into(), - source: None, + source: None, }; let agent_err = Error::from(sdk_err); assert!(matches!(agent_err, Error::Llm(_))); @@ -92,7 +92,7 @@ mod tests { fn serde_roundtrip_llm_network() { let err = Error::Llm(LlmError::Network { message: "connection refused".into(), - source: None, + source: None, }); let json = serde_json::to_string(&err).unwrap(); let deserialized: Error = serde_json::from_str(&json).unwrap(); @@ -102,14 +102,14 @@ mod tests { #[test] fn serde_roundtrip_llm_provider() { let err = Error::Llm(LlmError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail { - message: "too fast".into(), - provider: "openai".into(), + message: "too fast".into(), + provider: "openai".into(), status_code: Some(429), - error_code: None, + error_code: None, retry_after: Some(2.0), - raw: None, + raw: None, }), }); let json = serde_json::to_string(&err).unwrap(); @@ -156,7 +156,7 @@ mod tests { let errors: Vec = vec![ Error::Llm(LlmError::Network { message: "refused".into(), - source: None, + source: None, }), Error::SessionClosed, Error::InvalidState("reason".into()), @@ -174,7 +174,7 @@ mod tests { fn serde_tag_format_llm() { let err = Error::Llm(LlmError::Network { message: "refused".into(), - source: None, + source: None, }); let json = serde_json::to_string(&err).unwrap(); let v: serde_json::Value = serde_json::from_str(&json).unwrap(); diff --git a/lib/crates/fabro-agent/src/sandbox.rs b/lib/crates/fabro-agent/src/sandbox.rs index b452538c9..6df954184 100644 --- a/lib/crates/fabro-agent/src/sandbox.rs +++ b/lib/crates/fabro-agent/src/sandbox.rs @@ -1,8 +1,8 @@ // Re-export all sandbox types from fabro-sandbox. // Re-export the delegate_sandbox! macro at crate root so existing // `crate::delegate_sandbox!` invocations continue to work. -pub use fabro_sandbox::delegate_sandbox; pub use fabro_sandbox::{ DirEntry, ExecResult, GrepOptions, Sandbox, SandboxEvent, SandboxEventCallback, WorktreeEvent, - WorktreeEventCallback, WorktreeOptions, WorktreeSandbox, format_lines_numbered, shell_quote, + WorktreeEventCallback, WorktreeOptions, WorktreeSandbox, delegate_sandbox, + format_lines_numbered, shell_quote, }; diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/crates/fabro-cli/tests/it/cmd/config.rs index 42a6476d2..9a9aeb223 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,10 +529,9 @@ 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)); } diff --git a/lib/crates/fabro-config/src/effective_settings.rs b/lib/crates/fabro-config/src/effective_settings.rs index 39ac69a4c..c1eea1c6e 100644 --- a/lib/crates/fabro-config/src/effective_settings.rs +++ b/lib/crates/fabro-config/src/effective_settings.rs @@ -23,10 +23,10 @@ pub enum EffectiveSettingsMode { #[derive(Clone, Debug, Default)] pub struct EffectiveSettingsLayers { - pub args: SettingsLayer, + pub args: SettingsLayer, pub workflow: SettingsLayer, - pub project: SettingsLayer, - pub user: SettingsLayer, + pub project: SettingsLayer, + pub user: SettingsLayer, } impl EffectiveSettingsLayers { diff --git a/lib/crates/fabro-config/src/error.rs b/lib/crates/fabro-config/src/error.rs index 6061c8d62..ead3042ad 100644 --- a/lib/crates/fabro-config/src/error.rs +++ b/lib/crates/fabro-config/src/error.rs @@ -24,7 +24,7 @@ fn format_path_suffix(path: Option<&PathBuf>) -> String { pub enum Error { #[error("reading config file {path}: {source}")] ReadFile { - path: PathBuf, + path: PathBuf, #[source] source: std::io::Error, }, @@ -32,14 +32,14 @@ pub enum Error { #[error("{context}{}: {source}", format_path_suffix(.path.as_ref()))] ParseSettings { context: &'static str, - path: Option, + path: Option, #[source] - source: ParseError, + source: ParseError, }, #[error("parsing TOML config at {path}: {source}")] TomlParse { - path: PathBuf, + path: PathBuf, #[source] source: TomlError, }, @@ -47,13 +47,13 @@ pub enum Error { #[error("{context}:\n{}", format_resolve_errors(.errors))] Resolve { context: &'static str, - errors: Vec, + errors: Vec, }, #[error("missing required environment variable {var} for {field}")] MissingEnvVar { - field: String, - var: String, + field: String, + var: String, #[source] source: std::env::VarError, }, diff --git a/lib/crates/fabro-config/src/project.rs b/lib/crates/fabro-config/src/project.rs index d1741265e..59c6cbd95 100644 --- a/lib/crates/fabro-config/src/project.rs +++ b/lib/crates/fabro-config/src/project.rs @@ -21,10 +21,10 @@ const CONFIG_FILENAME: &str = "fabro.toml"; #[derive(Clone, Debug)] pub struct WorkflowPathResolution { pub resolved_workflow_path: PathBuf, - pub dot_path: PathBuf, - pub workflow_config: Option, - pub workflow_toml_path: Option, - pub workflow_slug: Option, + pub dot_path: PathBuf, + pub workflow_config: Option, + pub workflow_toml_path: Option, + pub workflow_slug: Option, } /// Parse a project config from a TOML string. @@ -222,8 +222,8 @@ fn user_workflows_dir() -> PathBuf { /// Metadata about a discovered workflow. #[derive(Clone, Debug, Serialize)] pub struct WorkflowInfo { - pub name: String, - pub goal: Option, + pub name: String, + pub goal: Option, pub source: WorkflowSource, } diff --git a/lib/crates/fabro-config/src/run.rs b/lib/crates/fabro-config/src/run.rs index d6a6d93af..e4e536913 100644 --- a/lib/crates/fabro-config/src/run.rs +++ b/lib/crates/fabro-config/src/run.rs @@ -43,7 +43,7 @@ pub enum ResolveRunGoalError { var: String, }, Io { - path: PathBuf, + path: PathBuf, source: std::io::Error, }, } @@ -81,7 +81,7 @@ pub fn resolve_run_goal( match goal { RunGoalLayer::Inline(text) => Ok(Some(ResolvedRunGoal { - text: text.as_source(), + text: text.as_source(), source: ResolvedGoalSource::Inline, })), RunGoalLayer::File { file } => { diff --git a/lib/crates/fabro-core/src/error.rs b/lib/crates/fabro-core/src/error.rs index cda29f081..75f973f64 100644 --- a/lib/crates/fabro-core/src/error.rs +++ b/lib/crates/fabro-core/src/error.rs @@ -22,9 +22,9 @@ impl fmt::Display for VisitLimitSource { /// to_fail_outcome(). #[derive(Debug, Clone)] pub struct HandlerErrorDetail { - pub message: String, + pub message: String, pub retryable: bool, - pub category: Option, + pub category: Option, pub signature: Option, } @@ -48,9 +48,9 @@ pub enum Error { "node \"{node_id}\" visited {visits} times ({limit_source} limit {limit}); run is stuck in a cycle" )] VisitLimitExceeded { - node_id: String, - visits: usize, - limit: usize, + node_id: String, + visits: usize, + limit: usize, limit_source: VisitLimitSource, }, #[error("stall timeout on node \"{node_id}\"")] @@ -81,8 +81,8 @@ impl Error { Self::Handler { detail } => Outcome { status: StageStatus::Fail, failure: Some(FailureDetail { - message: detail.message.clone(), - category: detail.category.unwrap_or(FailureCategory::Deterministic), + message: detail.message.clone(), + category: detail.category.unwrap_or(FailureCategory::Deterministic), signature: detail.signature.clone(), }), ..Outcome::default() @@ -119,9 +119,9 @@ mod tests { ); assert_eq!( Error::VisitLimitExceeded { - node_id: "n1".into(), - visits: 5, - limit: 3, + node_id: "n1".into(), + visits: 5, + limit: 3, limit_source: VisitLimitSource::Node, } .to_string(), @@ -143,17 +143,17 @@ mod tests { #[test] fn core_error_handler_is_retryable() { let retryable = Error::handler(HandlerErrorDetail { - message: "timeout".into(), + message: "timeout".into(), retryable: true, - category: None, + category: None, signature: None, }); assert!(retryable.is_retryable()); let not_retryable = Error::handler(HandlerErrorDetail { - message: "bad input".into(), + message: "bad input".into(), retryable: false, - category: None, + category: None, signature: None, }); assert!(!not_retryable.is_retryable()); @@ -163,9 +163,9 @@ mod tests { fn core_error_handler_to_fail_outcome() { use crate::outcome::FailureCategory; let err = Error::handler(HandlerErrorDetail { - message: "api down".into(), + message: "api down".into(), retryable: true, - category: Some(FailureCategory::TransientInfra), + category: Some(FailureCategory::TransientInfra), signature: Some("sig123".into()), }); let outcome: Outcome = err.to_fail_outcome(); diff --git a/lib/crates/fabro-llm/src/error.rs b/lib/crates/fabro-llm/src/error.rs index 5c9b97b06..c846f251c 100644 --- a/lib/crates/fabro-llm/src/error.rs +++ b/lib/crates/fabro-llm/src/error.rs @@ -30,23 +30,23 @@ impl std::fmt::Display for ProviderErrorKind { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ProviderErrorDetail { - pub message: String, - pub provider: String, + pub message: String, + pub provider: String, pub status_code: Option, - pub error_code: Option, + pub error_code: Option, pub retry_after: Option, - pub raw: Option, + pub raw: Option, } impl ProviderErrorDetail { pub fn new(message: impl Into, provider: impl Into) -> Self { Self { - message: message.into(), - provider: provider.into(), + message: message.into(), + provider: provider.into(), status_code: None, - error_code: None, + error_code: None, retry_after: None, - raw: None, + raw: None, } } } @@ -58,7 +58,7 @@ use std::sync::Arc; pub enum Error { #[error("{kind} {}: {}", .detail.provider, .detail.message)] Provider { - kind: ProviderErrorKind, + kind: ProviderErrorKind, detail: Box, }, @@ -67,7 +67,7 @@ pub enum Error { message: String, #[source] #[serde(skip)] - source: Option>, + source: Option>, }, #[error("Request interrupted: {message}")] @@ -78,7 +78,7 @@ pub enum Error { message: String, #[source] #[serde(skip)] - source: Option>, + source: Option>, }, #[error("Stream error: {message}")] @@ -86,7 +86,7 @@ pub enum Error { message: String, #[source] #[serde(skip)] - source: Option>, + source: Option>, }, #[error("Invalid tool call: {message}")] @@ -100,7 +100,7 @@ pub enum Error { message: String, #[source] #[serde(skip)] - source: Option>, + source: Option>, }, #[error("Unsupported tool choice: {message}")] @@ -114,7 +114,7 @@ impl Error { ) -> Self { Self::Network { message: message.into(), - source: Some(Arc::new(source)), + source: Some(Arc::new(source)), } } @@ -124,7 +124,7 @@ impl Error { ) -> Self { Self::RequestTimeout { message: message.into(), - source: Some(Arc::new(source)), + source: Some(Arc::new(source)), } } @@ -134,7 +134,7 @@ impl Error { ) -> Self { Self::Stream { message: message.into(), - source: Some(Arc::new(source)), + source: Some(Arc::new(source)), } } @@ -144,7 +144,7 @@ impl Error { ) -> Self { Self::Configuration { message: message.into(), - source: Some(Arc::new(source)), + source: Some(Arc::new(source)), } } @@ -291,7 +291,7 @@ pub fn error_from_status_code( 408 => { return Error::RequestTimeout { message: detail.message, - source: None, + source: None, }; } 413 => ProviderErrorKind::ContextLength, @@ -349,7 +349,7 @@ pub fn error_from_grpc_status( "DEADLINE_EXCEEDED" => { return Error::RequestTimeout { message: detail.message, - source: None, + source: None, }; } _ => ProviderErrorKind::Server, @@ -373,7 +373,7 @@ mod tests { #[test] fn retryable_classification() { let auth_err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail { status_code: Some(401), ..ProviderErrorDetail::new("bad key", "openai") @@ -382,7 +382,7 @@ mod tests { assert!(!auth_err.retryable()); let rate_err = SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail { status_code: Some(429), retry_after: Some(2.0), @@ -393,7 +393,7 @@ mod tests { assert_eq!(rate_err.retry_after(), Some(2.0)); let server_err = SdkError::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("internal error", "anthropic") @@ -403,19 +403,19 @@ mod tests { let timeout = SdkError::RequestTimeout { message: "timed out".into(), - source: None, + source: None, }; assert!(!timeout.retryable()); let network = SdkError::Network { message: "connection refused".into(), - source: None, + source: None, }; assert!(network.retryable()); let config = SdkError::Configuration { message: "missing provider".into(), - source: None, + source: None, }; assert!(!config.retryable()); } @@ -425,37 +425,37 @@ mod tests { let detail = || Box::new(ProviderErrorDetail::new("error", "openai")); let access_denied = SdkError::Provider { - kind: ProviderErrorKind::AccessDenied, + kind: ProviderErrorKind::AccessDenied, detail: detail(), }; assert!(!access_denied.retryable()); let not_found = SdkError::Provider { - kind: ProviderErrorKind::NotFound, + kind: ProviderErrorKind::NotFound, detail: detail(), }; assert!(!not_found.retryable()); let invalid_req = SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, + kind: ProviderErrorKind::InvalidRequest, detail: detail(), }; assert!(!invalid_req.retryable()); let ctx_length = SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: detail(), }; assert!(!ctx_length.retryable()); let quota = SdkError::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: detail(), }; assert!(!quota.retryable()); let content_filter = SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, + kind: ProviderErrorKind::ContentFilter, detail: detail(), }; assert!(!content_filter.retryable()); @@ -489,44 +489,32 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::Authentication, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::Authentication, + .. + })); assert!(!err.retryable()); let err = error_from_status_code(403, "forbidden".into(), "openai".into(), None, None, None); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::AccessDenied, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::AccessDenied, + .. + })); let err = error_from_status_code(404, "not found".into(), "openai".into(), None, None, None); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::NotFound, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::NotFound, + .. + })); let err = error_from_status_code(400, "bad request".into(), "openai".into(), None, None, None); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::InvalidRequest, + .. + })); let err = error_from_status_code( 422, @@ -536,26 +524,20 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::InvalidRequest, + .. + })); let err = error_from_status_code(408, "timeout".into(), "openai".into(), None, None, None); assert!(matches!(err, SdkError::RequestTimeout { .. })); let err = error_from_status_code(413, "too large".into(), "openai".into(), None, None, None); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::ContextLength, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::ContextLength, + .. + })); let err = error_from_status_code( 429, @@ -565,35 +547,26 @@ mod tests { None, Some(5.0), ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::RateLimit, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::RateLimit, + .. + })); assert!(err.retryable()); assert_eq!(err.retry_after(), Some(5.0)); let err = error_from_status_code(500, "internal".into(), "openai".into(), None, None, None); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::Server, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::Server, + .. + })); assert!(err.retryable()); let err = error_from_status_code(502, "bad gateway".into(), "openai".into(), None, None, None); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::Server, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::Server, + .. + })); let err = error_from_status_code( 529, @@ -603,13 +576,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::Server, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::Server, + .. + })); assert!(err.retryable()); } @@ -623,13 +593,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::ContextLength, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::ContextLength, + .. + })); } #[test] @@ -642,13 +609,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::ContextLength, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::ContextLength, + .. + })); } #[test] @@ -661,13 +625,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::ContentFilter, + .. + })); } #[test] @@ -680,13 +641,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::ContentFilter, + .. + })); } #[test] @@ -699,13 +657,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::NotFound, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::NotFound, + .. + })); } #[test] @@ -718,13 +673,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::NotFound, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::NotFound, + .. + })); } #[test] @@ -737,13 +689,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::Authentication, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::Authentication, + .. + })); } #[test] @@ -756,13 +705,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::Authentication, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::Authentication, + .. + })); } #[test] @@ -775,13 +721,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::NotFound, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::NotFound, + .. + })); let err = error_from_grpc_status( "RESOURCE_EXHAUSTED", @@ -791,13 +734,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::RateLimit, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::RateLimit, + .. + })); assert!(err.retryable()); let err = error_from_grpc_status( @@ -808,13 +748,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::Authentication, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::Authentication, + .. + })); let err = error_from_grpc_status( "DEADLINE_EXCEEDED", @@ -834,19 +771,16 @@ mod tests { None, None, ); - assert!(matches!( - err, - SdkError::Provider { - kind: ProviderErrorKind::Server, - .. - } - )); + assert!(matches!(err, SdkError::Provider { + kind: ProviderErrorKind::Server, + .. + })); } #[test] fn error_display_messages() { let err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail { status_code: Some(401), ..ProviderErrorDetail::new("invalid api key", "openai") @@ -859,7 +793,7 @@ mod tests { let err = SdkError::Configuration { message: "no provider".into(), - source: None, + source: None, }; assert_eq!(err.to_string(), "Configuration error: no provider"); } @@ -867,7 +801,7 @@ mod tests { #[test] fn status_code_accessor() { let err = SdkError::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(503), ..ProviderErrorDetail::new("error", "openai") @@ -877,7 +811,7 @@ mod tests { let err = SdkError::Network { message: "refused".into(), - source: None, + source: None, }; assert_eq!(err.status_code(), None); } @@ -885,7 +819,7 @@ mod tests { #[test] fn provider_name_from_provider_variant() { let err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }; assert_eq!(err.provider_name(), "openai"); @@ -895,7 +829,7 @@ mod tests { fn provider_name_defaults_to_unknown() { let err = SdkError::Network { message: "refused".into(), - source: None, + source: None, }; assert_eq!(err.provider_name(), "unknown"); } @@ -906,7 +840,7 @@ mod tests { assert!( SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: detail(), } .failover_eligible() @@ -914,7 +848,7 @@ mod tests { assert!( SdkError::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: detail(), } .failover_eligible() @@ -922,7 +856,7 @@ mod tests { assert!( SdkError::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: detail(), } .failover_eligible() @@ -934,7 +868,7 @@ mod tests { assert!( SdkError::RequestTimeout { message: "timed out".into(), - source: None, + source: None, } .failover_eligible() ); @@ -942,7 +876,7 @@ mod tests { assert!( SdkError::Network { message: "refused".into(), - source: None, + source: None, } .failover_eligible() ); @@ -950,7 +884,7 @@ mod tests { assert!( SdkError::Stream { message: "broken".into(), - source: None, + source: None, } .failover_eligible() ); @@ -962,7 +896,7 @@ mod tests { assert!( !SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: detail(), } .failover_eligible() @@ -970,7 +904,7 @@ mod tests { assert!( !SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, + kind: ProviderErrorKind::InvalidRequest, detail: detail(), } .failover_eligible() @@ -978,7 +912,7 @@ mod tests { assert!( !SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: detail(), } .failover_eligible() @@ -986,7 +920,7 @@ mod tests { assert!( !SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, + kind: ProviderErrorKind::ContentFilter, detail: detail(), } .failover_eligible() @@ -998,7 +932,7 @@ mod tests { assert!( !SdkError::Configuration { message: "bad".into(), - source: None, + source: None, } .failover_eligible() ); @@ -1035,7 +969,7 @@ mod tests { #[test] fn failure_signature_hint_provider_transient() { let err = SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }; assert_eq!( @@ -1044,7 +978,7 @@ mod tests { ); let err = SdkError::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail::new("500", "anthropic")), }; assert_eq!( @@ -1056,7 +990,7 @@ mod tests { #[test] fn failure_signature_hint_provider_deterministic() { let err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }; assert_eq!( @@ -1065,7 +999,7 @@ mod tests { ); let err = SdkError::Provider { - kind: ProviderErrorKind::AccessDenied, + kind: ProviderErrorKind::AccessDenied, detail: Box::new(ProviderErrorDetail::new("denied", "anthropic")), }; assert_eq!( @@ -1074,7 +1008,7 @@ mod tests { ); let err = SdkError::Provider { - kind: ProviderErrorKind::NotFound, + kind: ProviderErrorKind::NotFound, detail: Box::new(ProviderErrorDetail::new("missing", "openai")), }; assert_eq!( @@ -1083,7 +1017,7 @@ mod tests { ); let err = SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, + kind: ProviderErrorKind::InvalidRequest, detail: Box::new(ProviderErrorDetail::new("bad", "openai")), }; assert_eq!( @@ -1092,7 +1026,7 @@ mod tests { ); let err = SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, + kind: ProviderErrorKind::ContentFilter, detail: Box::new(ProviderErrorDetail::new("blocked", "openai")), }; assert_eq!( @@ -1101,7 +1035,7 @@ mod tests { ); let err = SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: Box::new(ProviderErrorDetail::new("too long", "openai")), }; assert_eq!( @@ -1110,7 +1044,7 @@ mod tests { ); let err = SdkError::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: Box::new(ProviderErrorDetail::new("out of quota", "openai")), }; assert_eq!( @@ -1124,7 +1058,7 @@ mod tests { assert_eq!( SdkError::RequestTimeout { message: "timed out".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|timeout" @@ -1132,7 +1066,7 @@ mod tests { assert_eq!( SdkError::Network { message: "refused".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|network" @@ -1140,7 +1074,7 @@ mod tests { assert_eq!( SdkError::Stream { message: "broken".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|stream" @@ -1155,7 +1089,7 @@ mod tests { assert_eq!( SdkError::Configuration { message: "bad".into(), - source: None, + source: None, } .failure_signature_hint(), "api_deterministic|unknown|configuration" diff --git a/lib/crates/fabro-server/src/error.rs b/lib/crates/fabro-server/src/error.rs index 7f93ec53f..0e675af67 100644 --- a/lib/crates/fabro-server/src/error.rs +++ b/lib/crates/fabro-server/src/error.rs @@ -53,7 +53,7 @@ pub type Result = std::result::Result; #[derive(Serialize)] struct ErrorEntry { status: String, - title: String, + title: String, detail: String, } diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 4db97e2ef..1244b759e 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -707,15 +707,15 @@ mod tests { fn apply_runtime_settings_preserves_storage_dir() { let base = SettingsLayer::default(); let args = ServeArgs { - bind: None, - model: None, - provider: None, - dry_run: false, - sandbox: None, - web: false, - no_web: false, + bind: None, + model: None, + provider: None, + dry_run: false, + sandbox: None, + web: false, + no_web: false, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = @@ -741,15 +741,15 @@ enabled = false ", ); let args = ServeArgs { - bind: None, - model: None, - provider: None, - dry_run: false, - sandbox: None, - web: true, - no_web: false, + bind: None, + model: None, + provider: None, + dry_run: false, + sandbox: None, + web: true, + no_web: false, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = apply_runtime_settings(&base, &args, false, &PathBuf::from("/srv/fabro")); @@ -768,15 +768,15 @@ enabled = false fn apply_runtime_settings_disables_web_from_cli_flag() { let base = SettingsLayer::default(); let args = ServeArgs { - bind: None, - model: None, - provider: None, - dry_run: false, - sandbox: None, - web: false, - no_web: true, + bind: None, + model: None, + provider: None, + dry_run: false, + sandbox: None, + web: false, + no_web: true, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = apply_runtime_settings(&base, &args, false, &PathBuf::from("/srv/fabro")); diff --git a/lib/crates/fabro-store/src/lib.rs b/lib/crates/fabro-store/src/lib.rs index dc6c858ca..9df7b83e9 100644 --- a/lib/crates/fabro-store/src/lib.rs +++ b/lib/crates/fabro-store/src/lib.rs @@ -17,5 +17,5 @@ pub use types::{EventEnvelope, EventPayload, RunSummary}; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct ListRunsQuery { pub start: Option>, - pub end: Option>, + pub end: Option>, } diff --git a/lib/crates/fabro-workflow/src/error.rs b/lib/crates/fabro-workflow/src/error.rs index 0af240b09..d85afbbc7 100644 --- a/lib/crates/fabro-workflow/src/error.rs +++ b/lib/crates/fabro-workflow/src/error.rs @@ -202,13 +202,13 @@ pub enum Error { #[error("Engine error: {message}")] Engine { - message: String, + message: String, failure_class: FailureCategory, }, #[error("Handler error: {message}")] Handler { - message: String, + message: String, failure_class: FailureCategory, }, @@ -308,8 +308,8 @@ impl Error { /// Build a fail `Outcome` with structured `FailureDetail`. pub fn to_fail_outcome(&self) -> Outcome { let failure = FailureDetail { - message: self.to_string(), - category: self.failure_category(), + message: self.to_string(), + category: self.failure_category(), signature: self.failure_signature_hint(), }; Outcome { @@ -393,12 +393,12 @@ mod tests { fn validation_failed_display() { let err = FabroError::ValidationFailed { diagnostics: vec![Diagnostic { - rule: "test".to_string(), + rule: "test".to_string(), severity: fabro_validate::Severity::Error, - message: "missing start node".to_string(), - node_id: None, - edge: None, - fix: None, + message: "missing start node".to_string(), + node_id: None, + edge: None, + fix: None, }], }; assert_eq!(err.to_string(), "Validation failed"); @@ -677,7 +677,7 @@ mod tests { fn llm_error_display() { let sdk_err = SdkError::Network { message: "connection refused".into(), - source: None, + source: None, }; let err = FabroError::Llm(sdk_err); assert_eq!( @@ -690,13 +690,13 @@ mod tests { fn llm_error_retryable_delegates_to_sdk() { let retryable = FabroError::Llm(SdkError::Network { message: "timeout".into(), - source: None, + source: None, }); assert!(retryable.is_retryable()); let non_retryable = FabroError::Llm(SdkError::Configuration { message: "bad config".into(), - source: None, + source: None, }); assert!(!non_retryable.is_retryable()); } @@ -705,7 +705,7 @@ mod tests { fn llm_error_from_sdk_error() { let sdk_err = SdkError::Stream { message: "broken pipe".into(), - source: None, + source: None, }; let err = FabroError::from(sdk_err); assert!(matches!(err, FabroError::Llm(_))); @@ -756,7 +756,7 @@ mod tests { #[test] fn failure_class_llm_rate_limit() { let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); @@ -765,7 +765,7 @@ mod tests { #[test] fn failure_class_llm_context_length() { let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: Box::new(ProviderErrorDetail::new("too long", "openai")), }); assert_eq!(err.failure_category(), FailureCategory::BudgetExhausted); @@ -774,7 +774,7 @@ mod tests { #[test] fn failure_class_llm_auth() { let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }); assert_eq!(err.failure_category(), FailureCategory::Deterministic); @@ -792,7 +792,7 @@ mod tests { fn failure_class_llm_timeout() { let err = FabroError::Llm(SdkError::RequestTimeout { message: "timed out".into(), - source: None, + source: None, }); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); } @@ -802,7 +802,7 @@ mod tests { #[test] fn classify_sdk_rate_limit() { let err = SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::TransientInfra); @@ -811,7 +811,7 @@ mod tests { #[test] fn classify_sdk_server() { let err = SdkError::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail::new("500", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::TransientInfra); @@ -820,7 +820,7 @@ mod tests { #[test] fn classify_sdk_context_length() { let err = SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: Box::new(ProviderErrorDetail::new("too long", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::BudgetExhausted); @@ -829,7 +829,7 @@ mod tests { #[test] fn classify_sdk_quota_exceeded() { let err = SdkError::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: Box::new(ProviderErrorDetail::new("out of quota", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::BudgetExhausted); @@ -838,7 +838,7 @@ mod tests { #[test] fn classify_sdk_auth() { let err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::Deterministic); @@ -848,7 +848,7 @@ mod tests { fn classify_sdk_request_timeout() { let err = SdkError::RequestTimeout { message: "timed out".into(), - source: None, + source: None, }; assert_eq!(classify_sdk_error(&err), FailureCategory::TransientInfra); } @@ -1493,7 +1493,7 @@ mod tests { #[test] fn failure_signature_hint_llm_returns_some() { let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }); assert_eq!( @@ -1519,7 +1519,7 @@ mod tests { #[test] fn to_fail_outcome_llm_has_class_and_signature() { let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }); let outcome = err.to_fail_outcome(); @@ -1546,7 +1546,7 @@ mod tests { fn to_fail_outcome_includes_error_message_as_reason() { let err = FabroError::Llm(SdkError::Network { message: "connection refused".into(), - source: None, + source: None, }); let outcome = err.to_fail_outcome(); assert!( @@ -1561,7 +1561,7 @@ mod tests { fn to_fail_outcome_no_context_updates() { let err = FabroError::Llm(SdkError::Network { message: "refused".into(), - source: None, + source: None, }); let outcome = err.to_fail_outcome(); assert!(outcome.context_updates.is_empty()); @@ -1605,19 +1605,19 @@ mod tests { FabroError::Validation("bad".into()), FabroError::ValidationFailed { diagnostics: vec![Diagnostic { - rule: "test".into(), + rule: "test".into(), severity: fabro_validate::Severity::Error, - message: "bad".into(), - node_id: None, - edge: None, - fix: None, + message: "bad".into(), + node_id: None, + edge: None, + fix: None, }], }, FabroError::engine("engine err"), FabroError::handler("handler err"), FabroError::Llm(SdkError::Network { message: "refused".into(), - source: None, + source: None, }), FabroError::Checkpoint("cp err".into()), FabroError::Stylesheet("style err".into()), @@ -1685,7 +1685,7 @@ mod tests { // 1. Create SdkError → FabroError let sdk_err = SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }; let arc_err = FabroError::Llm(sdk_err); @@ -1701,10 +1701,10 @@ mod tests { // 3. Outcome → StageFailed event let failure = outcome.failure.clone().unwrap(); let event = Event::StageFailed { - node_id: "code".into(), - name: "code".into(), - index: 0, - failure: failure.clone(), + node_id: "code".into(), + name: "code".into(), + index: 0, + failure: failure.clone(), will_retry: false, }; @@ -1770,7 +1770,7 @@ mod tests { use fabro_agent::Error as AgentError; let err = AgentError::Llm(SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }); let json = serde_json::to_string(&err).unwrap(); diff --git a/lib/crates/fabro-workflow/src/lib.rs b/lib/crates/fabro-workflow/src/lib.rs index ab563a789..9fd4d9928 100644 --- a/lib/crates/fabro-workflow/src/lib.rs +++ b/lib/crates/fabro-workflow/src/lib.rs @@ -72,15 +72,15 @@ pub fn build_completed_stages(cp: &records::Checkpoint, run_failed: bool) -> Vec last.failed = true; } else { stages.push(CompletedStage { - node_id: "unknown".to_string(), - status: "fail".to_string(), - succeeded: false, - failed: true, - retries: 0, + node_id: "unknown".to_string(), + status: "fail".to_string(), + succeeded: false, + failed: true, + retries: 0, billing_usd_micros: None, - notes: None, - failure_reason: None, - files_touched: vec![], + notes: None, + failure_reason: None, + files_touched: vec![], }); } }