1use thiserror::Error;
8
9#[derive(Error, Debug)]
15#[non_exhaustive]
16pub enum ClawftError {
17 #[error("retry required: {source} (attempt {attempts})")]
20 Retry {
21 #[source]
23 source: Box<dyn std::error::Error + Send + Sync>,
24 attempts: u32,
26 },
27
28 #[error("operation timed out: {operation}")]
30 Timeout {
31 operation: String,
33 },
34
35 #[error("provider error: {message}")]
37 Provider {
38 message: String,
40 },
41
42 #[error("rate limited: retry after {retry_after_ms}ms")]
44 RateLimited {
45 retry_after_ms: u64,
47 },
48
49 #[error("invalid config: {reason}")]
52 ConfigInvalid {
53 reason: String,
55 },
56
57 #[error("failed to load plugin: {plugin}")]
59 PluginLoadFailed {
60 plugin: String,
62 },
63
64 #[error("io error: {0}")]
66 Io(#[from] std::io::Error),
67
68 #[error("json error: {0}")]
70 Json(#[from] serde_json::Error),
71
72 #[error("channel error: {0}")]
74 Channel(String),
75
76 #[error("security violation: {reason}")]
78 SecurityViolation {
79 reason: String,
81 },
82}
83
84#[derive(Error, Debug)]
89#[non_exhaustive]
90pub enum ChannelError {
91 #[error("connection failed: {0}")]
93 ConnectionFailed(String),
94
95 #[error("authentication failed: {0}")]
97 AuthFailed(String),
98
99 #[error("send failed: {0}")]
101 SendFailed(String),
102
103 #[error("receive failed: {0}")]
105 ReceiveFailed(String),
106
107 #[error("not connected")]
109 NotConnected,
110
111 #[error("channel not found: {0}")]
113 NotFound(String),
114
115 #[error("{0}")]
117 Other(String),
118}
119
120pub type Result<T> = std::result::Result<T, ClawftError>;
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn clawft_error_display() {
129 let err = ClawftError::Timeout {
130 operation: "llm_call".into(),
131 };
132 assert_eq!(err.to_string(), "operation timed out: llm_call");
133 }
134
135 #[test]
136 fn clawft_error_from_io() {
137 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
138 let err: ClawftError = io_err.into();
139 assert!(matches!(err, ClawftError::Io(_)));
140 assert!(err.to_string().contains("missing"));
141 }
142
143 #[test]
144 fn clawft_error_from_json() {
145 let json_err = serde_json::from_str::<serde_json::Value>("{{bad}}").unwrap_err();
146 let err: ClawftError = json_err.into();
147 assert!(matches!(err, ClawftError::Json(_)));
148 }
149
150 #[test]
151 fn channel_error_display() {
152 let err = ChannelError::NotConnected;
153 assert_eq!(err.to_string(), "not connected");
154
155 let err = ChannelError::AuthFailed("bad token".into());
156 assert_eq!(err.to_string(), "authentication failed: bad token");
157 }
158
159 #[test]
160 fn retry_error_preserves_source() {
161 let source: Box<dyn std::error::Error + Send + Sync> = "transient".into();
162 let err = ClawftError::Retry {
163 source,
164 attempts: 3,
165 };
166 assert!(err.to_string().contains("attempt 3"));
167 assert!(err.to_string().contains("transient"));
168 }
169
170 #[test]
171 fn security_violation_display() {
172 let err = ClawftError::SecurityViolation {
173 reason: "path traversal detected".into(),
174 };
175 assert_eq!(
176 err.to_string(),
177 "security violation: path traversal detected"
178 );
179 }
180
181 #[test]
182 fn result_alias_works() {
183 fn ok_fn() -> Result<i32> {
184 Ok(42)
185 }
186 fn err_fn() -> Result<i32> {
187 Err(ClawftError::Provider {
188 message: "boom".into(),
189 })
190 }
191 assert_eq!(ok_fn().unwrap(), 42);
192 assert!(err_fn().is_err());
193 }
194}