1use serde::Serialize;
2use std::fs;
3use std::path::PathBuf;
4
5#[derive(Debug, Serialize)]
6pub struct SessionSummary {
11 pub id: String,
12 pub created_at: String,
13 pub message_count: usize,
14 pub size_bytes: u64,
15}
16
17#[derive(Debug, Serialize)]
18pub struct SessionDetail {
23 pub id: String,
24 pub messages: serde_json::Value,
25}
26
27fn sessions_dir() -> PathBuf {
28 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
29 PathBuf::from(home).join(".pawan").join("sessions")
30}
31
32pub fn list_sessions() -> Result<Vec<SessionSummary>, String> {
40 let dir = sessions_dir();
41 if !dir.exists() {
42 return Ok(vec![]);
43 }
44
45 let mut sessions = Vec::new();
46 let entries = fs::read_dir(&dir).map_err(|e| e.to_string())?;
47
48 for entry in entries.flatten() {
49 let path = entry.path();
50 if path.extension().and_then(|e| e.to_str()) != Some("json") {
51 continue;
52 }
53
54 let id = path
55 .file_stem()
56 .and_then(|s| s.to_str())
57 .unwrap_or("unknown")
58 .to_string();
59
60 let metadata = fs::metadata(&path).map_err(|e| e.to_string())?;
61 let size_bytes = metadata.len();
62
63 let created_at = metadata
64 .created()
65 .or_else(|_| metadata.modified())
66 .map(|t| {
67 let dt: chrono::DateTime<chrono::Utc> = t.into();
68 dt.to_rfc3339()
69 })
70 .unwrap_or_default();
71
72 let message_count = fs::read_to_string(&path)
74 .ok()
75 .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
76 .and_then(|v| {
77 v.get("messages")
78 .and_then(|m| m.as_array())
79 .map(|a| a.len())
80 })
81 .unwrap_or(0);
82
83 sessions.push(SessionSummary {
84 id,
85 created_at,
86 message_count,
87 size_bytes,
88 });
89 }
90
91 sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at));
92 Ok(sessions)
93}
94
95pub fn get_session(id: &str) -> Result<SessionDetail, String> {
106 let path = sessions_dir().join(format!("{}.json", id));
107 if !path.exists() {
108 return Err(format!("session '{}' not found", id));
109 }
110
111 let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
112 let messages: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
113
114 Ok(SessionDetail {
115 id: id.to_string(),
116 messages,
117 })
118}
119
120pub fn delete_session(id: &str) -> Result<(), String> {
131 let path = sessions_dir().join(format!("{}.json", id));
132 if !path.exists() {
133 return Err(format!("session '{}' not found", id));
134 }
135 fs::remove_file(&path).map_err(|e| e.to_string())
136}
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use std::fs;
141 use tempfile::TempDir;
142
143 #[test]
144 fn test_sessions_dir_uses_home_env() {
145 let dir = sessions_dir();
146 assert!(dir.ends_with(".pawan/sessions"));
147 }
148
149 #[test]
150 fn test_list_sessions_empty_directory() {
151 let tmp = TempDir::new().unwrap();
152 let sessions_path = tmp.path().join(".pawan").join("sessions");
153 fs::create_dir_all(&sessions_path).unwrap();
154
155 std::env::set_var("HOME", tmp.path());
157 let sessions = list_sessions().unwrap();
158 std::env::remove_var("HOME");
159
160 assert!(sessions.is_empty());
161 }
162
163 #[test]
164 fn test_list_sessions_nonexistent_directory() {
165 std::env::set_var("HOME", "/nonexistent/path/that/does/not/exist");
167 let sessions = list_sessions().unwrap();
168 std::env::remove_var("HOME");
169
170 assert!(sessions.is_empty());
171 }
172
173 #[test]
174 fn test_get_session_not_found() {
175 let tmp = TempDir::new().unwrap();
176 std::env::set_var("HOME", tmp.path());
177
178 let result = get_session("nonexistent");
179 std::env::remove_var("HOME");
180
181 assert!(result.is_err());
182 assert!(result.unwrap_err().contains("not found"));
183 }
184
185 #[test]
186 fn test_delete_session_not_found() {
187 let tmp = TempDir::new().unwrap();
188 std::env::set_var("HOME", tmp.path());
189
190 let result = delete_session("nonexistent");
191 std::env::remove_var("HOME");
192
193 assert!(result.is_err());
194 assert!(result.unwrap_err().contains("not found"));
195 }
196
197 #[test]
198 fn test_delete_session_success() {
199 let tmp = TempDir::new().unwrap();
200 let sessions_path = tmp.path().join(".pawan").join("sessions");
201 fs::create_dir_all(&sessions_path).unwrap();
202
203 let session_path = sessions_path.join("test.json");
204 fs::write(&session_path, r#"{"messages":[]}"#).unwrap();
205
206 std::env::set_var("HOME", tmp.path());
207 let result = delete_session("test");
208 std::env::remove_var("HOME");
209
210 assert!(result.is_ok());
211 assert!(!session_path.exists());
212 }
213
214 #[test]
215 fn test_get_session_success() {
216 let tmp = TempDir::new().unwrap();
217 let sessions_path = tmp.path().join(".pawan").join("sessions");
218 fs::create_dir_all(&sessions_path).unwrap();
219
220 let session_path = sessions_path.join("test.json");
221 let content = r#"{"messages":[{"role":"user","content":"hello"}]}"#;
222 fs::write(&session_path, content).unwrap();
223
224 std::env::set_var("HOME", tmp.path());
225 let result = get_session("test");
226 std::env::remove_var("HOME");
227
228 assert!(result.is_ok());
229 let session = result.unwrap();
230 assert_eq!(session.id, "test");
231 assert!(session.messages.is_object());
232 }
233}