Skip to main content

rs_genai/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(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(SetupError),
41
42    /// Server requested graceful disconnect.
43    #[error("Server sent GoAway (time left: {time_left:?})")]
44    GoAway {
45        /// Time remaining before forced disconnect.
46        time_left: Option<std::time::Duration>,
47    },
48
49    /// Internal channel was closed unexpectedly.
50    #[error("Internal channel closed")]
51    ChannelClosed,
52
53    /// Send queue is full.
54    #[error("Send queue full")]
55    SendQueueFull,
56
57    /// Authentication error.
58    #[error("Auth error: {0}")]
59    Auth(AuthError),
60}
61
62/// WebSocket-level errors with structured detail.
63#[derive(Debug, Error, Clone)]
64pub enum WebSocketError {
65    /// Remote server refused the connection.
66    #[error("Connection refused: {0}")]
67    ConnectionRefused(String),
68
69    /// Protocol-level WebSocket error (frame errors, encoding, etc.).
70    #[error("Protocol error: {0}")]
71    ProtocolError(String),
72
73    /// Connection was closed with a status code and reason.
74    #[error("Connection closed (code={code}, reason={reason})")]
75    Closed {
76        /// WebSocket close status code.
77        code: u16,
78        /// Human-readable close reason.
79        reason: String,
80    },
81}
82
83/// Errors during the setup handshake phase.
84#[derive(Debug, Error, Clone)]
85pub enum SetupError {
86    /// The specified model was invalid or not found.
87    #[error("Invalid model: {0}")]
88    InvalidModel(String),
89
90    /// Authentication failed during setup.
91    #[error("Authentication failed: {0}")]
92    AuthenticationFailed(String),
93
94    /// Server rejected the setup request.
95    #[error("Server rejected: {message}")]
96    ServerRejected {
97        /// Optional error code from the server.
98        code: Option<String>,
99        /// Error message from the server.
100        message: String,
101    },
102
103    /// Setup timed out before receiving setupComplete.
104    #[error("Setup timed out")]
105    Timeout,
106}
107
108/// Authentication-specific errors.
109#[derive(Debug, Error, Clone)]
110pub enum AuthError {
111    /// The bearer token has expired.
112    #[error("Token expired")]
113    TokenExpired,
114
115    /// Failed to fetch a fresh token.
116    #[error("Token fetch failed: {0}")]
117    TokenFetchFailed(String),
118
119    /// Token lacks required scopes.
120    #[error("Insufficient scopes: {0}")]
121    InsufficientScopes(String),
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use std::time::Duration;
128
129    // -----------------------------------------------------------------------
130    // WebSocketError Display tests
131    // -----------------------------------------------------------------------
132
133    #[test]
134    fn websocket_error_connection_refused_display() {
135        let err = WebSocketError::ConnectionRefused("host unreachable".into());
136        assert_eq!(err.to_string(), "Connection refused: host unreachable");
137    }
138
139    #[test]
140    fn websocket_error_protocol_error_display() {
141        let err = WebSocketError::ProtocolError("invalid frame".into());
142        assert_eq!(err.to_string(), "Protocol error: invalid frame");
143    }
144
145    #[test]
146    fn websocket_error_closed_display() {
147        let err = WebSocketError::Closed {
148            code: 1006,
149            reason: "abnormal closure".into(),
150        };
151        assert_eq!(
152            err.to_string(),
153            "Connection closed (code=1006, reason=abnormal closure)"
154        );
155    }
156
157    // -----------------------------------------------------------------------
158    // SetupError Display tests
159    // -----------------------------------------------------------------------
160
161    #[test]
162    fn setup_error_invalid_model_display() {
163        let err = SetupError::InvalidModel("no-such-model".into());
164        assert_eq!(err.to_string(), "Invalid model: no-such-model");
165    }
166
167    #[test]
168    fn setup_error_authentication_failed_display() {
169        let err = SetupError::AuthenticationFailed("bad token".into());
170        assert_eq!(err.to_string(), "Authentication failed: bad token");
171    }
172
173    #[test]
174    fn setup_error_server_rejected_display() {
175        let err = SetupError::ServerRejected {
176            code: Some("400".into()),
177            message: "invalid config".into(),
178        };
179        assert_eq!(err.to_string(), "Server rejected: invalid config");
180    }
181
182    #[test]
183    fn setup_error_server_rejected_no_code_display() {
184        let err = SetupError::ServerRejected {
185            code: None,
186            message: "closed during setup".into(),
187        };
188        assert_eq!(err.to_string(), "Server rejected: closed during setup");
189    }
190
191    #[test]
192    fn setup_error_timeout_display() {
193        let err = SetupError::Timeout;
194        assert_eq!(err.to_string(), "Setup timed out");
195    }
196
197    // -----------------------------------------------------------------------
198    // AuthError Display tests
199    // -----------------------------------------------------------------------
200
201    #[test]
202    fn auth_error_token_expired_display() {
203        let err = AuthError::TokenExpired;
204        assert_eq!(err.to_string(), "Token expired");
205    }
206
207    #[test]
208    fn auth_error_token_fetch_failed_display() {
209        let err = AuthError::TokenFetchFailed("network error".into());
210        assert_eq!(err.to_string(), "Token fetch failed: network error");
211    }
212
213    #[test]
214    fn auth_error_insufficient_scopes_display() {
215        let err = AuthError::InsufficientScopes("cloud-platform".into());
216        assert_eq!(err.to_string(), "Insufficient scopes: cloud-platform");
217    }
218
219    // -----------------------------------------------------------------------
220    // SessionError Display tests
221    // -----------------------------------------------------------------------
222
223    #[test]
224    fn session_error_websocket_display() {
225        let err =
226            SessionError::WebSocket(WebSocketError::ConnectionRefused("host unreachable".into()));
227        assert_eq!(
228            err.to_string(),
229            "WebSocket error: Connection refused: host unreachable"
230        );
231    }
232
233    #[test]
234    fn session_error_timeout_display() {
235        let err = SessionError::Timeout {
236            phase: SessionPhase::SetupSent,
237            elapsed: Duration::from_secs(15),
238        };
239        assert_eq!(err.to_string(), "Timeout in SetupSent after 15s");
240    }
241
242    #[test]
243    fn session_error_timeout_connecting_display() {
244        let err = SessionError::Timeout {
245            phase: SessionPhase::Connecting,
246            elapsed: Duration::from_secs(10),
247        };
248        assert_eq!(err.to_string(), "Timeout in Connecting after 10s");
249    }
250
251    #[test]
252    fn session_error_setup_failed_display() {
253        let err = SessionError::SetupFailed(SetupError::AuthenticationFailed("bad token".into()));
254        assert_eq!(
255            err.to_string(),
256            "Setup failed: Authentication failed: bad token"
257        );
258    }
259
260    #[test]
261    fn session_error_go_away_with_time_display() {
262        let err = SessionError::GoAway {
263            time_left: Some(Duration::from_secs(30)),
264        };
265        assert_eq!(err.to_string(), "Server sent GoAway (time left: Some(30s))");
266    }
267
268    #[test]
269    fn session_error_go_away_no_time_display() {
270        let err = SessionError::GoAway { time_left: None };
271        assert_eq!(err.to_string(), "Server sent GoAway (time left: None)");
272    }
273
274    #[test]
275    fn session_error_auth_display() {
276        let err = SessionError::Auth(AuthError::TokenExpired);
277        assert_eq!(err.to_string(), "Auth error: Token expired");
278    }
279
280    #[test]
281    fn session_error_not_connected_display() {
282        let err = SessionError::NotConnected;
283        assert_eq!(err.to_string(), "Not connected");
284    }
285
286    #[test]
287    fn session_error_channel_closed_display() {
288        let err = SessionError::ChannelClosed;
289        assert_eq!(err.to_string(), "Internal channel closed");
290    }
291
292    #[test]
293    fn session_error_send_queue_full_display() {
294        let err = SessionError::SendQueueFull;
295        assert_eq!(err.to_string(), "Send queue full");
296    }
297
298    #[test]
299    fn session_error_invalid_transition_display() {
300        let err = SessionError::InvalidTransition {
301            from: SessionPhase::Active,
302            to: SessionPhase::SetupSent,
303        };
304        assert_eq!(
305            err.to_string(),
306            "Invalid transition from Active to SetupSent"
307        );
308    }
309
310    // -----------------------------------------------------------------------
311    // Clone tests (ensure all error types are Clone)
312    // -----------------------------------------------------------------------
313
314    #[test]
315    fn error_types_are_clone() {
316        let ws_err = WebSocketError::ProtocolError("test".into());
317        let _ = ws_err.clone();
318
319        let setup_err = SetupError::InvalidModel("test".into());
320        let _ = setup_err.clone();
321
322        let auth_err = AuthError::TokenExpired;
323        let _ = auth_err.clone();
324
325        let session_err = SessionError::WebSocket(WebSocketError::ProtocolError("test".into()));
326        let _ = session_err.clone();
327    }
328}