use std::fmt;
use thiserror::Error;
#[derive(Debug)]
pub enum ProviderError {
Auth(String),
RateLimit(String),
Billing(String),
ServerError(String),
InvalidRequest(String),
ModelNotFound(String),
Timeout(String),
Unknown(String),
Overloaded(String),
Format(String),
}
impl fmt::Display for ProviderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProviderError::Auth(msg) => write!(f, "Authentication error: {}", msg),
ProviderError::RateLimit(msg) => write!(f, "Rate limit error: {}", msg),
ProviderError::Billing(msg) => write!(f, "Billing error: {}", msg),
ProviderError::ServerError(msg) => write!(f, "Server error: {}", msg),
ProviderError::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
ProviderError::ModelNotFound(msg) => write!(f, "Model not found: {}", msg),
ProviderError::Timeout(msg) => write!(f, "Timeout: {}", msg),
ProviderError::Unknown(msg) => write!(f, "Unknown provider error: {}", msg),
ProviderError::Overloaded(msg) => write!(f, "Overloaded error: {}", msg),
ProviderError::Format(msg) => write!(f, "Format error: {}", msg),
}
}
}
impl ProviderError {
pub fn is_retryable(&self) -> bool {
matches!(
self,
ProviderError::RateLimit(_)
| ProviderError::ServerError(_)
| ProviderError::Timeout(_)
| ProviderError::Overloaded(_)
)
}
pub fn should_fallback(&self) -> bool {
!matches!(
self,
ProviderError::Auth(_)
| ProviderError::InvalidRequest(_)
| ProviderError::Billing(_)
| ProviderError::Format(_)
)
}
pub fn status_code(&self) -> Option<u16> {
match self {
ProviderError::Auth(_) => Some(401),
ProviderError::RateLimit(_) => Some(429),
ProviderError::Billing(_) => Some(402),
ProviderError::ServerError(_) => Some(500),
ProviderError::InvalidRequest(_) => Some(400),
ProviderError::ModelNotFound(_) => Some(404),
ProviderError::Timeout(_) => None,
ProviderError::Overloaded(_) => Some(503),
ProviderError::Format(_) => Some(400),
ProviderError::Unknown(_) => None,
}
}
}
impl From<ProviderError> for ZeptoError {
fn from(err: ProviderError) -> Self {
ZeptoError::ProviderTyped(err)
}
}
#[derive(Error, Debug)]
pub enum ZeptoError {
#[error("Configuration error: {0}")]
Config(String),
#[error("Provider error: {0}")]
Provider(String),
#[error("Provider error: {0}")]
ProviderTyped(ProviderError),
#[error("Channel error: {0}")]
Channel(String),
#[error("Tool error: {0}")]
Tool(String),
#[error("Session error: {0}")]
Session(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Bus error: channel closed")]
BusClosed,
#[error("Not found: {0}")]
NotFound(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Security violation: {0}")]
SecurityViolation(String),
#[error("Safety violation: {0}")]
Safety(String),
#[error("MCP error: {0}")]
Mcp(String),
#[error("Quota exceeded: {0}")]
QuotaExceeded(String),
#[error("Quota rejected: {0}")]
QuotaRejected(String),
}
pub type Result<T> = std::result::Result<T, ZeptoError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = ZeptoError::Config("missing API key".to_string());
assert_eq!(err.to_string(), "Configuration error: missing API key");
}
#[test]
fn test_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let zepto_err: ZeptoError = io_err.into();
assert!(matches!(zepto_err, ZeptoError::Io(_)));
}
#[test]
fn test_result_type() {
fn returns_result() -> Result<i32> {
Ok(42)
}
assert_eq!(returns_result().unwrap(), 42);
}
#[test]
fn test_error_variants() {
let _ = ZeptoError::Config("test".into());
let _ = ZeptoError::Provider("test".into());
let _ = ZeptoError::ProviderTyped(ProviderError::Auth("test".into()));
let _ = ZeptoError::Channel("test".into());
let _ = ZeptoError::Tool("test".into());
let _ = ZeptoError::Session("test".into());
let _ = ZeptoError::BusClosed;
let _ = ZeptoError::NotFound("test".into());
let _ = ZeptoError::Unauthorized("test".into());
let _ = ZeptoError::SecurityViolation("test".into());
let _ = ZeptoError::Safety("test".into());
let _ = ZeptoError::Mcp("test".into());
let _ = ZeptoError::QuotaExceeded("test".into());
let _ = ZeptoError::QuotaRejected("test".into());
}
#[test]
fn test_security_violation_display() {
let err = ZeptoError::SecurityViolation("path traversal attempt detected".to_string());
assert_eq!(
err.to_string(),
"Security violation: path traversal attempt detected"
);
}
#[test]
fn test_provider_error_display() {
assert!(ProviderError::Auth("bad key".into())
.to_string()
.contains("Authentication error"));
assert!(ProviderError::RateLimit("quota".into())
.to_string()
.contains("Rate limit error"));
assert!(ProviderError::Billing("no funds".into())
.to_string()
.contains("Billing error"));
assert!(ProviderError::ServerError("500".into())
.to_string()
.contains("Server error"));
assert!(ProviderError::InvalidRequest("bad json".into())
.to_string()
.contains("Invalid request"));
assert!(ProviderError::ModelNotFound("gpt-99".into())
.to_string()
.contains("Model not found"));
assert!(ProviderError::Timeout("30s".into())
.to_string()
.contains("Timeout"));
assert!(ProviderError::Unknown("???".into())
.to_string()
.contains("Unknown provider error"));
assert!(ProviderError::Overloaded("busy".into())
.to_string()
.contains("Overloaded error"));
assert!(ProviderError::Format("bad id".into())
.to_string()
.contains("Format error"));
}
#[test]
fn test_provider_error_is_retryable() {
assert!(ProviderError::RateLimit("429".into()).is_retryable());
assert!(ProviderError::ServerError("500".into()).is_retryable());
assert!(ProviderError::Timeout("timeout".into()).is_retryable());
assert!(ProviderError::Overloaded("busy".into()).is_retryable());
assert!(!ProviderError::Auth("401".into()).is_retryable());
assert!(!ProviderError::Billing("402".into()).is_retryable());
assert!(!ProviderError::InvalidRequest("400".into()).is_retryable());
assert!(!ProviderError::ModelNotFound("404".into()).is_retryable());
assert!(!ProviderError::Unknown("???".into()).is_retryable());
assert!(!ProviderError::Format("bad id".into()).is_retryable());
}
#[test]
fn test_provider_error_should_fallback() {
assert!(ProviderError::RateLimit("429".into()).should_fallback());
assert!(ProviderError::ServerError("500".into()).should_fallback());
assert!(ProviderError::Timeout("timeout".into()).should_fallback());
assert!(ProviderError::ModelNotFound("404".into()).should_fallback());
assert!(ProviderError::Unknown("???".into()).should_fallback());
assert!(ProviderError::Overloaded("busy".into()).should_fallback());
assert!(!ProviderError::Auth("401".into()).should_fallback());
assert!(!ProviderError::InvalidRequest("400".into()).should_fallback());
assert!(!ProviderError::Billing("402".into()).should_fallback());
assert!(!ProviderError::Format("bad id".into()).should_fallback());
}
#[test]
fn test_provider_error_status_code() {
assert_eq!(ProviderError::Auth("x".into()).status_code(), Some(401));
assert_eq!(
ProviderError::RateLimit("x".into()).status_code(),
Some(429)
);
assert_eq!(ProviderError::Billing("x".into()).status_code(), Some(402));
assert_eq!(
ProviderError::ServerError("x".into()).status_code(),
Some(500)
);
assert_eq!(
ProviderError::InvalidRequest("x".into()).status_code(),
Some(400)
);
assert_eq!(
ProviderError::ModelNotFound("x".into()).status_code(),
Some(404)
);
assert_eq!(ProviderError::Timeout("x".into()).status_code(), None);
assert_eq!(
ProviderError::Overloaded("x".into()).status_code(),
Some(503)
);
assert_eq!(ProviderError::Format("x".into()).status_code(), Some(400));
assert_eq!(ProviderError::Unknown("x".into()).status_code(), None);
}
#[test]
fn test_provider_error_into_zepto_error() {
let pe = ProviderError::RateLimit("too fast".into());
let ze: ZeptoError = pe.into();
assert!(matches!(ze, ZeptoError::ProviderTyped(_)));
assert!(ze.to_string().contains("Rate limit error"));
}
#[test]
fn test_provider_typed_display() {
let err = ZeptoError::ProviderTyped(ProviderError::Auth("invalid key".into()));
assert_eq!(
err.to_string(),
"Provider error: Authentication error: invalid key"
);
}
#[test]
fn test_quota_exceeded_error_display() {
let err = ZeptoError::QuotaExceeded("anthropic monthly $50.00 exceeded".to_string());
assert_eq!(
err.to_string(),
"Quota exceeded: anthropic monthly $50.00 exceeded"
);
}
}