use thiserror::Error;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ClawftError {
#[error("retry required: {source} (attempt {attempts})")]
Retry {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
attempts: u32,
},
#[error("operation timed out: {operation}")]
Timeout {
operation: String,
},
#[error("provider error: {message}")]
Provider {
message: String,
},
#[error("rate limited: retry after {retry_after_ms}ms")]
RateLimited {
retry_after_ms: u64,
},
#[error("invalid config: {reason}")]
ConfigInvalid {
reason: String,
},
#[error("failed to load plugin: {plugin}")]
PluginLoadFailed {
plugin: String,
},
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("channel error: {0}")]
Channel(String),
#[error("security violation: {reason}")]
SecurityViolation {
reason: String,
},
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ChannelError {
#[error("connection failed: {0}")]
ConnectionFailed(String),
#[error("authentication failed: {0}")]
AuthFailed(String),
#[error("send failed: {0}")]
SendFailed(String),
#[error("receive failed: {0}")]
ReceiveFailed(String),
#[error("not connected")]
NotConnected,
#[error("channel not found: {0}")]
NotFound(String),
#[error("{0}")]
Other(String),
}
pub type Result<T> = std::result::Result<T, ClawftError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clawft_error_display() {
let err = ClawftError::Timeout {
operation: "llm_call".into(),
};
assert_eq!(err.to_string(), "operation timed out: llm_call");
}
#[test]
fn clawft_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
let err: ClawftError = io_err.into();
assert!(matches!(err, ClawftError::Io(_)));
assert!(err.to_string().contains("missing"));
}
#[test]
fn clawft_error_from_json() {
let json_err = serde_json::from_str::<serde_json::Value>("{{bad}}").unwrap_err();
let err: ClawftError = json_err.into();
assert!(matches!(err, ClawftError::Json(_)));
}
#[test]
fn channel_error_display() {
let err = ChannelError::NotConnected;
assert_eq!(err.to_string(), "not connected");
let err = ChannelError::AuthFailed("bad token".into());
assert_eq!(err.to_string(), "authentication failed: bad token");
}
#[test]
fn retry_error_preserves_source() {
let source: Box<dyn std::error::Error + Send + Sync> = "transient".into();
let err = ClawftError::Retry {
source,
attempts: 3,
};
assert!(err.to_string().contains("attempt 3"));
assert!(err.to_string().contains("transient"));
}
#[test]
fn security_violation_display() {
let err = ClawftError::SecurityViolation {
reason: "path traversal detected".into(),
};
assert_eq!(
err.to_string(),
"security violation: path traversal detected"
);
}
#[test]
fn result_alias_works() {
fn ok_fn() -> Result<i32> {
Ok(42)
}
fn err_fn() -> Result<i32> {
Err(ClawftError::Provider {
message: "boom".into(),
})
}
assert_eq!(ok_fn().unwrap(), 42);
assert!(err_fn().is_err());
}
}