Skip to main content

opencode_sdk/http/
sessions.rs

1//! Sessions API for OpenCode.
2//!
3//! This module provides methods for session endpoints (18 total).
4
5use crate::error::Result;
6use crate::http::HttpClient;
7use crate::types::session::{
8    CreateSessionRequest, RevertRequest, Session, SessionCreateOptions, SessionDiff, SessionStatus,
9    ShareInfo, SummarizeRequest, TodoItem, UpdateSessionRequest,
10};
11use reqwest::Method;
12
13/// Sessions API client.
14#[derive(Clone)]
15pub struct SessionsApi {
16    http: HttpClient,
17}
18
19impl SessionsApi {
20    /// Create a new Sessions API client.
21    pub fn new(http: HttpClient) -> Self {
22        Self { http }
23    }
24
25    /// Create a new session.
26    ///
27    /// # Errors
28    ///
29    /// Returns an error if the request fails.
30    pub async fn create(&self, req: &CreateSessionRequest) -> Result<Session> {
31        let body = serde_json::to_value(req)?;
32
33        let path = if let Some(directory) = &req.directory {
34            format!("/session?directory={}", urlencoding::encode(directory))
35        } else {
36            "/session".to_string()
37        };
38
39        self.http
40            .request_json(Method::POST, &path, Some(body))
41            .await
42    }
43
44    /// Create a new session using convenience options.
45    ///
46    /// This maps to the same wire format/query semantics as [`Self::create`].
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if the request fails.
51    pub async fn create_with(&self, options: SessionCreateOptions) -> Result<Session> {
52        let req: CreateSessionRequest = options.into();
53        self.create(&req).await
54    }
55
56    /// Get session by ID.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if the request fails or session not found.
61    pub async fn get(&self, id: &str) -> Result<Session> {
62        self.http
63            .request_json(Method::GET, &format!("/session/{}", id), None)
64            .await
65    }
66
67    /// List all sessions.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if the request fails.
72    pub async fn list(&self) -> Result<Vec<Session>> {
73        self.http.request_json(Method::GET, "/session", None).await
74    }
75
76    /// Delete session by ID.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if the request fails.
81    pub async fn delete(&self, id: &str) -> Result<()> {
82        self.http
83            .request_empty(Method::DELETE, &format!("/session/{}", id), None)
84            .await
85    }
86
87    /// Fork a session from a specific point.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if the request fails.
92    pub async fn fork(&self, id: &str) -> Result<Session> {
93        self.http
94            .request_json(
95                Method::POST,
96                &format!("/session/{}/fork", id),
97                Some(serde_json::json!({})),
98            )
99            .await
100    }
101
102    /// Abort an active session.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the request fails.
107    pub async fn abort(&self, id: &str) -> Result<()> {
108        self.http
109            .request_empty(
110                Method::POST,
111                &format!("/session/{}/abort", id),
112                Some(serde_json::json!({})),
113            )
114            .await
115    }
116
117    /// Get session status.
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if the request fails.
122    pub async fn status(&self) -> Result<SessionStatus> {
123        self.http
124            .request_json(Method::GET, "/session/status", None)
125            .await
126    }
127
128    /// Get children of a session (forked sessions).
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the request fails.
133    pub async fn children(&self, id: &str) -> Result<Vec<Session>> {
134        self.http
135            .request_json(Method::GET, &format!("/session/{}/children", id), None)
136            .await
137    }
138
139    /// Get todos for a session.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if the request fails.
144    pub async fn todo(&self, id: &str) -> Result<Vec<TodoItem>> {
145        self.http
146            .request_json(Method::GET, &format!("/session/{}/todo", id), None)
147            .await
148    }
149
150    /// Update a session.
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if the request fails.
155    pub async fn update(&self, id: &str, req: &UpdateSessionRequest) -> Result<Session> {
156        let body = serde_json::to_value(req)?;
157        self.http
158            .request_json(Method::PATCH, &format!("/session/{}", id), Some(body))
159            .await
160    }
161
162    /// Initialize a session.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if the request fails.
167    pub async fn init(&self, id: &str) -> Result<Session> {
168        self.http
169            .request_json(
170                Method::POST,
171                &format!("/session/{}/init", id),
172                Some(serde_json::json!({})),
173            )
174            .await
175    }
176
177    /// Share a session.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error if the request fails.
182    pub async fn share(&self, id: &str) -> Result<ShareInfo> {
183        self.http
184            .request_json(
185                Method::POST,
186                &format!("/session/{}/share", id),
187                Some(serde_json::json!({})),
188            )
189            .await
190    }
191
192    /// Unshare a session.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if the request fails.
197    pub async fn unshare(&self, id: &str) -> Result<()> {
198        self.http
199            .request_empty(Method::DELETE, &format!("/session/{}/share", id), None)
200            .await
201    }
202
203    /// Get session diff.
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if the request fails.
208    pub async fn diff(&self, id: &str) -> Result<SessionDiff> {
209        self.http
210            .request_json(Method::GET, &format!("/session/{}/diff", id), None)
211            .await
212    }
213
214    /// Get session diff since a specific message.
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if the request fails.
219    pub async fn diff_since_message(&self, id: &str, message_id: &str) -> Result<SessionDiff> {
220        let encoded = urlencoding::encode(message_id);
221        self.http
222            .request_json(
223                Method::GET,
224                &format!("/session/{}/diff?messageID={}", id, encoded),
225                None,
226            )
227            .await
228    }
229
230    /// Summarize a session.
231    ///
232    /// # Errors
233    ///
234    /// Returns an error if the request fails.
235    pub async fn summarize(&self, id: &str, req: &SummarizeRequest) -> Result<Session> {
236        let body = serde_json::to_value(req)?;
237        self.http
238            .request_json(
239                Method::POST,
240                &format!("/session/{}/summarize", id),
241                Some(body),
242            )
243            .await
244    }
245
246    /// Revert a session to a previous state.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the request fails.
251    pub async fn revert(&self, id: &str, req: &RevertRequest) -> Result<Session> {
252        let body = serde_json::to_value(req)?;
253        self.http
254            .request_json(Method::POST, &format!("/session/{}/revert", id), Some(body))
255            .await
256    }
257
258    /// Unrevert a session (undo a revert).
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if the request fails.
263    pub async fn unrevert(&self, id: &str) -> Result<Session> {
264        self.http
265            .request_json(
266                Method::POST,
267                &format!("/session/{}/unrevert", id),
268                Some(serde_json::json!({})),
269            )
270            .await
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::http::HttpConfig;
278    use std::time::Duration;
279    use wiremock::matchers::{body_json, method, path, query_param};
280    use wiremock::{Mock, MockServer, ResponseTemplate};
281
282    #[tokio::test]
283    async fn test_create_session() {
284        let mock_server = MockServer::start().await;
285
286        Mock::given(method("POST"))
287            .and(path("/session"))
288            .and(body_json(serde_json::json!({})))
289            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
290                "id": "session123",
291                "projectId": "proj1",
292                "directory": "/path",
293                "title": "New Session",
294                "version": "1.0",
295                "time": {"created": 1234567890, "updated": 1234567890}
296            })))
297            .mount(&mock_server)
298            .await;
299
300        let http = HttpClient::new(HttpConfig {
301            base_url: mock_server.uri(),
302            directory: None,
303            timeout: Duration::from_secs(30),
304        })
305        .unwrap();
306
307        let sessions = SessionsApi::new(http);
308        let session = sessions
309            .create(&CreateSessionRequest::default())
310            .await
311            .unwrap();
312        assert_eq!(session.id, "session123");
313    }
314
315    #[tokio::test]
316    async fn test_create_session_with_options() {
317        let mock_server = MockServer::start().await;
318
319        Mock::given(method("POST"))
320            .and(path("/session"))
321            .and(query_param("directory", "/tmp/project"))
322            .and(body_json(serde_json::json!({
323                "title": "Quick Win",
324                "parentID": "parent-123"
325            })))
326            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
327                "id": "session123",
328                "projectId": "proj1",
329                "directory": "/tmp/project",
330                "title": "Quick Win",
331                "version": "1.0",
332                "time": {"created": 1234567890, "updated": 1234567890}
333            })))
334            .mount(&mock_server)
335            .await;
336
337        let http = HttpClient::new(HttpConfig {
338            base_url: mock_server.uri(),
339            directory: None,
340            timeout: Duration::from_secs(30),
341        })
342        .unwrap();
343
344        let sessions = SessionsApi::new(http);
345        let session = sessions
346            .create_with(
347                SessionCreateOptions::new()
348                    .with_title("Quick Win")
349                    .with_parent_id("parent-123")
350                    .with_directory("/tmp/project"),
351            )
352            .await
353            .unwrap();
354
355        assert_eq!(session.id, "session123");
356        assert_eq!(session.title, "Quick Win");
357    }
358
359    #[tokio::test]
360    async fn test_get_session() {
361        let mock_server = MockServer::start().await;
362
363        Mock::given(method("GET"))
364            .and(path("/session/abc123"))
365            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
366                "id": "abc123",
367                "projectId": "p1",
368                "directory": "/path",
369                "title": "Test Session",
370                "version": "1.0",
371                "time": {"created": 1234567890, "updated": 1234567890}
372            })))
373            .mount(&mock_server)
374            .await;
375
376        let http = HttpClient::new(HttpConfig {
377            base_url: mock_server.uri(),
378            directory: None,
379            timeout: Duration::from_secs(30),
380        })
381        .unwrap();
382
383        let sessions = SessionsApi::new(http);
384        let session = sessions.get("abc123").await.unwrap();
385        assert_eq!(session.id, "abc123");
386        assert_eq!(session.title, "Test Session");
387    }
388
389    #[tokio::test]
390    async fn test_list_sessions() {
391        let mock_server = MockServer::start().await;
392
393        Mock::given(method("GET"))
394            .and(path("/session"))
395            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
396                {"id": "s1", "projectId": "p1", "directory": "/path", "title": "S1", "version": "1.0", "time": {"created": 1234567890, "updated": 1234567890}},
397                {"id": "s2", "projectId": "p1", "directory": "/path", "title": "S2", "version": "1.0", "time": {"created": 1234567890, "updated": 1234567890}}
398            ])))
399            .mount(&mock_server)
400            .await;
401
402        let http = HttpClient::new(HttpConfig {
403            base_url: mock_server.uri(),
404            directory: None,
405            timeout: Duration::from_secs(30),
406        })
407        .unwrap();
408
409        let sessions = SessionsApi::new(http);
410        let list = sessions.list().await.unwrap();
411        assert_eq!(list.len(), 2);
412    }
413
414    #[tokio::test]
415    async fn test_delete_session() {
416        let mock_server = MockServer::start().await;
417
418        Mock::given(method("DELETE"))
419            .and(path("/session/del123"))
420            .respond_with(ResponseTemplate::new(204))
421            .mount(&mock_server)
422            .await;
423
424        let http = HttpClient::new(HttpConfig {
425            base_url: mock_server.uri(),
426            directory: None,
427            timeout: Duration::from_secs(30),
428        })
429        .unwrap();
430
431        let sessions = SessionsApi::new(http);
432        sessions.delete("del123").await.unwrap();
433    }
434
435    #[tokio::test]
436    async fn test_children() {
437        let mock_server = MockServer::start().await;
438
439        Mock::given(method("GET"))
440            .and(path("/session/parent123/children"))
441            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
442                {"id": "child1", "projectId": "p1", "directory": "/path", "title": "Child 1", "version": "1.0", "time": {"created": 1234567890, "updated": 1234567890}}
443            ])))
444            .mount(&mock_server)
445            .await;
446
447        let http = HttpClient::new(HttpConfig {
448            base_url: mock_server.uri(),
449            directory: None,
450            timeout: Duration::from_secs(30),
451        })
452        .unwrap();
453
454        let sessions = SessionsApi::new(http);
455        let children = sessions.children("parent123").await.unwrap();
456        assert_eq!(children.len(), 1);
457        assert_eq!(children[0].id, "child1");
458    }
459
460    #[tokio::test]
461    async fn test_todo() {
462        let mock_server = MockServer::start().await;
463
464        Mock::given(method("GET"))
465            .and(path("/session/s1/todo"))
466            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
467                {"id": "t1", "content": "Task 1", "completed": false},
468                {"id": "t2", "content": "Task 2", "completed": true}
469            ])))
470            .mount(&mock_server)
471            .await;
472
473        let http = HttpClient::new(HttpConfig {
474            base_url: mock_server.uri(),
475            directory: None,
476            timeout: Duration::from_secs(30),
477        })
478        .unwrap();
479
480        let sessions = SessionsApi::new(http);
481        let todos = sessions.todo("s1").await.unwrap();
482        assert_eq!(todos.len(), 2);
483        assert!(!todos[0].completed);
484        assert!(todos[1].completed);
485    }
486
487    #[tokio::test]
488    async fn test_update_session() {
489        let mock_server = MockServer::start().await;
490
491        Mock::given(method("PATCH"))
492            .and(path("/session/s1"))
493            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
494                "id": "s1",
495                "projectId": "p1",
496                "directory": "/path",
497                "title": "Updated Title",
498                "version": "1.0",
499                "time": {"created": 1234567890, "updated": 1234567891}
500            })))
501            .mount(&mock_server)
502            .await;
503
504        let http = HttpClient::new(HttpConfig {
505            base_url: mock_server.uri(),
506            directory: None,
507            timeout: Duration::from_secs(30),
508        })
509        .unwrap();
510
511        let sessions = SessionsApi::new(http);
512        let session = sessions
513            .update(
514                "s1",
515                &UpdateSessionRequest {
516                    title: Some("Updated Title".into()),
517                },
518            )
519            .await
520            .unwrap();
521        assert_eq!(session.title, "Updated Title");
522    }
523
524    #[tokio::test]
525    async fn test_share() {
526        let mock_server = MockServer::start().await;
527
528        Mock::given(method("POST"))
529            .and(path("/session/s1/share"))
530            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
531                "url": "https://share.example.com/s1"
532            })))
533            .mount(&mock_server)
534            .await;
535
536        let http = HttpClient::new(HttpConfig {
537            base_url: mock_server.uri(),
538            directory: None,
539            timeout: Duration::from_secs(30),
540        })
541        .unwrap();
542
543        let sessions = SessionsApi::new(http);
544        let share = sessions.share("s1").await.unwrap();
545        assert_eq!(share.url, "https://share.example.com/s1");
546    }
547
548    #[tokio::test]
549    async fn test_unshare() {
550        let mock_server = MockServer::start().await;
551
552        Mock::given(method("DELETE"))
553            .and(path("/session/s1/share"))
554            .respond_with(ResponseTemplate::new(204))
555            .mount(&mock_server)
556            .await;
557
558        let http = HttpClient::new(HttpConfig {
559            base_url: mock_server.uri(),
560            directory: None,
561            timeout: Duration::from_secs(30),
562        })
563        .unwrap();
564
565        let sessions = SessionsApi::new(http);
566        sessions.unshare("s1").await.unwrap();
567    }
568
569    #[tokio::test]
570    async fn test_diff() {
571        let mock_server = MockServer::start().await;
572
573        Mock::given(method("GET"))
574            .and(path("/session/s1/diff"))
575            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
576                "diff": "--- a/file.rs\n+++ b/file.rs\n@@ -1 +1 @@\n-old\n+new",
577                "files": ["file.rs"]
578            })))
579            .mount(&mock_server)
580            .await;
581
582        let http = HttpClient::new(HttpConfig {
583            base_url: mock_server.uri(),
584            directory: None,
585            timeout: Duration::from_secs(30),
586        })
587        .unwrap();
588
589        let sessions = SessionsApi::new(http);
590        let diff = sessions.diff("s1").await.unwrap();
591        assert!(diff.diff.contains("file.rs"));
592        assert_eq!(diff.files.len(), 1);
593    }
594
595    #[tokio::test]
596    async fn test_summarize() {
597        let mock_server = MockServer::start().await;
598
599        Mock::given(method("POST"))
600            .and(path("/session/s1/summarize"))
601            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
602                "id": "s1",
603                "projectId": "p1",
604                "directory": "/path",
605                "title": "Summarized Session",
606                "version": "1.0",
607                "time": {"created": 1234567890, "updated": 1234567891}
608            })))
609            .mount(&mock_server)
610            .await;
611
612        let http = HttpClient::new(HttpConfig {
613            base_url: mock_server.uri(),
614            directory: None,
615            timeout: Duration::from_secs(30),
616        })
617        .unwrap();
618
619        let sessions = SessionsApi::new(http);
620        let session = sessions
621            .summarize(
622                "s1",
623                &SummarizeRequest {
624                    provider_id: "anthropic".into(),
625                    model_id: "claude-3-5-sonnet".into(),
626                    auto: None,
627                },
628            )
629            .await
630            .unwrap();
631        assert_eq!(session.id, "s1");
632    }
633
634    #[tokio::test]
635    async fn test_revert() {
636        let mock_server = MockServer::start().await;
637
638        Mock::given(method("POST"))
639            .and(path("/session/s1/revert"))
640            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
641                "id": "s1",
642                "projectId": "p1",
643                "directory": "/path",
644                "title": "Reverted Session",
645                "version": "1.0",
646                "time": {"created": 1234567890, "updated": 1234567891}
647            })))
648            .mount(&mock_server)
649            .await;
650
651        let http = HttpClient::new(HttpConfig {
652            base_url: mock_server.uri(),
653            directory: None,
654            timeout: Duration::from_secs(30),
655        })
656        .unwrap();
657
658        let sessions = SessionsApi::new(http);
659        let session = sessions
660            .revert(
661                "s1",
662                &crate::types::session::RevertRequest {
663                    message_id: "m5".into(),
664                    part_id: None,
665                },
666            )
667            .await
668            .unwrap();
669        assert_eq!(session.id, "s1");
670    }
671
672    #[tokio::test]
673    async fn test_unrevert() {
674        let mock_server = MockServer::start().await;
675
676        Mock::given(method("POST"))
677            .and(path("/session/s1/unrevert"))
678            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
679                "id": "s1",
680                "projectId": "p1",
681                "directory": "/path",
682                "title": "Unreverted Session",
683                "version": "1.0",
684                "time": {"created": 1234567890, "updated": 1234567891}
685            })))
686            .mount(&mock_server)
687            .await;
688
689        let http = HttpClient::new(HttpConfig {
690            base_url: mock_server.uri(),
691            directory: None,
692            timeout: Duration::from_secs(30),
693        })
694        .unwrap();
695
696        let sessions = SessionsApi::new(http);
697        let session = sessions.unrevert("s1").await.unwrap();
698        assert_eq!(session.id, "s1");
699    }
700
701    // ==================== Error Case Tests ====================
702
703    #[tokio::test]
704    async fn test_get_session_not_found() {
705        let mock_server = MockServer::start().await;
706
707        Mock::given(method("GET"))
708            .and(path("/session/nonexistent"))
709            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
710                "name": "NotFound",
711                "message": "Session not found",
712                "data": {"id": "nonexistent"}
713            })))
714            .mount(&mock_server)
715            .await;
716
717        let http = HttpClient::new(HttpConfig {
718            base_url: mock_server.uri(),
719            directory: None,
720            timeout: Duration::from_secs(30),
721        })
722        .unwrap();
723
724        let sessions = SessionsApi::new(http);
725        let result = sessions.get("nonexistent").await;
726        assert!(result.is_err());
727        let err = result.unwrap_err();
728        assert!(err.is_not_found());
729        assert_eq!(err.error_name(), Some("NotFound"));
730    }
731
732    #[tokio::test]
733    async fn test_create_session_validation_error() {
734        let mock_server = MockServer::start().await;
735
736        Mock::given(method("POST"))
737            .and(path("/session"))
738            .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
739                "name": "ValidationError",
740                "message": "Invalid session configuration"
741            })))
742            .mount(&mock_server)
743            .await;
744
745        let http = HttpClient::new(HttpConfig {
746            base_url: mock_server.uri(),
747            directory: None,
748            timeout: Duration::from_secs(30),
749        })
750        .unwrap();
751
752        let sessions = SessionsApi::new(http);
753        let result = sessions.create(&CreateSessionRequest::default()).await;
754        assert!(result.is_err());
755        let err = result.unwrap_err();
756        assert!(err.is_validation_error());
757    }
758
759    #[tokio::test]
760    async fn test_children_not_found() {
761        let mock_server = MockServer::start().await;
762
763        Mock::given(method("GET"))
764            .and(path("/session/missing/children"))
765            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
766                "name": "NotFound",
767                "message": "Session not found"
768            })))
769            .mount(&mock_server)
770            .await;
771
772        let http = HttpClient::new(HttpConfig {
773            base_url: mock_server.uri(),
774            directory: None,
775            timeout: Duration::from_secs(30),
776        })
777        .unwrap();
778
779        let sessions = SessionsApi::new(http);
780        let result = sessions.children("missing").await;
781        assert!(result.is_err());
782        assert!(result.unwrap_err().is_not_found());
783    }
784
785    #[tokio::test]
786    async fn test_update_session_not_found() {
787        let mock_server = MockServer::start().await;
788
789        Mock::given(method("PATCH"))
790            .and(path("/session/missing"))
791            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
792                "name": "NotFound",
793                "message": "Session not found"
794            })))
795            .mount(&mock_server)
796            .await;
797
798        let http = HttpClient::new(HttpConfig {
799            base_url: mock_server.uri(),
800            directory: None,
801            timeout: Duration::from_secs(30),
802        })
803        .unwrap();
804
805        let sessions = SessionsApi::new(http);
806        let result = sessions
807            .update("missing", &UpdateSessionRequest { title: None })
808            .await;
809        assert!(result.is_err());
810        assert!(result.unwrap_err().is_not_found());
811    }
812
813    #[tokio::test]
814    async fn test_share_server_error() {
815        let mock_server = MockServer::start().await;
816
817        Mock::given(method("POST"))
818            .and(path("/session/s1/share"))
819            .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
820                "name": "InternalError",
821                "message": "Failed to generate share link"
822            })))
823            .mount(&mock_server)
824            .await;
825
826        let http = HttpClient::new(HttpConfig {
827            base_url: mock_server.uri(),
828            directory: None,
829            timeout: Duration::from_secs(30),
830        })
831        .unwrap();
832
833        let sessions = SessionsApi::new(http);
834        let result = sessions.share("s1").await;
835        assert!(result.is_err());
836        assert!(result.unwrap_err().is_server_error());
837    }
838
839    #[tokio::test]
840    async fn test_diff_not_found() {
841        let mock_server = MockServer::start().await;
842
843        Mock::given(method("GET"))
844            .and(path("/session/missing/diff"))
845            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
846                "name": "NotFound",
847                "message": "Session not found"
848            })))
849            .mount(&mock_server)
850            .await;
851
852        let http = HttpClient::new(HttpConfig {
853            base_url: mock_server.uri(),
854            directory: None,
855            timeout: Duration::from_secs(30),
856        })
857        .unwrap();
858
859        let sessions = SessionsApi::new(http);
860        let result = sessions.diff("missing").await;
861        assert!(result.is_err());
862        assert!(result.unwrap_err().is_not_found());
863    }
864
865    #[tokio::test]
866    async fn test_summarize_validation_error() {
867        let mock_server = MockServer::start().await;
868
869        Mock::given(method("POST"))
870            .and(path("/session/s1/summarize"))
871            .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
872                "name": "ValidationError",
873                "message": "Invalid provider or model"
874            })))
875            .mount(&mock_server)
876            .await;
877
878        let http = HttpClient::new(HttpConfig {
879            base_url: mock_server.uri(),
880            directory: None,
881            timeout: Duration::from_secs(30),
882        })
883        .unwrap();
884
885        let sessions = SessionsApi::new(http);
886        let result = sessions
887            .summarize(
888                "s1",
889                &SummarizeRequest {
890                    provider_id: "invalid".into(),
891                    model_id: "invalid".into(),
892                    auto: None,
893                },
894            )
895            .await;
896        assert!(result.is_err());
897        assert!(result.unwrap_err().is_validation_error());
898    }
899
900    #[tokio::test]
901    async fn test_revert_not_found() {
902        let mock_server = MockServer::start().await;
903
904        Mock::given(method("POST"))
905            .and(path("/session/missing/revert"))
906            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
907                "name": "NotFound",
908                "message": "Session not found"
909            })))
910            .mount(&mock_server)
911            .await;
912
913        let http = HttpClient::new(HttpConfig {
914            base_url: mock_server.uri(),
915            directory: None,
916            timeout: Duration::from_secs(30),
917        })
918        .unwrap();
919
920        let sessions = SessionsApi::new(http);
921        let result = sessions
922            .revert(
923                "missing",
924                &crate::types::session::RevertRequest {
925                    message_id: "m1".into(),
926                    part_id: None,
927                },
928            )
929            .await;
930        assert!(result.is_err());
931        assert!(result.unwrap_err().is_not_found());
932    }
933
934    #[tokio::test]
935    async fn test_abort_server_error() {
936        let mock_server = MockServer::start().await;
937
938        Mock::given(method("POST"))
939            .and(path("/session/s1/abort"))
940            .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
941                "name": "InternalError",
942                "message": "Failed to abort session"
943            })))
944            .mount(&mock_server)
945            .await;
946
947        let http = HttpClient::new(HttpConfig {
948            base_url: mock_server.uri(),
949            directory: None,
950            timeout: Duration::from_secs(30),
951        })
952        .unwrap();
953
954        let sessions = SessionsApi::new(http);
955        let result = sessions.abort("s1").await;
956        assert!(result.is_err());
957        assert!(result.unwrap_err().is_server_error());
958    }
959
960    #[tokio::test]
961    async fn test_todo_not_found() {
962        let mock_server = MockServer::start().await;
963
964        Mock::given(method("GET"))
965            .and(path("/session/missing/todo"))
966            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
967                "name": "NotFound",
968                "message": "Session not found"
969            })))
970            .mount(&mock_server)
971            .await;
972
973        let http = HttpClient::new(HttpConfig {
974            base_url: mock_server.uri(),
975            directory: None,
976            timeout: Duration::from_secs(30),
977        })
978        .unwrap();
979
980        let sessions = SessionsApi::new(http);
981        let result = sessions.todo("missing").await;
982        assert!(result.is_err());
983        assert!(result.unwrap_err().is_not_found());
984    }
985}