gemini_genai_rs/session/
errors.rs1use super::state::SessionPhase;
7use thiserror::Error;
8
9#[derive(Debug, Error, Clone)]
11pub enum SessionError {
12 #[error("WebSocket error: {0}")]
14 WebSocket(#[source] WebSocketError),
15
16 #[error("Timeout in {phase} after {elapsed:?}")]
18 Timeout {
19 phase: SessionPhase,
21 elapsed: std::time::Duration,
23 },
24
25 #[error("Invalid transition from {from} to {to}")]
27 InvalidTransition {
28 from: SessionPhase,
30 to: SessionPhase,
32 },
33
34 #[error("Not connected")]
36 NotConnected,
37
38 #[error("Setup failed: {0}")]
40 SetupFailed(#[source] SetupError),
41
42 #[error("Codec error: {0}")]
44 Codec(#[source] crate::transport::CodecError),
45
46 #[error("Server sent GoAway (time left: {time_left:?})")]
48 GoAway {
49 time_left: Option<std::time::Duration>,
51 },
52
53 #[error("Internal channel closed")]
55 ChannelClosed,
56
57 #[error("Send queue full")]
59 SendQueueFull,
60
61 #[error("Auth error: {0}")]
63 Auth(#[source] AuthError),
64}
65
66#[derive(Debug, Error, Clone)]
68pub enum WebSocketError {
69 #[error("Connection refused: {0}")]
71 ConnectionRefused(String),
72
73 #[error("Protocol error: {0}")]
75 ProtocolError(String),
76
77 #[error("Connection closed (code={code}, reason={reason})")]
79 Closed {
80 code: u16,
82 reason: String,
84 },
85}
86
87#[derive(Debug, Error, Clone)]
89pub enum SetupError {
90 #[error("Invalid model: {0}")]
92 InvalidModel(String),
93
94 #[error("Authentication failed: {0}")]
96 AuthenticationFailed(String),
97
98 #[error("Server rejected: {message}")]
100 ServerRejected {
101 code: Option<String>,
103 message: String,
105 },
106
107 #[error("Setup timed out")]
109 Timeout,
110}
111
112#[derive(Debug, Error, Clone)]
114pub enum AuthError {
115 #[error("Token expired")]
117 TokenExpired,
118
119 #[error("Token fetch failed: {0}")]
121 TokenFetchFailed(String),
122
123 #[error("Insufficient scopes: {0}")]
125 InsufficientScopes(String),
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use std::time::Duration;
132
133 #[test]
138 fn websocket_error_connection_refused_display() {
139 let err = WebSocketError::ConnectionRefused("host unreachable".into());
140 assert_eq!(err.to_string(), "Connection refused: host unreachable");
141 }
142
143 #[test]
144 fn websocket_error_protocol_error_display() {
145 let err = WebSocketError::ProtocolError("invalid frame".into());
146 assert_eq!(err.to_string(), "Protocol error: invalid frame");
147 }
148
149 #[test]
150 fn websocket_error_closed_display() {
151 let err = WebSocketError::Closed {
152 code: 1006,
153 reason: "abnormal closure".into(),
154 };
155 assert_eq!(
156 err.to_string(),
157 "Connection closed (code=1006, reason=abnormal closure)"
158 );
159 }
160
161 #[test]
166 fn setup_error_invalid_model_display() {
167 let err = SetupError::InvalidModel("no-such-model".into());
168 assert_eq!(err.to_string(), "Invalid model: no-such-model");
169 }
170
171 #[test]
172 fn setup_error_authentication_failed_display() {
173 let err = SetupError::AuthenticationFailed("bad token".into());
174 assert_eq!(err.to_string(), "Authentication failed: bad token");
175 }
176
177 #[test]
178 fn setup_error_server_rejected_display() {
179 let err = SetupError::ServerRejected {
180 code: Some("400".into()),
181 message: "invalid config".into(),
182 };
183 assert_eq!(err.to_string(), "Server rejected: invalid config");
184 }
185
186 #[test]
187 fn setup_error_server_rejected_no_code_display() {
188 let err = SetupError::ServerRejected {
189 code: None,
190 message: "closed during setup".into(),
191 };
192 assert_eq!(err.to_string(), "Server rejected: closed during setup");
193 }
194
195 #[test]
196 fn setup_error_timeout_display() {
197 let err = SetupError::Timeout;
198 assert_eq!(err.to_string(), "Setup timed out");
199 }
200
201 #[test]
206 fn auth_error_token_expired_display() {
207 let err = AuthError::TokenExpired;
208 assert_eq!(err.to_string(), "Token expired");
209 }
210
211 #[test]
212 fn auth_error_token_fetch_failed_display() {
213 let err = AuthError::TokenFetchFailed("network error".into());
214 assert_eq!(err.to_string(), "Token fetch failed: network error");
215 }
216
217 #[test]
218 fn auth_error_insufficient_scopes_display() {
219 let err = AuthError::InsufficientScopes("cloud-platform".into());
220 assert_eq!(err.to_string(), "Insufficient scopes: cloud-platform");
221 }
222
223 #[test]
228 fn session_error_websocket_display() {
229 let err =
230 SessionError::WebSocket(WebSocketError::ConnectionRefused("host unreachable".into()));
231 assert_eq!(
232 err.to_string(),
233 "WebSocket error: Connection refused: host unreachable"
234 );
235 }
236
237 #[test]
238 fn session_error_timeout_display() {
239 let err = SessionError::Timeout {
240 phase: SessionPhase::SetupSent,
241 elapsed: Duration::from_secs(15),
242 };
243 assert_eq!(err.to_string(), "Timeout in SetupSent after 15s");
244 }
245
246 #[test]
247 fn session_error_timeout_connecting_display() {
248 let err = SessionError::Timeout {
249 phase: SessionPhase::Connecting,
250 elapsed: Duration::from_secs(10),
251 };
252 assert_eq!(err.to_string(), "Timeout in Connecting after 10s");
253 }
254
255 #[test]
256 fn session_error_setup_failed_display() {
257 let err = SessionError::SetupFailed(SetupError::AuthenticationFailed("bad token".into()));
258 assert_eq!(
259 err.to_string(),
260 "Setup failed: Authentication failed: bad token"
261 );
262 }
263
264 #[test]
265 fn session_error_go_away_with_time_display() {
266 let err = SessionError::GoAway {
267 time_left: Some(Duration::from_secs(30)),
268 };
269 assert_eq!(err.to_string(), "Server sent GoAway (time left: Some(30s))");
270 }
271
272 #[test]
273 fn session_error_go_away_no_time_display() {
274 let err = SessionError::GoAway { time_left: None };
275 assert_eq!(err.to_string(), "Server sent GoAway (time left: None)");
276 }
277
278 #[test]
279 fn session_error_auth_display() {
280 let err = SessionError::Auth(AuthError::TokenExpired);
281 assert_eq!(err.to_string(), "Auth error: Token expired");
282 }
283
284 #[test]
285 fn session_error_not_connected_display() {
286 let err = SessionError::NotConnected;
287 assert_eq!(err.to_string(), "Not connected");
288 }
289
290 #[test]
291 fn session_error_channel_closed_display() {
292 let err = SessionError::ChannelClosed;
293 assert_eq!(err.to_string(), "Internal channel closed");
294 }
295
296 #[test]
297 fn session_error_send_queue_full_display() {
298 let err = SessionError::SendQueueFull;
299 assert_eq!(err.to_string(), "Send queue full");
300 }
301
302 #[test]
303 fn session_error_invalid_transition_display() {
304 let err = SessionError::InvalidTransition {
305 from: SessionPhase::Active,
306 to: SessionPhase::SetupSent,
307 };
308 assert_eq!(
309 err.to_string(),
310 "Invalid transition from Active to SetupSent"
311 );
312 }
313
314 #[test]
319 fn error_types_are_clone() {
320 let ws_err = WebSocketError::ProtocolError("test".into());
321 let _ = ws_err.clone();
322
323 let setup_err = SetupError::InvalidModel("test".into());
324 let _ = setup_err.clone();
325
326 let auth_err = AuthError::TokenExpired;
327 let _ = auth_err.clone();
328
329 let session_err = SessionError::WebSocket(WebSocketError::ProtocolError("test".into()));
330 let _ = session_err.clone();
331 }
332}
333
334#[cfg(test)]
335mod source_chain {
336 use super::*;
340 use std::error::Error as _;
341
342 #[test]
343 fn websocket_setup_and_auth_variants_expose_their_source() {
344 let ws = SessionError::WebSocket(WebSocketError::ConnectionRefused("x".into()));
345 assert!(
346 ws.source().is_some(),
347 "WebSocket variant must expose its source"
348 );
349 let auth = SessionError::Auth(AuthError::TokenExpired);
350 assert!(
351 auth.source().is_some(),
352 "Auth variant must expose its source"
353 );
354 }
355}