ricecoder_sessions/
context.rs

1//! Context management for sessions
2
3use crate::error::{SessionError, SessionResult};
4use crate::models::SessionContext;
5
6/// Manages session context with isolation between sessions
7///
8/// Each ContextManager instance maintains its own isolated context.
9/// Modifications to one context do not affect other contexts.
10#[derive(Debug, Clone)]
11pub struct ContextManager {
12    /// The current session context
13    context: Option<SessionContext>,
14}
15
16impl ContextManager {
17    /// Create a new context manager
18    pub fn new() -> Self {
19        Self { context: None }
20    }
21
22    /// Create a context manager with an initial context
23    pub fn with_context(context: SessionContext) -> Self {
24        Self {
25            context: Some(context),
26        }
27    }
28
29    /// Set the session context
30    ///
31    /// This replaces the entire context. Each ContextManager maintains
32    /// its own isolated context, so changes here do not affect other
33    /// ContextManager instances.
34    pub fn set_context(&mut self, context: SessionContext) {
35        self.context = Some(context);
36    }
37
38    /// Get the current context
39    ///
40    /// Returns a clone of the context, ensuring isolation between
41    /// different parts of the system.
42    pub fn get_context(&self) -> SessionResult<SessionContext> {
43        self.context
44            .clone()
45            .ok_or_else(|| SessionError::Invalid("No context set".to_string()))
46    }
47
48    /// Get a mutable reference to the context for modifications
49    ///
50    /// This is used internally for operations that need to modify the context.
51    fn get_context_mut(&mut self) -> SessionResult<&mut SessionContext> {
52        self.context
53            .as_mut()
54            .ok_or_else(|| SessionError::Invalid("No context set".to_string()))
55    }
56
57    /// Add a file to the context
58    ///
59    /// Files are stored as paths in the context. Adding a file to one
60    /// context does not affect other contexts.
61    pub fn add_file(&mut self, file_path: String) -> SessionResult<()> {
62        let context = self.get_context_mut()?;
63
64        // Avoid duplicates
65        if !context.files.contains(&file_path) {
66            context.files.push(file_path);
67        }
68
69        Ok(())
70    }
71
72    /// Remove a file from the context
73    ///
74    /// Removes the specified file path from the context. If the file
75    /// is not in the context, this is a no-op.
76    pub fn remove_file(&mut self, file_path: &str) -> SessionResult<()> {
77        let context = self.get_context_mut()?;
78        context.files.retain(|f| f != file_path);
79        Ok(())
80    }
81
82    /// Get all files in the context
83    pub fn get_files(&self) -> SessionResult<Vec<String>> {
84        self.get_context().map(|ctx| ctx.files)
85    }
86
87    /// Clear all files from the context
88    pub fn clear_files(&mut self) -> SessionResult<()> {
89        let context = self.get_context_mut()?;
90        context.files.clear();
91        Ok(())
92    }
93
94    /// Set the project path in the context
95    pub fn set_project_path(&mut self, path: Option<String>) -> SessionResult<()> {
96        let context = self.get_context_mut()?;
97        context.project_path = path;
98        Ok(())
99    }
100
101    /// Get the project path from the context
102    pub fn get_project_path(&self) -> SessionResult<Option<String>> {
103        self.get_context().map(|ctx| ctx.project_path)
104    }
105
106    /// Check if a file is in the context
107    pub fn has_file(&self, file_path: &str) -> SessionResult<bool> {
108        self.get_context()
109            .map(|ctx| ctx.files.contains(&file_path.to_string()))
110    }
111
112    /// Check if context is set
113    pub fn is_set(&self) -> bool {
114        self.context.is_some()
115    }
116
117    /// Clear the entire context
118    pub fn clear(&mut self) {
119        self.context = None;
120    }
121
122    /// Switch to a different project
123    ///
124    /// This updates the project path in the context and clears the file list
125    /// to reflect the new project context.
126    pub fn switch_project(&mut self, project_path: String) -> SessionResult<()> {
127        let context = self.get_context_mut()?;
128        context.project_path = Some(project_path);
129        // Clear files when switching projects
130        context.files.clear();
131        Ok(())
132    }
133
134    /// Get the context for persistence
135    ///
136    /// Returns the current context for saving to disk. This is used by
137    /// the SessionStore to persist context with the session.
138    pub fn get_context_for_persistence(&self) -> SessionResult<SessionContext> {
139        self.get_context()
140    }
141
142    /// Restore context from persistence
143    ///
144    /// Restores the context from a previously saved state. This is used
145    /// by the SessionStore when loading a session from disk.
146    pub fn restore_from_persistence(&mut self, context: SessionContext) {
147        self.context = Some(context);
148    }
149}
150
151impl Default for ContextManager {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::models::SessionMode;
161
162    fn create_test_context() -> SessionContext {
163        SessionContext::new("openai".to_string(), "gpt-4".to_string(), SessionMode::Chat)
164    }
165
166    #[test]
167    fn test_context_manager_new() {
168        let manager = ContextManager::new();
169        assert!(!manager.is_set());
170    }
171
172    #[test]
173    fn test_set_and_get_context() {
174        let mut manager = ContextManager::new();
175        let context = create_test_context();
176
177        manager.set_context(context.clone());
178
179        let retrieved = manager.get_context().unwrap();
180        assert_eq!(retrieved.provider, context.provider);
181        assert_eq!(retrieved.model, context.model);
182    }
183
184    #[test]
185    fn test_add_file() {
186        let mut manager = ContextManager::new();
187        manager.set_context(create_test_context());
188
189        manager.add_file("file1.rs".to_string()).unwrap();
190        manager.add_file("file2.rs".to_string()).unwrap();
191
192        let files = manager.get_files().unwrap();
193        assert_eq!(files.len(), 2);
194        assert!(files.contains(&"file1.rs".to_string()));
195        assert!(files.contains(&"file2.rs".to_string()));
196    }
197
198    #[test]
199    fn test_add_duplicate_file() {
200        let mut manager = ContextManager::new();
201        manager.set_context(create_test_context());
202
203        manager.add_file("file1.rs".to_string()).unwrap();
204        manager.add_file("file1.rs".to_string()).unwrap();
205
206        let files = manager.get_files().unwrap();
207        assert_eq!(files.len(), 1);
208    }
209
210    #[test]
211    fn test_remove_file() {
212        let mut manager = ContextManager::new();
213        manager.set_context(create_test_context());
214
215        manager.add_file("file1.rs".to_string()).unwrap();
216        manager.add_file("file2.rs".to_string()).unwrap();
217
218        manager.remove_file("file1.rs").unwrap();
219
220        let files = manager.get_files().unwrap();
221        assert_eq!(files.len(), 1);
222        assert!(files.contains(&"file2.rs".to_string()));
223    }
224
225    #[test]
226    fn test_context_isolation() {
227        let mut manager1 = ContextManager::new();
228        let mut manager2 = ContextManager::new();
229
230        manager1.set_context(create_test_context());
231        manager2.set_context(create_test_context());
232
233        manager1.add_file("file1.rs".to_string()).unwrap();
234        manager2.add_file("file2.rs".to_string()).unwrap();
235
236        let files1 = manager1.get_files().unwrap();
237        let files2 = manager2.get_files().unwrap();
238
239        assert_eq!(files1.len(), 1);
240        assert_eq!(files2.len(), 1);
241        assert!(files1.contains(&"file1.rs".to_string()));
242        assert!(files2.contains(&"file2.rs".to_string()));
243    }
244
245    #[test]
246    fn test_clear_files() {
247        let mut manager = ContextManager::new();
248        manager.set_context(create_test_context());
249
250        manager.add_file("file1.rs".to_string()).unwrap();
251        manager.add_file("file2.rs".to_string()).unwrap();
252
253        manager.clear_files().unwrap();
254
255        let files = manager.get_files().unwrap();
256        assert_eq!(files.len(), 0);
257    }
258
259    #[test]
260    fn test_project_path() {
261        let mut manager = ContextManager::new();
262        manager.set_context(create_test_context());
263
264        manager
265            .set_project_path(Some("/path/to/project".to_string()))
266            .unwrap();
267
268        let path = manager.get_project_path().unwrap();
269        assert_eq!(path, Some("/path/to/project".to_string()));
270    }
271
272    #[test]
273    fn test_has_file() {
274        let mut manager = ContextManager::new();
275        manager.set_context(create_test_context());
276
277        manager.add_file("file1.rs".to_string()).unwrap();
278
279        assert!(manager.has_file("file1.rs").unwrap());
280        assert!(!manager.has_file("file2.rs").unwrap());
281    }
282
283    #[test]
284    fn test_clear_context() {
285        let mut manager = ContextManager::new();
286        manager.set_context(create_test_context());
287
288        assert!(manager.is_set());
289
290        manager.clear();
291
292        assert!(!manager.is_set());
293        assert!(manager.get_context().is_err());
294    }
295
296    #[test]
297    fn test_operations_without_context() {
298        let mut manager = ContextManager::new();
299
300        assert!(manager.get_context().is_err());
301        assert!(manager.add_file("file.rs".to_string()).is_err());
302        assert!(manager.remove_file("file.rs").is_err());
303    }
304
305    #[test]
306    fn test_switch_project() {
307        let mut manager = ContextManager::new();
308        manager.set_context(create_test_context());
309
310        manager.add_file("file1.rs".to_string()).unwrap();
311        manager.add_file("file2.rs".to_string()).unwrap();
312
313        manager.switch_project("/new/project".to_string()).unwrap();
314
315        let path = manager.get_project_path().unwrap();
316        assert_eq!(path, Some("/new/project".to_string()));
317
318        // Files should be cleared when switching projects
319        let files = manager.get_files().unwrap();
320        assert_eq!(files.len(), 0);
321    }
322
323    #[test]
324    fn test_persistence_roundtrip() {
325        let mut manager1 = ContextManager::new();
326        let context = create_test_context();
327        manager1.set_context(context);
328
329        manager1.add_file("file1.rs".to_string()).unwrap();
330        manager1.add_file("file2.rs".to_string()).unwrap();
331        manager1
332            .set_project_path(Some("/project".to_string()))
333            .unwrap();
334
335        // Get context for persistence
336        let persisted_context = manager1.get_context_for_persistence().unwrap();
337
338        // Create new manager and restore
339        let mut manager2 = ContextManager::new();
340        manager2.restore_from_persistence(persisted_context);
341
342        // Verify context is restored
343        let restored_context = manager2.get_context().unwrap();
344        assert_eq!(restored_context.provider, "openai");
345        assert_eq!(restored_context.model, "gpt-4");
346        assert_eq!(restored_context.project_path, Some("/project".to_string()));
347
348        let files = manager2.get_files().unwrap();
349        assert_eq!(files.len(), 2);
350        assert!(files.contains(&"file1.rs".to_string()));
351        assert!(files.contains(&"file2.rs".to_string()));
352    }
353
354    #[test]
355    fn test_context_isolation_with_operations() {
356        let mut manager1 = ContextManager::new();
357        let mut manager2 = ContextManager::new();
358
359        manager1.set_context(create_test_context());
360        manager2.set_context(create_test_context());
361
362        // Perform different operations on each manager
363        manager1.add_file("file1.rs".to_string()).unwrap();
364        manager1
365            .set_project_path(Some("/project1".to_string()))
366            .unwrap();
367
368        manager2.add_file("file2.rs".to_string()).unwrap();
369        manager2
370            .set_project_path(Some("/project2".to_string()))
371            .unwrap();
372
373        // Verify isolation
374        let context1 = manager1.get_context().unwrap();
375        let context2 = manager2.get_context().unwrap();
376
377        assert_eq!(context1.project_path, Some("/project1".to_string()));
378        assert_eq!(context2.project_path, Some("/project2".to_string()));
379
380        assert_eq!(context1.files.len(), 1);
381        assert_eq!(context2.files.len(), 1);
382        assert!(context1.files.contains(&"file1.rs".to_string()));
383        assert!(context2.files.contains(&"file2.rs".to_string()));
384    }
385}