1use crate::db::MissionDB;
6use crate::types::{Slot, SlotConfig};
7use std::collections::HashMap;
8use std::sync::{Arc, RwLock};
9use tracing::{debug, info};
10
11pub struct SlotManager {
15 slots: Arc<RwLock<HashMap<String, Slot>>>,
16 db: Arc<MissionDB>,
17}
18
19impl SlotManager {
20 pub fn new(db: Arc<MissionDB>) -> Self {
22 Self {
23 slots: Arc::new(RwLock::new(HashMap::new())),
24 db,
25 }
26 }
27
28 pub fn load_slots(&self, configs: Vec<SlotConfig>) {
30 let mut slots = self.slots.write().unwrap();
31
32 for config in configs {
33 let saved_session_id = self.db.get_slot_session(&config.id).ok().flatten();
35
36 let slot = Slot {
37 config: config.clone(),
38 session_id: saved_session_id,
39 };
40
41 info!(slot_id = %config.id, role = %config.role, "Slot loaded");
42 slots.insert(config.id.clone(), slot);
43 }
44 }
45
46 pub fn get_all_slots(&self) -> Vec<Slot> {
48 let slots = self.slots.read().unwrap();
49 slots.values().cloned().collect()
50 }
51
52 pub fn get_slot(&self, slot_id: &str) -> Option<Slot> {
54 let slots = self.slots.read().unwrap();
55 slots.get(slot_id).cloned()
56 }
57
58 pub fn find_slot_by_role(&self, role: &str) -> Option<Slot> {
60 let slots = self.slots.read().unwrap();
61 slots.values().find(|s| s.config.role == role).cloned()
62 }
63
64 pub fn get_slots_by_role(&self, role: &str) -> Vec<Slot> {
66 let slots = self.slots.read().unwrap();
67 slots
68 .values()
69 .filter(|s| s.config.role == role)
70 .cloned()
71 .collect()
72 }
73
74 pub fn update_session(&self, slot_id: &str, session_id: &str) {
76 let mut slots = self.slots.write().unwrap();
77
78 if let Some(slot) = slots.get_mut(slot_id) {
79 slot.session_id = Some(session_id.to_string());
80 let _ = self.db.set_slot_session(slot_id, session_id);
81 debug!(slot_id = %slot_id, session_id = %session_id, "Session updated");
82 }
83 }
84
85 pub fn reset_session(&self, slot_id: &str) {
87 let mut slots = self.slots.write().unwrap();
88
89 if let Some(slot) = slots.get_mut(slot_id) {
90 slot.session_id = None;
91 self.db.clear_slot_session(slot_id);
92 info!(slot_id = %slot_id, "Session reset");
93 }
94 }
95
96 pub fn get_stats(&self) -> SlotStats {
98 let slots = self.slots.read().unwrap();
99 let mut by_role: HashMap<String, usize> = HashMap::new();
100
101 for slot in slots.values() {
102 *by_role.entry(slot.config.role.clone()).or_insert(0) += 1;
103 }
104
105 SlotStats {
106 total: slots.len(),
107 by_role,
108 }
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct SlotStats {
115 pub total: usize,
116 pub by_role: HashMap<String, usize>,
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use tempfile::tempdir;
123
124 fn create_test_db() -> Arc<MissionDB> {
125 let dir = tempdir().unwrap();
126 let db_path = dir.path().join("test.db");
127 Arc::new(MissionDB::open(db_path).unwrap())
128 }
129
130 #[test]
131 fn test_load_and_get_slots() {
132 let db = create_test_db();
133 let manager = SlotManager::new(db);
134
135 let configs = vec![
136 SlotConfig {
137 id: "slot-1".to_string(),
138 role: "worker".to_string(),
139 description: "Worker slot".to_string(),
140 cwd: None,
141 mcp_config: None,
142 auto_start: None,
143 },
144 SlotConfig {
145 id: "slot-2".to_string(),
146 role: "worker".to_string(),
147 description: "Another worker".to_string(),
148 cwd: None,
149 mcp_config: None,
150 auto_start: None,
151 },
152 SlotConfig {
153 id: "slot-3".to_string(),
154 role: "specialist".to_string(),
155 description: "Specialist slot".to_string(),
156 cwd: None,
157 mcp_config: None,
158 auto_start: None,
159 },
160 ];
161
162 manager.load_slots(configs);
163
164 let all = manager.get_all_slots();
166 assert_eq!(all.len(), 3);
167
168 let slot = manager.get_slot("slot-1").unwrap();
170 assert_eq!(slot.config.role, "worker");
171
172 let workers = manager.get_slots_by_role("worker");
174 assert_eq!(workers.len(), 2);
175
176 let specialist = manager.find_slot_by_role("specialist").unwrap();
178 assert_eq!(specialist.config.id, "slot-3");
179 }
180
181 #[test]
182 fn test_session_management() {
183 let db = create_test_db();
184 let manager = SlotManager::new(db);
185
186 let configs = vec![SlotConfig {
187 id: "slot-1".to_string(),
188 role: "worker".to_string(),
189 description: "Worker slot".to_string(),
190 cwd: None,
191 mcp_config: None,
192 auto_start: None,
193 }];
194
195 manager.load_slots(configs);
196
197 let slot = manager.get_slot("slot-1").unwrap();
199 assert!(slot.session_id.is_none());
200
201 manager.update_session("slot-1", "session-abc");
203 let slot = manager.get_slot("slot-1").unwrap();
204 assert_eq!(slot.session_id, Some("session-abc".to_string()));
205
206 manager.reset_session("slot-1");
208 let slot = manager.get_slot("slot-1").unwrap();
209 assert!(slot.session_id.is_none());
210 }
211
212 #[test]
213 fn test_stats() {
214 let db = create_test_db();
215 let manager = SlotManager::new(db);
216
217 let configs = vec![
218 SlotConfig {
219 id: "slot-1".to_string(),
220 role: "worker".to_string(),
221 description: "Worker 1".to_string(),
222 cwd: None,
223 mcp_config: None,
224 auto_start: None,
225 },
226 SlotConfig {
227 id: "slot-2".to_string(),
228 role: "worker".to_string(),
229 description: "Worker 2".to_string(),
230 cwd: None,
231 mcp_config: None,
232 auto_start: None,
233 },
234 SlotConfig {
235 id: "slot-3".to_string(),
236 role: "specialist".to_string(),
237 description: "Specialist".to_string(),
238 cwd: None,
239 mcp_config: None,
240 auto_start: None,
241 },
242 ];
243
244 manager.load_slots(configs);
245
246 let stats = manager.get_stats();
247 assert_eq!(stats.total, 3);
248 assert_eq!(stats.by_role.get("worker"), Some(&2));
249 assert_eq!(stats.by_role.get("specialist"), Some(&1));
250 }
251}