ta_changeset/
review_session_store.rs1use std::fs;
7use std::path::PathBuf;
8
9use uuid::Uuid;
10
11use crate::review_session::ReviewSession;
12use crate::ChangeSetError;
13
14pub struct ReviewSessionStore {
16 sessions_dir: PathBuf,
17}
18
19impl ReviewSessionStore {
20 pub fn new(sessions_dir: PathBuf) -> Result<Self, ChangeSetError> {
22 fs::create_dir_all(&sessions_dir)?;
23 Ok(Self { sessions_dir })
24 }
25
26 pub fn save(&self, session: &ReviewSession) -> Result<(), ChangeSetError> {
28 let path = self.session_path(session.session_id);
29 let json = serde_json::to_string_pretty(session)?;
30 fs::write(&path, json)?;
31 Ok(())
32 }
33
34 pub fn load(&self, session_id: Uuid) -> Result<ReviewSession, ChangeSetError> {
36 let path = self.session_path(session_id);
37 if !path.exists() {
38 return Err(ChangeSetError::InvalidData(format!(
39 "Review session not found: {}",
40 session_id
41 )));
42 }
43 let json = fs::read_to_string(&path)?;
44 let session = serde_json::from_str(&json)?;
45 Ok(session)
46 }
47
48 pub fn list(&self) -> Result<Vec<ReviewSession>, ChangeSetError> {
50 let mut sessions = Vec::new();
51 if !self.sessions_dir.exists() {
52 return Ok(sessions);
53 }
54
55 for entry in fs::read_dir(&self.sessions_dir)? {
56 let entry = entry?;
57 let path = entry.path();
58 if path.extension().is_some_and(|ext| ext == "json") {
59 if let Ok(json) = fs::read_to_string(&path) {
60 if let Ok(session) = serde_json::from_str::<ReviewSession>(&json) {
61 sessions.push(session);
62 }
63 }
64 }
65 }
66
67 sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
69 Ok(sessions)
70 }
71
72 pub fn find_active_for_draft(
74 &self,
75 draft_package_id: Uuid,
76 ) -> Result<Option<ReviewSession>, ChangeSetError> {
77 let sessions = self.list()?;
78 Ok(sessions.into_iter().find(|s| {
79 s.draft_package_id == draft_package_id
80 && s.state == crate::review_session::ReviewState::Active
81 }))
82 }
83
84 pub fn delete(&self, session_id: Uuid) -> Result<(), ChangeSetError> {
86 let path = self.session_path(session_id);
87 if path.exists() {
88 fs::remove_file(&path)?;
89 }
90 Ok(())
91 }
92
93 fn session_path(&self, session_id: Uuid) -> PathBuf {
95 self.sessions_dir.join(format!("{}.json", session_id))
96 }
97
98 pub fn exists(&self, session_id: Uuid) -> bool {
100 self.session_path(session_id).exists()
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use tempfile::TempDir;
108
109 #[test]
110 fn save_and_load_session() {
111 let temp = TempDir::new().unwrap();
112 let store = ReviewSessionStore::new(temp.path().to_path_buf()).unwrap();
113
114 let mut session = ReviewSession::new(Uuid::new_v4(), "reviewer-1".to_string());
115 session.add_comment("fs://workspace/main.rs", "reviewer-1", "Looks good");
116 session.set_disposition(
117 "fs://workspace/main.rs",
118 crate::draft_package::ArtifactDisposition::Approved,
119 );
120
121 store.save(&session).unwrap();
122
123 let loaded = store.load(session.session_id).unwrap();
124 assert_eq!(loaded.session_id, session.session_id);
125 assert_eq!(loaded.reviewer, session.reviewer);
126 assert_eq!(loaded.artifact_reviews.len(), 1);
127 }
128
129 #[test]
130 fn list_sessions_returns_all() {
131 let temp = TempDir::new().unwrap();
132 let store = ReviewSessionStore::new(temp.path().to_path_buf()).unwrap();
133
134 let session1 = ReviewSession::new(Uuid::new_v4(), "reviewer-1".to_string());
135 let session2 = ReviewSession::new(Uuid::new_v4(), "reviewer-2".to_string());
136
137 store.save(&session1).unwrap();
138 store.save(&session2).unwrap();
139
140 let sessions = store.list().unwrap();
141 assert_eq!(sessions.len(), 2);
142 }
143
144 #[test]
145 fn find_active_for_draft_returns_active_session() {
146 let temp = TempDir::new().unwrap();
147 let store = ReviewSessionStore::new(temp.path().to_path_buf()).unwrap();
148
149 let draft_id = Uuid::new_v4();
150 let session = ReviewSession::new(draft_id, "reviewer-1".to_string());
151 store.save(&session).unwrap();
152
153 let found = store.find_active_for_draft(draft_id).unwrap();
154 assert!(found.is_some());
155 assert_eq!(found.unwrap().draft_package_id, draft_id);
156 }
157
158 #[test]
159 fn find_active_for_draft_returns_none_when_no_active() {
160 let temp = TempDir::new().unwrap();
161 let store = ReviewSessionStore::new(temp.path().to_path_buf()).unwrap();
162
163 let draft_id = Uuid::new_v4();
164 let mut session = ReviewSession::new(draft_id, "reviewer-1".to_string());
165 session.state = crate::review_session::ReviewState::Completed;
166 store.save(&session).unwrap();
167
168 let found = store.find_active_for_draft(draft_id).unwrap();
169 assert!(found.is_none());
170 }
171
172 #[test]
173 fn delete_removes_session() {
174 let temp = TempDir::new().unwrap();
175 let store = ReviewSessionStore::new(temp.path().to_path_buf()).unwrap();
176
177 let session = ReviewSession::new(Uuid::new_v4(), "reviewer-1".to_string());
178 let session_id = session.session_id;
179
180 store.save(&session).unwrap();
181 assert!(store.exists(session_id));
182
183 store.delete(session_id).unwrap();
184 assert!(!store.exists(session_id));
185 }
186
187 #[test]
188 fn exists_returns_false_for_nonexistent_session() {
189 let temp = TempDir::new().unwrap();
190 let store = ReviewSessionStore::new(temp.path().to_path_buf()).unwrap();
191
192 assert!(!store.exists(Uuid::new_v4()));
193 }
194}