Skip to main content

saorsa_agent/session/
path.rs

1//! Filesystem path utilities for session storage.
2
3use crate::SaorsaAgentError;
4use crate::session::SessionId;
5use std::path::{Path, PathBuf};
6
7/// Get the base directory for session storage.
8///
9/// Uses XDG Base Directory specification:
10/// - `$XDG_DATA_HOME/saorsa/sessions` if XDG_DATA_HOME is set
11/// - `~/.saorsa/sessions` otherwise
12pub fn sessions_dir() -> Result<PathBuf, SaorsaAgentError> {
13    let base = if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") {
14        PathBuf::from(xdg_data).join("saorsa")
15    } else if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"))
16    {
17        PathBuf::from(home).join(".saorsa")
18    } else {
19        return Err(SaorsaAgentError::Session(
20            "Cannot determine home directory".to_string(),
21        ));
22    };
23
24    Ok(base.join("sessions"))
25}
26
27/// Get the directory for a specific session.
28pub fn session_dir(session_id: &SessionId) -> Result<PathBuf, SaorsaAgentError> {
29    Ok(sessions_dir()?.join(session_id.as_str()))
30}
31
32/// Get the path to the manifest file for a session.
33pub fn manifest_path(session_id: &SessionId) -> Result<PathBuf, SaorsaAgentError> {
34    Ok(session_dir(session_id)?.join("manifest.json"))
35}
36
37/// Get the path to the tree file for a session.
38pub fn tree_path(session_id: &SessionId) -> Result<PathBuf, SaorsaAgentError> {
39    Ok(session_dir(session_id)?.join("tree.json"))
40}
41
42/// Get the messages directory for a session.
43pub fn messages_dir(session_id: &SessionId) -> Result<PathBuf, SaorsaAgentError> {
44    Ok(session_dir(session_id)?.join("messages"))
45}
46
47/// Get the path for a specific message file.
48///
49/// Format: `messages/{index}-{type}.json`
50/// Example: `messages/0-user.json`, `messages/1-assistant.json`
51pub fn message_path(
52    session_id: &SessionId,
53    index: usize,
54    message_type: &str,
55) -> Result<PathBuf, SaorsaAgentError> {
56    Ok(messages_dir(session_id)?.join(format!("{}-{}.json", index, message_type)))
57}
58
59/// Ensure a directory exists, creating it if necessary.
60pub fn ensure_dir(path: &Path) -> Result<(), SaorsaAgentError> {
61    std::fs::create_dir_all(path).map_err(|e| {
62        SaorsaAgentError::Session(format!("Failed to create directory {:?}: {}", path, e))
63    })?;
64    Ok(())
65}
66
67#[cfg(test)]
68#[allow(clippy::unwrap_used)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_sessions_dir_uses_xdg() {
74        unsafe {
75            std::env::set_var("XDG_DATA_HOME", "/tmp/xdg_test");
76        }
77        let path = sessions_dir().unwrap();
78        assert!(path.to_string_lossy().contains("xdg_test"));
79        assert!(path.ends_with("saorsa/sessions"));
80        unsafe {
81            std::env::remove_var("XDG_DATA_HOME");
82        }
83    }
84
85    #[test]
86    fn test_sessions_dir_falls_back_to_home() {
87        unsafe {
88            std::env::remove_var("XDG_DATA_HOME");
89        }
90        let path = sessions_dir().unwrap();
91        assert!(path.to_string_lossy().contains(".saorsa"));
92        assert!(path.ends_with(".saorsa/sessions"));
93    }
94
95    #[test]
96    fn test_session_dir_includes_id() {
97        let id = SessionId::new();
98        let path = session_dir(&id).unwrap();
99        assert!(path.to_string_lossy().contains(&id.as_str()));
100    }
101
102    #[test]
103    fn test_manifest_path() {
104        let id = SessionId::new();
105        let p = manifest_path(&id).unwrap();
106        assert!(p.ends_with("manifest.json"));
107        assert!(p.to_string_lossy().contains(&id.as_str()));
108    }
109
110    #[test]
111    fn test_tree_path() {
112        let id = SessionId::new();
113        let p = tree_path(&id).unwrap();
114        assert!(p.ends_with("tree.json"));
115    }
116
117    #[test]
118    fn test_messages_dir() {
119        let id = SessionId::new();
120        let p = messages_dir(&id).unwrap();
121        assert!(p.ends_with("messages"));
122    }
123
124    #[test]
125    fn test_message_path_format() {
126        let id = SessionId::new();
127        let p = message_path(&id, 0, "user").unwrap();
128        assert!(p.ends_with(Path::new("messages").join("0-user.json")));
129
130        let p2 = message_path(&id, 42, "assistant").unwrap();
131        assert!(p2.ends_with(Path::new("messages").join("42-assistant.json")));
132    }
133}