gemini_genai_rs/session/
errors.rs

1//! Session error types.
2//!
3//! Structured error hierarchy: [`SessionError`] wraps [`WebSocketError`],
4//! [`SetupError`], and [`AuthError`] for fine-grained matching.
5
6use super::state::SessionPhase;
7use thiserror::Error;
8
9/// Errors that can occur during a session.
10#[derive(Debug, Error, Clone)]
11pub enum SessionError {
12    /// WebSocket-level error (transient, may be retried).
13    #[error("WebSocket error: {0}")]
14    WebSocket(#[source] WebSocketError),
15
16    /// Timeout waiting for handshake or setup.
17    #[error("Timeout in {phase} after {elapsed:?}")]
18    Timeout {
19        /// Which phase timed out.
20        phase: SessionPhase,
21        /// How long was waited before timing out.
22        elapsed: std::time::Duration,
23    },
24
25    /// Attempted an invalid phase transition.
26    #[error("Invalid transition from {from} to {to}")]
27    InvalidTransition {
28        /// Phase the session was in.
29        from: SessionPhase,
30        /// Phase the transition attempted to reach.
31        to: SessionPhase,
32    },
33
34    /// Operation requires an active connection but session is not connected.
35    #[error("Not connected")]
36    NotConnected,
37
38    /// Server rejected the setup configuration.
39    #[error("Setup failed: {0}")]
40    SetupFailed(#[source] SetupError),
41
42    /// A client message could not be encoded, or a server message decoded.
43    #[error("Codec error: {0}")]
44    Codec(#[source] crate::transport::CodecError),
45
46    /// Server requested graceful disconnect.
47    #[error("Server sent GoAway (time left: {time_left:?})")]
48    GoAway {
49        /// Time remaining before forced disconnect.
50        time_left: Option<std::time::Duration>,
51    },
52
53    /// Internal channel was closed unexpectedly.
54    #[error("Internal channel closed")]
55    ChannelClosed,
56
57    /// Send queue is full.
58    #[error("Send queue full")]
59    SendQueueFull,
60
61    /// Authentication error.
62    #[error("Auth error: {0}")]
63    Auth(#[source] AuthError),
64}
65
66/// WebSocket-level errors with structured detail.
67#[derive(Debug, Error, Clone)]
68pub enum WebSocketError {
69    /// Remote server refused the connection.
70    #[error("Connection refused: {0}")]
71    ConnectionRefused(String),
72
73    /// Protocol-level WebSocket error (frame errors, encoding, etc.).
74    #[error("Protocol error: {0}")]
75    ProtocolError(String),
76
77    /// Connection was closed with a status code and reason.
78    #[error("Connection closed (code={code}, reason={reason})")]
79    Closed {
80        /// WebSocket close status code.
81        code: u16,
82        /// Human-readable close reason.
83        reason: String,
84    },
85}
86
87/// Errors during the setup handshake phase.
88#[derive(Debug, Error, Clone)]
89pub enum SetupError {
90    /// The specified model was invalid or not found.
91    #[error("Invalid model: {0}")]
92    InvalidModel(String),
93
94    /// Authentication failed during setup.
95    #[error("Authentication failed: {0}")]
96    AuthenticationFailed(String),
97
98    /// Server rejected the setup request.
99    #[error("Server rejected: {message}")]
100    ServerRejected {
101        /// Optional error code from the server.
102        code: Option<String>,
103        /// Error message from the server.
104        message: String,
105    },
106
107    /// Setup timed out before receiving setupComplete.
108    #[error("Setup timed out")]
109    Timeout,
110}
111
112/// Authentication-specific errors.
113#[derive(Debug, Error, Clone)]
114pub enum AuthError {
115    /// The bearer token has expired.
116    #[error("Token expired")]
117    TokenExpired,
118
119    /// Failed to fetch a fresh token.
120    #[error("Token fetch failed: {0}")]
121    TokenFetchFailed(String),
122
123    /// Token lacks required scopes.
124    #[error("Insufficient scopes: {0}")]
125    InsufficientScopes(String),
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use std::time::Duration;
132
133    // -----------------------------------------------------------------------
134    // WebSocketError Display tests
135    // -----------------------------------------------------------------------
136
137    #[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    // -----------------------------------------------------------------------
162    // SetupError Display tests
163    // -----------------------------------------------------------------------
164
165    #[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    // -----------------------------------------------------------------------
202    // AuthError Display tests
203    // -----------------------------------------------------------------------
204
205    #[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    // -----------------------------------------------------------------------
224    // SessionError Display tests
225    // -----------------------------------------------------------------------
226
227    #[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    // -----------------------------------------------------------------------
315    // Clone tests (ensure all error types are Clone)
316    // -----------------------------------------------------------------------
317
318    #[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    //! The structured inner errors must be reachable through
337    //! `std::error::Error::source()`, otherwise the L2 → L1 → L0 chain
338    //! dead-ends at the top L0 variant and callers cannot walk to the cause.
339    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}