1use crate::Result;
32use serde::{Deserialize, Serialize};
33use std::path::PathBuf;
34use std::time::{SystemTime, UNIX_EPOCH};
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[allow(clippy::exhaustive_structs)]
39pub struct Session {
40 pub id: String,
42 pub name: Option<String>,
44 pub job_ids: Vec<String>,
46 pub created_at: u64,
48 pub updated_at: u64,
50 pub status: SessionStatus,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57#[allow(clippy::exhaustive_enums)]
58pub enum SessionStatus {
59 Active,
61 Paused,
63 Completed,
65 Terminated,
67}
68
69#[allow(clippy::exhaustive_structs)]
74pub struct SessionManager {
75 sessions_dir: PathBuf,
76}
77
78impl SessionManager {
79 pub fn new(sessions_dir: PathBuf) -> Result<Self> {
84 std::fs::create_dir_all(&sessions_dir).map_err(|e| {
85 crate::Error::SessionError(format!("Failed to create sessions dir: {}", e))
86 })?;
87 Ok(Self { sessions_dir })
88 }
89
90 pub fn create_session(&mut self, name: Option<&str>) -> Result<Session> {
95 let now = SystemTime::now()
96 .duration_since(UNIX_EPOCH)
97 .unwrap_or_default();
98 let ts = now.as_secs();
99 let id = crate::utils::generate_id();
100
101 let session = Session {
102 id,
103 name: name.map(String::from),
104 job_ids: Vec::new(),
105 created_at: ts,
106 updated_at: ts,
107 status: SessionStatus::Active,
108 };
109
110 self.save_session(&session)?;
111 Ok(session)
112 }
113
114 pub fn load_session(&self, session_id: &str) -> Result<Session> {
119 let path = self.session_path(session_id);
120 let content = std::fs::read_to_string(&path).map_err(|e| {
121 crate::Error::SessionError(format!("Session not found {}: {}", session_id, e))
122 })?;
123 serde_json::from_str(&content)
124 .map_err(|e| crate::Error::SessionError(format!("Failed to parse session: {}", e)))
125 }
126
127 pub fn add_job(&mut self, session_id: &str, job_id: &str) -> Result<()> {
132 let mut session = self.load_session(session_id)?;
133 session.job_ids.push(job_id.to_string());
134 session.updated_at = SystemTime::now()
135 .duration_since(UNIX_EPOCH)
136 .unwrap_or_default()
137 .as_secs();
138 self.save_session(&session)
139 }
140
141 pub fn list_sessions(&self) -> Result<Vec<Session>> {
147 let mut sessions = Vec::new();
148 if !self.sessions_dir.exists() {
149 return Ok(sessions);
150 }
151
152 for entry in std::fs::read_dir(&self.sessions_dir)
153 .map_err(|e| crate::Error::SessionError(format!("Failed to read sessions: {}", e)))?
154 {
155 let entry = entry
156 .map_err(|e| crate::Error::SessionError(format!("Failed to read entry: {}", e)))?;
157 let path = entry.path();
158 if path.extension().is_some_and(|ext| ext == "json") {
159 if let Ok(content) = std::fs::read_to_string(&path) {
160 if let Ok(session) = serde_json::from_str(&content) {
161 sessions.push(session);
162 }
163 }
164 }
165 }
166
167 sessions.sort_by_key(|s: &Session| s.updated_at);
168 sessions.reverse();
169 Ok(sessions)
170 }
171
172 fn session_path(&self, session_id: &str) -> PathBuf {
173 self.sessions_dir.join(format!("{}.json", session_id))
174 }
175
176 fn save_session(&self, session: &Session) -> Result<()> {
177 let path = self.session_path(&session.id);
178 let content = serde_json::to_string_pretty(session).map_err(|e| {
179 crate::Error::SessionError(format!("Failed to serialize session: {}", e))
180 })?;
181 std::fs::write(&path, content)
182 .map_err(|e| crate::Error::SessionError(format!("Failed to write session: {}", e)))?;
183 Ok(())
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use std::fs;
191
192 fn tmp_dir(name: &str) -> PathBuf {
193 let dir = std::env::temp_dir().join(format!("runtimo_test_sessions_{}", name));
194 let _ = fs::remove_dir_all(&dir);
195 fs::create_dir_all(&dir).unwrap();
196 dir
197 }
198
199 #[test]
200 fn creates_session() {
201 let dir = tmp_dir("creates");
202 let mut mgr = SessionManager::new(dir).unwrap();
203 let session = mgr.create_session(Some("test")).unwrap();
204 assert!(!session.id.is_empty());
205 assert_eq!(session.name, Some("test".to_string()));
206 assert_eq!(session.job_ids.len(), 0);
207 }
208
209 #[test]
210 fn adds_job_to_session() {
211 let dir = tmp_dir("adds_job");
212 let mut mgr = SessionManager::new(dir).unwrap();
213 let session = mgr.create_session(None).unwrap();
214 mgr.add_job(&session.id, "job-123").unwrap();
215
216 let loaded = mgr.load_session(&session.id).unwrap();
217 assert_eq!(loaded.job_ids.len(), 1);
218 assert_eq!(loaded.job_ids[0], "job-123");
219 }
220
221 #[test]
222 fn lists_sessions() {
223 let dir = tmp_dir("lists");
224 let mut mgr = SessionManager::new(dir).unwrap();
225 let _ = mgr.create_session(Some("first")).unwrap();
226 let _ = mgr.create_session(Some("second")).unwrap();
227
228 let sessions = mgr.list_sessions().unwrap();
229 assert_eq!(sessions.len(), 2);
230 }
231}