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)]
71pub struct SessionManager {
72 sessions_dir: PathBuf,
73}
74
75impl SessionManager {
76 pub fn new(sessions_dir: PathBuf) -> Result<Self> {
81 std::fs::create_dir_all(&sessions_dir).map_err(|e| {
82 crate::Error::SessionError(format!("Failed to create sessions dir: {}", e))
83 })?;
84 Ok(Self { sessions_dir })
85 }
86
87 pub fn create_session(&mut self, name: Option<&str>) -> Result<Session> {
92 let now = SystemTime::now()
93 .duration_since(UNIX_EPOCH)
94 .unwrap_or_default();
95 let ts = now.as_secs();
96 let id = crate::utils::generate_id();
97
98 let session = Session {
99 id,
100 name: name.map(String::from),
101 job_ids: Vec::new(),
102 created_at: ts,
103 updated_at: ts,
104 status: SessionStatus::Active,
105 };
106
107 self.save_session(&session)?;
108 Ok(session)
109 }
110
111 pub fn load_session(&self, session_id: &str) -> Result<Session> {
117 let path = self.session_path(session_id);
118 let content = std::fs::read_to_string(&path)
119 .map_err(|e| crate::Error::SessionError(format!("Session not found {}: {}", session_id, e)))?;
120 serde_json::from_str(&content)
121 .map_err(|e| crate::Error::SessionError(format!("Failed to parse session: {}", e)))
122 }
123
124 pub fn add_job(&mut self, session_id: &str, job_id: &str) -> Result<()> {
129 let mut session = self.load_session(session_id)?;
130 session.job_ids.push(job_id.to_string());
131 session.updated_at = SystemTime::now()
132 .duration_since(UNIX_EPOCH)
133 .unwrap_or_default()
134 .as_secs();
135 self.save_session(&session)
136 }
137
138 pub fn list_sessions(&self) -> Result<Vec<Session>> {
144 let mut sessions = Vec::new();
145 if !self.sessions_dir.exists() {
146 return Ok(sessions);
147 }
148
149 for entry in std::fs::read_dir(&self.sessions_dir)
150 .map_err(|e| crate::Error::SessionError(format!("Failed to read sessions: {}", e)))?
151 {
152 let entry = entry
153 .map_err(|e| crate::Error::SessionError(format!("Failed to read entry: {}", e)))?;
154 let path = entry.path();
155 if path.extension().is_some_and(|ext| ext == "json") {
156 if let Ok(content) = std::fs::read_to_string(&path) {
157 if let Ok(session) = serde_json::from_str(&content) {
158 sessions.push(session);
159 }
160 }
161 }
162 }
163
164 sessions.sort_by_key(|s: &Session| s.updated_at);
165 sessions.reverse();
166 Ok(sessions)
167 }
168
169 fn session_path(&self, session_id: &str) -> PathBuf {
170 self.sessions_dir.join(format!("{}.json", session_id))
171 }
172
173 fn save_session(&self, session: &Session) -> Result<()> {
174 let path = self.session_path(&session.id);
175 let content = serde_json::to_string_pretty(session).map_err(|e| {
176 crate::Error::SessionError(format!("Failed to serialize session: {}", e))
177 })?;
178 std::fs::write(&path, content)
179 .map_err(|e| crate::Error::SessionError(format!("Failed to write session: {}", e)))?;
180 Ok(())
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use std::fs;
188
189 fn tmp_dir(name: &str) -> PathBuf {
190 let dir = std::env::temp_dir().join(format!("runtimo_test_sessions_{}", name));
191 let _ = fs::remove_dir_all(&dir);
192 fs::create_dir_all(&dir).unwrap();
193 dir
194 }
195
196 #[test]
197 fn creates_session() {
198 let dir = tmp_dir("creates");
199 let mut mgr = SessionManager::new(dir).unwrap();
200 let session = mgr.create_session(Some("test")).unwrap();
201 assert!(!session.id.is_empty());
202 assert_eq!(session.name, Some("test".to_string()));
203 assert_eq!(session.job_ids.len(), 0);
204 }
205
206 #[test]
207 fn adds_job_to_session() {
208 let dir = tmp_dir("adds_job");
209 let mut mgr = SessionManager::new(dir).unwrap();
210 let session = mgr.create_session(None).unwrap();
211 mgr.add_job(&session.id, "job-123").unwrap();
212
213 let loaded = mgr.load_session(&session.id).unwrap();
214 assert_eq!(loaded.job_ids.len(), 1);
215 assert_eq!(loaded.job_ids[0], "job-123");
216 }
217
218 #[test]
219 fn lists_sessions() {
220 let dir = tmp_dir("lists");
221 let mut mgr = SessionManager::new(dir).unwrap();
222 let _ = mgr.create_session(Some("first")).unwrap();
223 let _ = mgr.create_session(Some("second")).unwrap();
224
225 let sessions = mgr.list_sessions().unwrap();
226 assert_eq!(sessions.len(), 2);
227 }
228}