libbrat_grite/
reconcile.rs1use std::collections::{HashMap, HashSet};
9
10use crate::types::{Session, SessionStatus};
11use crate::GriteClient;
12use crate::GriteError;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ReconciliationAction {
17 InSync {
19 session_id: String,
20 },
21
22 MarkCrashed {
24 session_id: String,
25 grite_issue_id: String,
26 task_id: String,
27 },
28
29 Orphaned {
32 session_id: String,
33 },
34
35 UpdateStatus {
37 session_id: String,
38 current_status: SessionStatus,
39 new_status: SessionStatus,
40 },
41}
42
43#[derive(Debug, Default)]
45pub struct ReconciliationResult {
46 pub actions: Vec<ReconciliationAction>,
48
49 pub in_sync_count: usize,
51
52 pub crashed_count: usize,
54
55 pub orphaned_count: usize,
57
58 pub status_update_count: usize,
60}
61
62impl ReconciliationResult {
63 pub fn is_clean(&self) -> bool {
65 self.crashed_count == 0 && self.orphaned_count == 0 && self.status_update_count == 0
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct EngineSessionInfo {
72 pub session_id: String,
74
75 pub alive: bool,
77
78 pub exit_code: Option<i32>,
80}
81
82pub fn reconcile_sessions(
96 grite_client: &GriteClient,
97 engine_sessions: &[EngineSessionInfo],
98) -> Result<ReconciliationResult, GriteError> {
99 let mut result = ReconciliationResult::default();
100
101 let grite_sessions = grite_client.session_list(None)?;
103 let grite_active: HashMap<String, Session> = grite_sessions
104 .into_iter()
105 .filter(|s| s.status != SessionStatus::Exit)
106 .map(|s| (s.session_id.clone(), s))
107 .collect();
108
109 let _engine_ids: HashSet<&str> = engine_sessions
111 .iter()
112 .map(|s| s.session_id.as_str())
113 .collect();
114
115 for (session_id, session) in &grite_active {
117 if let Some(engine_info) = engine_sessions.iter().find(|e| e.session_id == *session_id) {
118 if engine_info.alive {
120 result.actions.push(ReconciliationAction::InSync {
122 session_id: session_id.clone(),
123 });
124 result.in_sync_count += 1;
125 } else {
126 result.actions.push(ReconciliationAction::MarkCrashed {
128 session_id: session_id.clone(),
129 grite_issue_id: session.grite_issue_id.clone(),
130 task_id: session.task_id.clone(),
131 });
132 result.crashed_count += 1;
133 }
134 } else {
135 result.actions.push(ReconciliationAction::MarkCrashed {
137 session_id: session_id.clone(),
138 grite_issue_id: session.grite_issue_id.clone(),
139 task_id: session.task_id.clone(),
140 });
141 result.crashed_count += 1;
142 }
143 }
144
145 for engine_info in engine_sessions {
147 if !grite_active.contains_key(&engine_info.session_id) {
148 result.actions.push(ReconciliationAction::Orphaned {
149 session_id: engine_info.session_id.clone(),
150 });
151 result.orphaned_count += 1;
152 }
153 }
154
155 Ok(result)
156}
157
158pub fn execute_reconciliation(
171 grite_client: &GriteClient,
172 actions: &[ReconciliationAction],
173) -> (usize, Vec<GriteError>) {
174 let mut success_count = 0;
175 let mut errors = Vec::new();
176
177 for action in actions {
178 match action {
179 ReconciliationAction::MarkCrashed { session_id, .. } => {
180 match grite_client.session_exit(session_id, -1, "crash", None) {
181 Ok(()) => success_count += 1,
182 Err(e) => errors.push(e),
183 }
184 }
185 ReconciliationAction::UpdateStatus {
186 session_id,
187 new_status,
188 ..
189 } => {
190 match grite_client.session_update_status_with_options(session_id, *new_status, true)
192 {
193 Ok(()) => success_count += 1,
194 Err(e) => errors.push(e),
195 }
196 }
197 ReconciliationAction::InSync { .. } => {
198 success_count += 1;
200 }
201 ReconciliationAction::Orphaned { session_id } => {
202 eprintln!(
205 "Warning: orphaned session {} found in engine but not in Grit",
206 session_id
207 );
208 success_count += 1;
209 }
210 }
211 }
212
213 (success_count, errors)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_reconciliation_result_is_clean() {
222 let clean = ReconciliationResult::default();
223 assert!(clean.is_clean());
224
225 let not_clean = ReconciliationResult {
226 crashed_count: 1,
227 ..Default::default()
228 };
229 assert!(!not_clean.is_clean());
230 }
231
232 #[test]
233 fn test_engine_session_info() {
234 let info = EngineSessionInfo {
235 session_id: "s-20250117-test".to_string(),
236 alive: true,
237 exit_code: None,
238 };
239 assert!(info.alive);
240
241 let dead_info = EngineSessionInfo {
242 session_id: "s-20250117-dead".to_string(),
243 alive: false,
244 exit_code: Some(1),
245 };
246 assert!(!dead_info.alive);
247 assert_eq!(dead_info.exit_code, Some(1));
248 }
249}