rustant_core/browser/
persistence.rs1use crate::browser::cdp::TabInfo;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::io;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct BrowserConnectionInfo {
16 pub debug_port: u16,
17 pub ws_url: Option<String>,
18 pub user_data_dir: Option<PathBuf>,
19 pub tabs: Vec<TabInfo>,
20 pub active_tab_id: Option<String>,
21 pub saved_at: DateTime<Utc>,
22}
23
24pub struct BrowserSessionStore;
26
27const SESSION_FILE: &str = ".rustant/browser-session.json";
28
29impl BrowserSessionStore {
30 pub fn save(workspace: &Path, info: &BrowserConnectionInfo) -> io::Result<()> {
35 let path = workspace.join(SESSION_FILE);
36 if let Some(parent) = path.parent() {
37 std::fs::create_dir_all(parent)?;
38 }
39 let json = serde_json::to_string_pretty(info).map_err(io::Error::other)?;
40 let tmp_path = path.with_extension("json.tmp");
41 std::fs::write(&tmp_path, &json)?;
42 std::fs::rename(&tmp_path, &path)
43 }
44
45 pub fn load(workspace: &Path) -> io::Result<Option<BrowserConnectionInfo>> {
49 let path = workspace.join(SESSION_FILE);
50 if !path.exists() {
51 return Ok(None);
52 }
53 let json = std::fs::read_to_string(&path)?;
54 let info: BrowserConnectionInfo = serde_json::from_str(&json)
55 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
56
57 let age = Utc::now() - info.saved_at;
59 if age.num_hours() > 24 {
60 let _ = std::fs::remove_file(&path);
62 return Ok(None);
63 }
64
65 Ok(Some(info))
66 }
67
68 pub fn clear(workspace: &Path) -> io::Result<()> {
70 let path = workspace.join(SESSION_FILE);
71 if path.exists() {
72 std::fs::remove_file(&path)?;
73 }
74 Ok(())
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use tempfile::TempDir;
82
83 fn make_info() -> BrowserConnectionInfo {
84 BrowserConnectionInfo {
85 debug_port: 9222,
86 ws_url: Some("ws://127.0.0.1:9222/devtools/browser/abc".to_string()),
87 user_data_dir: None,
88 tabs: vec![
89 TabInfo {
90 id: "tab-0".to_string(),
91 url: "https://example.com".to_string(),
92 title: "Example".to_string(),
93 active: true,
94 },
95 TabInfo {
96 id: "tab-1".to_string(),
97 url: "https://rust-lang.org".to_string(),
98 title: "Rust".to_string(),
99 active: false,
100 },
101 ],
102 active_tab_id: Some("tab-0".to_string()),
103 saved_at: Utc::now(),
104 }
105 }
106
107 #[test]
108 fn test_save_and_load() {
109 let dir = TempDir::new().unwrap();
110 let info = make_info();
111 BrowserSessionStore::save(dir.path(), &info).unwrap();
112
113 let loaded = BrowserSessionStore::load(dir.path()).unwrap().unwrap();
114 assert_eq!(loaded.debug_port, 9222);
115 assert_eq!(loaded.tabs.len(), 2);
116 assert_eq!(loaded.active_tab_id, Some("tab-0".to_string()));
117 assert!(loaded.ws_url.is_some());
118 }
119
120 #[test]
121 fn test_load_missing_returns_none() {
122 let dir = TempDir::new().unwrap();
123 let loaded = BrowserSessionStore::load(dir.path()).unwrap();
124 assert!(loaded.is_none());
125 }
126
127 #[test]
128 fn test_clear_removes_file() {
129 let dir = TempDir::new().unwrap();
130 let info = make_info();
131 BrowserSessionStore::save(dir.path(), &info).unwrap();
132
133 let path = dir.path().join(SESSION_FILE);
134 assert!(path.exists());
135
136 BrowserSessionStore::clear(dir.path()).unwrap();
137 assert!(!path.exists());
138 }
139
140 #[test]
141 fn test_clear_missing_is_ok() {
142 let dir = TempDir::new().unwrap();
143 BrowserSessionStore::clear(dir.path()).unwrap();
144 }
145
146 #[test]
147 fn test_stale_session_discarded() {
148 let dir = TempDir::new().unwrap();
149 let mut info = make_info();
150 info.saved_at = Utc::now() - chrono::Duration::hours(25);
152 BrowserSessionStore::save(dir.path(), &info).unwrap();
153
154 let loaded = BrowserSessionStore::load(dir.path()).unwrap();
155 assert!(loaded.is_none());
156
157 let path = dir.path().join(SESSION_FILE);
159 assert!(!path.exists());
160 }
161}