mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-12 23:02:41 +00:00
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:
parent
dd44220bc3
commit
952e0831e8
15 changed files with 276 additions and 342 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue