Skip to main content

missiond_core/core/
slot_manager.rs

1//! Slot Manager - Workstation configuration management
2//!
3//! Manages slot configurations. Process state is handled by ProcessManager.
4
5use crate::db::MissionDB;
6use crate::types::{Slot, SlotConfig};
7use std::collections::HashMap;
8use std::sync::{Arc, RwLock};
9use tracing::{debug, info};
10
11/// Slot Manager
12///
13/// Manages workstation configurations (not process state, which is managed by ProcessManager)
14pub struct SlotManager {
15    slots: Arc<RwLock<HashMap<String, Slot>>>,
16    db: Arc<MissionDB>,
17}
18
19impl SlotManager {
20    /// Create a new SlotManager
21    pub fn new(db: Arc<MissionDB>) -> Self {
22        Self {
23            slots: Arc::new(RwLock::new(HashMap::new())),
24            db,
25        }
26    }
27
28    /// Load slot configurations
29    pub fn load_slots(&self, configs: Vec<SlotConfig>) {
30        let mut slots = self.slots.write().unwrap();
31
32        for config in configs {
33            // Restore session_id from database
34            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    /// Get all slots
47    pub fn get_all_slots(&self) -> Vec<Slot> {
48        let slots = self.slots.read().unwrap();
49        slots.values().cloned().collect()
50    }
51
52    /// Get a slot by ID
53    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    /// Find a slot by role
59    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    /// Get all slots with a specific role
65    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    /// Update a slot's session
75    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    /// Reset a slot's session
86    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    /// Get statistics (config stats only, no process state)
97    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/// Slot statistics
113#[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        // Test get_all_slots
165        let all = manager.get_all_slots();
166        assert_eq!(all.len(), 3);
167
168        // Test get_slot
169        let slot = manager.get_slot("slot-1").unwrap();
170        assert_eq!(slot.config.role, "worker");
171
172        // Test get_slots_by_role
173        let workers = manager.get_slots_by_role("worker");
174        assert_eq!(workers.len(), 2);
175
176        // Test find_slot_by_role
177        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        // Initially no session
198        let slot = manager.get_slot("slot-1").unwrap();
199        assert!(slot.session_id.is_none());
200
201        // Update session
202        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        // Reset session
207        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}