fmt: apply nightly rustfmt and document nightly requirement

The rustfmt.toml uses nightly-only options (struct_field_align_threshold,
imports_granularity, etc.) so stable rustfmt silently skips them,
producing different output. Use cargo +nightly fmt going forward.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-11 12:22:51 -04:00
parent dd44220bc3
commit 952e0831e8
No known key found for this signature in database
15 changed files with 276 additions and 342 deletions

View file

@ -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.

View file

@ -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<Error> = 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();

View file

@ -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,
};

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,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));
}

View file

@ -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 {

View file

@ -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<PathBuf>,
path: Option<PathBuf>,
#[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<ResolveError>,
errors: Vec<ResolveError>,
},
#[error("missing required environment variable {var} for {field}")]
MissingEnvVar {
field: String,
var: String,
field: String,
var: String,
#[source]
source: std::env::VarError,
},

View file

@ -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<SettingsLayer>,
pub workflow_toml_path: Option<PathBuf>,
pub workflow_slug: Option<String>,
pub dot_path: PathBuf,
pub workflow_config: Option<SettingsLayer>,
pub workflow_toml_path: Option<PathBuf>,
pub workflow_slug: Option<String>,
}
/// 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<String>,
pub name: String,
pub goal: Option<String>,
pub source: WorkflowSource,
}

View file

@ -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 } => {

View file

@ -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<FailureCategory>,
pub category: Option<FailureCategory>,
pub signature: Option<String>,
}
@ -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();

View file

@ -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<u16>,
pub error_code: Option<String>,
pub error_code: Option<String>,
pub retry_after: Option<f64>,
pub raw: Option<serde_json::Value>,
pub raw: Option<serde_json::Value>,
}
impl ProviderErrorDetail {
pub fn new(message: impl Into<String>, provider: impl Into<String>) -> 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<ProviderErrorDetail>,
},
@ -67,7 +67,7 @@ pub enum Error {
message: String,
#[source]
#[serde(skip)]
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
},
#[error("Request interrupted: {message}")]
@ -78,7 +78,7 @@ pub enum Error {
message: String,
#[source]
#[serde(skip)]
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
},
#[error("Stream error: {message}")]
@ -86,7 +86,7 @@ pub enum Error {
message: String,
#[source]
#[serde(skip)]
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
},
#[error("Invalid tool call: {message}")]
@ -100,7 +100,7 @@ pub enum Error {
message: String,
#[source]
#[serde(skip)]
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
},
#[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"

View file

@ -53,7 +53,7 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Serialize)]
struct ErrorEntry {
status: String,
title: String,
title: String,
detail: String,
}

View file

@ -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"));

View file

@ -17,5 +17,5 @@ pub use types::{EventEnvelope, EventPayload, RunSummary};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ListRunsQuery {
pub start: Option<DateTime<Utc>>,
pub end: Option<DateTime<Utc>>,
pub end: Option<DateTime<Utc>>,
}

View file

@ -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();

View file

@ -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![],
});
}
}