agentic_reality_mcp/session/
mod.rs1use std::path::{Path, PathBuf};
7
8use agentic_reality::engine::RealityEngine;
9use agentic_reality::format::{ArealReader, ArealWriter};
10
11pub struct SessionManager {
13 pub engine: RealityEngine,
15 pub data_path: Option<String>,
17 pub workspace_id: Option<String>,
19 pub autosave: bool,
21}
22
23impl SessionManager {
24 pub fn new() -> Self {
26 Self {
27 engine: RealityEngine::new(),
28 data_path: None,
29 workspace_id: None,
30 autosave: false,
31 }
32 }
33
34 pub fn with_path(path: String) -> Self {
36 let workspace_id = derive_workspace_id(&path);
37 Self {
38 engine: RealityEngine::new(),
39 data_path: Some(path),
40 workspace_id,
41 autosave: false,
42 }
43 }
44
45 pub fn set_autosave(&mut self, enabled: bool) {
47 self.autosave = enabled;
48 }
49
50 pub fn is_dirty(&self) -> bool {
52 self.engine.is_dirty()
53 }
54
55 pub fn save(&mut self) -> Result<bool, SessionError> {
59 let path_str = match &self.data_path {
60 Some(p) => p.clone(),
61 None => return Ok(false),
62 };
63
64 let path = PathBuf::from(&path_str);
65
66 if let Some(parent) = path.parent() {
68 std::fs::create_dir_all(parent).map_err(|e| SessionError::Io(e.to_string()))?;
69 }
70
71 ArealWriter::save(&self.engine, &path).map_err(|e| SessionError::Save(e.to_string()))?;
72
73 self.engine.mark_clean();
74 Ok(true)
75 }
76
77 pub fn load(&mut self) -> Result<bool, SessionError> {
81 let path_str = match &self.data_path {
82 Some(p) => p.clone(),
83 None => return Ok(false),
84 };
85
86 let path = PathBuf::from(&path_str);
87 if !path.exists() {
88 return Ok(false);
89 }
90
91 let engine = ArealReader::load(&path).map_err(|e| SessionError::Load(e.to_string()))?;
92
93 self.engine = engine;
94 Ok(true)
95 }
96
97 pub fn autosave_if_dirty(&mut self) -> Result<bool, SessionError> {
99 if self.autosave && self.is_dirty() {
100 self.save()
101 } else {
102 Ok(false)
103 }
104 }
105
106 pub fn workspace_id(&self) -> Option<&str> {
108 self.workspace_id.as_deref()
109 }
110}
111
112impl Default for SessionManager {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118fn derive_workspace_id(path: &str) -> Option<String> {
122 let canonical = match std::fs::canonicalize(Path::new(path)) {
123 Ok(p) => p.to_string_lossy().to_string(),
124 Err(_) => path.to_string(),
125 };
126 let hash = blake3::hash(canonical.as_bytes());
127 let hex = hash.to_hex();
128 Some(hex[..16].to_string())
129}
130
131#[derive(Debug)]
133pub enum SessionError {
134 Io(String),
136 Save(String),
138 Load(String),
140}
141
142impl std::fmt::Display for SessionError {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 match self {
145 SessionError::Io(msg) => write!(f, "session IO error: {}", msg),
146 SessionError::Save(msg) => write!(f, "session save error: {}", msg),
147 SessionError::Load(msg) => write!(f, "session load error: {}", msg),
148 }
149 }
150}
151
152impl std::error::Error for SessionError {}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_new_session() {
160 let s = SessionManager::new();
161 assert!(s.data_path.is_none());
162 assert!(s.workspace_id.is_none());
163 assert!(!s.autosave);
164 }
165
166 #[test]
167 fn test_session_with_path() {
168 let s = SessionManager::with_path("/tmp/test.areal".into());
169 assert_eq!(s.data_path.as_deref(), Some("/tmp/test.areal"));
170 assert!(s.workspace_id.is_some());
171 }
172
173 #[test]
174 fn test_autosave_toggle() {
175 let mut s = SessionManager::new();
176 assert!(!s.autosave);
177 s.set_autosave(true);
178 assert!(s.autosave);
179 }
180
181 #[test]
182 fn test_save_no_path_returns_false() {
183 let mut s = SessionManager::new();
184 let result = s.save();
185 assert!(result.is_ok());
186 match result {
187 Ok(saved) => assert!(!saved),
188 Err(_) => panic!("expected Ok(false)"),
189 }
190 }
191
192 #[test]
193 fn test_load_no_path_returns_false() {
194 let mut s = SessionManager::new();
195 let result = s.load();
196 assert!(result.is_ok());
197 match result {
198 Ok(loaded) => assert!(!loaded),
199 Err(_) => panic!("expected Ok(false)"),
200 }
201 }
202
203 #[test]
204 fn test_workspace_id_derivation() {
205 let id = derive_workspace_id("/tmp/test.areal");
206 assert!(id.is_some());
207 match id {
208 Some(ref s) => assert_eq!(s.len(), 16),
209 None => panic!("expected Some"),
210 }
211 }
212
213 #[test]
214 fn test_session_error_display() {
215 let e = SessionError::Save("disk full".into());
216 assert!(e.to_string().contains("disk full"));
217 }
218
219 #[test]
220 fn test_default() {
221 let s = SessionManager::default();
222 assert!(s.data_path.is_none());
223 }
224
225 #[test]
226 fn test_autosave_if_dirty_not_dirty() {
227 let mut s = SessionManager::new();
228 s.set_autosave(true);
229 let result = s.autosave_if_dirty();
230 assert!(result.is_ok());
231 match result {
233 Ok(saved) => assert!(!saved),
234 Err(_) => panic!("expected Ok(false)"),
235 }
236 }
237}