use crate::error::Result;
use crate::http::HttpClient;
use crate::http::encode_path_segment;
use crate::types::session::CreateSessionRequest;
use crate::types::session::RevertRequest;
use crate::types::session::Session;
use crate::types::session::SessionDiff;
use crate::types::session::SessionStatus;
use crate::types::session::SessionStatusInfo;
use crate::types::session::SessionStatusResponse;
use crate::types::session::ShareInfo;
use crate::types::session::SummarizeRequest;
use crate::types::session::TodoItem;
use crate::types::session::UpdateSessionRequest;
use reqwest::Method;
#[derive(Clone)]
pub struct SessionsApi {
http: HttpClient,
}
impl SessionsApi {
pub fn new(http: HttpClient) -> Self {
Self { http }
}
pub async fn create(&self, req: &CreateSessionRequest) -> Result<Session> {
let body = serde_json::to_value(req)?;
self.http
.request_json(Method::POST, "/session", Some(body))
.await
}
pub async fn get(&self, id: &str) -> Result<Session> {
let sid = encode_path_segment(id);
self.http
.request_json(Method::GET, &format!("/session/{sid}"), None)
.await
}
pub async fn list(&self) -> Result<Vec<Session>> {
self.http.request_json(Method::GET, "/session", None).await
}
pub async fn delete(&self, id: &str) -> Result<bool> {
let sid = encode_path_segment(id);
self.http
.request_json::<bool>(Method::DELETE, &format!("/session/{sid}"), None)
.await
}
pub async fn fork(&self, id: &str) -> Result<Session> {
let sid = encode_path_segment(id);
self.http
.request_json(
Method::POST,
&format!("/session/{sid}/fork"),
Some(serde_json::json!({})),
)
.await
}
pub async fn abort(&self, id: &str) -> Result<bool> {
let sid = encode_path_segment(id);
self.http
.request_json::<bool>(
Method::POST,
&format!("/session/{sid}/abort"),
Some(serde_json::json!({})),
)
.await
}
pub async fn status(&self) -> Result<SessionStatus> {
let response: SessionStatusResponse = self
.http
.request_json(Method::GET, "/session/status", None)
.await?;
Ok(response.into_legacy_summary())
}
pub async fn status_for(&self, session_id: &str) -> Result<SessionStatusInfo> {
let response: SessionStatusResponse = self
.http
.request_json(Method::GET, "/session/status", None)
.await?;
Ok(response.status_for(session_id))
}
pub async fn children(&self, id: &str) -> Result<Vec<Session>> {
let sid = encode_path_segment(id);
self.http
.request_json(Method::GET, &format!("/session/{sid}/children"), None)
.await
}
pub async fn todo(&self, id: &str) -> Result<Vec<TodoItem>> {
let sid = encode_path_segment(id);
self.http
.request_json(Method::GET, &format!("/session/{sid}/todo"), None)
.await
}
pub async fn update(&self, id: &str, req: &UpdateSessionRequest) -> Result<Session> {
let sid = encode_path_segment(id);
let body = serde_json::to_value(req)?;
self.http
.request_json(Method::PATCH, &format!("/session/{sid}"), Some(body))
.await
}
pub async fn init(&self, id: &str) -> Result<Session> {
let sid = encode_path_segment(id);
self.http
.request_json(
Method::POST,
&format!("/session/{sid}/init"),
Some(serde_json::json!({})),
)
.await
}
pub async fn share(&self, id: &str) -> Result<ShareInfo> {
let sid = encode_path_segment(id);
self.http
.request_json(
Method::POST,
&format!("/session/{sid}/share"),
Some(serde_json::json!({})),
)
.await
}
pub async fn unshare(&self, id: &str) -> Result<()> {
let sid = encode_path_segment(id);
self.http
.request_empty(Method::DELETE, &format!("/session/{sid}/share"), None)
.await
}
pub async fn diff(&self, id: &str) -> Result<SessionDiff> {
let sid = encode_path_segment(id);
self.http
.request_json(Method::GET, &format!("/session/{sid}/diff"), None)
.await
}
pub async fn diff_since_message(&self, id: &str, message_id: &str) -> Result<SessionDiff> {
let sid = encode_path_segment(id);
let mid = encode_path_segment(message_id);
self.http
.request_json(
Method::GET,
&format!("/session/{sid}/diff?messageID={mid}"),
None,
)
.await
}
pub async fn summarize(&self, id: &str, req: &SummarizeRequest) -> Result<bool> {
let sid = encode_path_segment(id);
let body = serde_json::to_value(req)?;
self.http
.request_json::<bool>(
Method::POST,
&format!("/session/{sid}/summarize"),
Some(body),
)
.await
}
pub async fn revert(&self, id: &str, req: &RevertRequest) -> Result<Session> {
let sid = encode_path_segment(id);
let body = serde_json::to_value(req)?;
self.http
.request_json(Method::POST, &format!("/session/{sid}/revert"), Some(body))
.await
}
pub async fn unrevert(&self, id: &str) -> Result<Session> {
let sid = encode_path_segment(id);
self.http
.request_json(
Method::POST,
&format!("/session/{sid}/unrevert"),
Some(serde_json::json!({})),
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::HttpConfig;
use std::time::Duration;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::body_json;
use wiremock::matchers::method;
use wiremock::matchers::path;
#[tokio::test]
async fn test_create_session() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session"))
.and(body_json(serde_json::json!({})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "session123",
"slug": "session123",
"projectId": "proj1",
"directory": "/path",
"title": "New Session",
"version": "1.0",
"time": {"created": 1_234_567_890, "updated": 1_234_567_890}
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let session = sessions
.create(&CreateSessionRequest::default())
.await
.unwrap();
assert_eq!(session.id, "session123");
}
#[tokio::test]
async fn test_get_session() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/session/abc123"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "abc123",
"slug": "abc123",
"projectId": "p1",
"directory": "/path",
"title": "Test Session",
"version": "1.0",
"time": {"created": 1_234_567_890, "updated": 1_234_567_890}
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let session = sessions.get("abc123").await.unwrap();
assert_eq!(session.id, "abc123");
assert_eq!(session.title, "Test Session");
}
#[tokio::test]
async fn test_list_sessions() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/session"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
{"id": "s1", "slug": "s1", "projectId": "p1", "directory": "/path", "title": "S1", "version": "1.0", "time": {"created": 1_234_567_890, "updated": 1_234_567_890}},
{"id": "s2", "slug": "s2", "projectId": "p1", "directory": "/path", "title": "S2", "version": "1.0", "time": {"created": 1_234_567_890, "updated": 1_234_567_890}}
])))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let list = sessions.list().await.unwrap();
assert_eq!(list.len(), 2);
}
#[tokio::test]
async fn test_delete_session() {
let mock_server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/session/del123"))
.respond_with(ResponseTemplate::new(200).set_body_json(true))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let ok = sessions.delete("del123").await.unwrap();
assert!(ok);
}
#[tokio::test]
async fn test_children() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/session/parent123/children"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
{"id": "child1", "slug": "child1", "projectId": "p1", "directory": "/path", "title": "Child 1", "version": "1.0", "time": {"created": 1_234_567_890, "updated": 1_234_567_890}}
])))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let children = sessions.children("parent123").await.unwrap();
assert_eq!(children.len(), 1);
assert_eq!(children[0].id, "child1");
}
#[tokio::test]
async fn test_todo() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/session/s1/todo"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
{"id": "t1", "content": "Task 1", "completed": false},
{"id": "t2", "content": "Task 2", "completed": true}
])))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let todos = sessions.todo("s1").await.unwrap();
assert_eq!(todos.len(), 2);
assert!(!todos[0].completed);
assert!(todos[1].completed);
}
#[tokio::test]
async fn test_update_session() {
let mock_server = MockServer::start().await;
Mock::given(method("PATCH"))
.and(path("/session/s1"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "s1",
"slug": "s1",
"projectId": "p1",
"directory": "/path",
"title": "Updated Title",
"version": "1.0",
"time": {"created": 1_234_567_890, "updated": 1_234_567_891}
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let session = sessions
.update(
"s1",
&UpdateSessionRequest {
title: Some("Updated Title".into()),
},
)
.await
.unwrap();
assert_eq!(session.title, "Updated Title");
}
#[tokio::test]
async fn test_share() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session/s1/share"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"url": "https://share.example.com/s1"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let share = sessions.share("s1").await.unwrap();
assert_eq!(share.url, "https://share.example.com/s1");
}
#[tokio::test]
async fn test_unshare() {
let mock_server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/session/s1/share"))
.respond_with(ResponseTemplate::new(204))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
sessions.unshare("s1").await.unwrap();
}
#[tokio::test]
async fn test_diff() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/session/s1/diff"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
{
"file": "file.rs",
"before": "old content",
"after": "new content",
"additions": 1,
"deletions": 1,
"status": "modified"
}
])))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let diff = sessions.diff("s1").await.unwrap();
assert_eq!(diff.len(), 1);
assert_eq!(diff[0].file, "file.rs");
assert_eq!(diff[0].additions, 1);
assert_eq!(diff[0].deletions, 1);
}
#[tokio::test]
async fn test_summarize() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session/s1/summarize"))
.respond_with(ResponseTemplate::new(200).set_body_json(true))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let ok = sessions
.summarize(
"s1",
&SummarizeRequest {
provider_id: "anthropic".into(),
model_id: "claude-3-5-sonnet".into(),
auto: None,
},
)
.await
.unwrap();
assert!(ok);
}
#[tokio::test]
async fn test_revert() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session/s1/revert"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "s1",
"slug": "s1",
"projectId": "p1",
"directory": "/path",
"title": "Reverted Session",
"version": "1.0",
"time": {"created": 1_234_567_890, "updated": 1_234_567_891}
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let session = sessions
.revert(
"s1",
&crate::types::session::RevertRequest {
message_id: "m5".into(),
part_id: None,
},
)
.await
.unwrap();
assert_eq!(session.id, "s1");
}
#[tokio::test]
async fn test_unrevert() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session/s1/unrevert"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "s1",
"slug": "s1",
"projectId": "p1",
"directory": "/path",
"title": "Unreverted Session",
"version": "1.0",
"time": {"created": 1_234_567_890, "updated": 1_234_567_891}
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let session = sessions.unrevert("s1").await.unwrap();
assert_eq!(session.id, "s1");
}
#[tokio::test]
async fn test_get_session_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/session/nonexistent"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"name": "NotFound",
"message": "Session not found",
"data": {"id": "nonexistent"}
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions.get("nonexistent").await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_not_found());
assert_eq!(err.error_name(), Some("NotFound"));
}
#[tokio::test]
async fn test_create_session_validation_error() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session"))
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
"name": "ValidationError",
"message": "Invalid session configuration"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions.create(&CreateSessionRequest::default()).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_validation_error());
}
#[tokio::test]
async fn test_children_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/session/missing/children"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"name": "NotFound",
"message": "Session not found"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions.children("missing").await;
assert!(result.is_err());
assert!(result.unwrap_err().is_not_found());
}
#[tokio::test]
async fn test_update_session_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("PATCH"))
.and(path("/session/missing"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"name": "NotFound",
"message": "Session not found"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions
.update("missing", &UpdateSessionRequest { title: None })
.await;
assert!(result.is_err());
assert!(result.unwrap_err().is_not_found());
}
#[tokio::test]
async fn test_share_server_error() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session/s1/share"))
.respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
"name": "InternalError",
"message": "Failed to generate share link"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions.share("s1").await;
assert!(result.is_err());
assert!(result.unwrap_err().is_server_error());
}
#[tokio::test]
async fn test_diff_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/session/missing/diff"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"name": "NotFound",
"message": "Session not found"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions.diff("missing").await;
assert!(result.is_err());
assert!(result.unwrap_err().is_not_found());
}
#[tokio::test]
async fn test_summarize_validation_error() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session/s1/summarize"))
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
"name": "ValidationError",
"message": "Invalid provider or model"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions
.summarize(
"s1",
&SummarizeRequest {
provider_id: "invalid".into(),
model_id: "invalid".into(),
auto: None,
},
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().is_validation_error());
}
#[tokio::test]
async fn test_revert_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session/missing/revert"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"name": "NotFound",
"message": "Session not found"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions
.revert(
"missing",
&crate::types::session::RevertRequest {
message_id: "m1".into(),
part_id: None,
},
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().is_not_found());
}
#[tokio::test]
async fn test_abort_server_error() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/session/s1/abort"))
.respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
"name": "InternalError",
"message": "Failed to abort session"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions.abort("s1").await;
assert!(result.is_err());
assert!(result.unwrap_err().is_server_error());
}
#[tokio::test]
async fn test_todo_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/session/missing/todo"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"name": "NotFound",
"message": "Session not found"
})))
.mount(&mock_server)
.await;
let http = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let sessions = SessionsApi::new(http);
let result = sessions.todo("missing").await;
assert!(result.is_err());
assert!(result.unwrap_err().is_not_found());
}
}