1use crate::core::types::BackendMode;
24use crate::platform::path_bridge;
25use crate::platform::service::{self, ServiceConfig, ServiceRegistry};
26use crate::platform::wsl;
27use serde::{Deserialize, Serialize};
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35pub struct ControlResult {
36 pub operation: ControlOperation,
38 pub success: bool,
40 pub message: String,
42 pub data: serde_json::Value,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48pub enum ControlOperation {
49 StatusCheck,
51 PathTranslate,
53 ServiceList,
55 HealthCheck,
57 WslCheck,
59}
60
61#[derive(Debug, Clone)]
70pub struct ControlShell {
71 backend_mode: BackendMode,
73 services: ServiceRegistry,
75}
76
77impl ControlShell {
78 pub fn new(backend_mode: BackendMode) -> Self {
80 Self {
81 backend_mode,
82 services: ServiceRegistry::new(),
83 }
84 }
85
86 pub fn auto() -> Self {
88 Self::new(crate::platform::windows::detect_backend_mode())
89 }
90
91 pub fn register_service(&mut self, config: ServiceConfig) -> Result<(), service::ServiceError> {
93 self.services.register(config)
94 }
95
96 pub fn backend_mode(&self) -> BackendMode {
98 self.backend_mode
99 }
100
101 pub fn services(&self) -> &ServiceRegistry {
103 &self.services
104 }
105
106 pub fn status_check(&self) -> ControlResult {
115 let wsl_status = wsl::detect_wsl2();
116
117 let data = serde_json::json!({
118 "backend_mode": self.backend_mode,
119 "wsl2": wsl_status,
120 });
121
122 ControlResult {
123 operation: ControlOperation::StatusCheck,
124 success: true,
125 message: format!(
126 "Platform: {:?} | WSL2: {}",
127 self.backend_mode,
128 match &wsl_status {
129 wsl::WslStatus::NotInstalled => "not installed".to_string(),
130 wsl::WslStatus::Available { distro, .. } => format!("available ({})", distro),
131 wsl::WslStatus::Error { message } => format!("error: {}", message),
132 }
133 ),
134 data,
135 }
136 }
137
138 pub fn translate_path(&self, path: &str) -> ControlResult {
142 let result = path_bridge::translate_path(path);
143
144 let data = serde_json::json!({
145 "original": path,
146 "translated": result.translated,
147 "direction": result.direction,
148 });
149
150 ControlResult {
151 operation: ControlOperation::PathTranslate,
152 success: true,
153 message: format!("{} → {}", path, result.translated),
154 data,
155 }
156 }
157
158 pub fn list_services(&self) -> ControlResult {
162 let services: Vec<serde_json::Value> = self
163 .services
164 .all()
165 .iter()
166 .map(|s| {
167 serde_json::json!({
168 "name": s.name,
169 "port": s.port,
170 "enabled": s.enabled,
171 "address": s.socket_addr().to_string(),
172 "health_url": s.health_url(),
173 })
174 })
175 .collect();
176
177 let count = services.len();
178 let data = serde_json::json!({ "services": services });
179
180 ControlResult {
181 operation: ControlOperation::ServiceList,
182 success: true,
183 message: format!("{} service(s) registered", count),
184 data,
185 }
186 }
187
188 pub fn check_health(&self) -> ControlResult {
193 let urls = self.services.health_urls();
194 let all_local = self.services.all_local_only();
195
196 let checks: Vec<serde_json::Value> = urls
197 .iter()
198 .map(|url| {
199 serde_json::json!({
200 "url": url,
201 "local_only": all_local,
202 })
203 })
204 .collect();
205
206 let data = serde_json::json!({
207 "checks": checks,
208 "all_local_only": all_local,
209 });
210
211 ControlResult {
212 operation: ControlOperation::HealthCheck,
213 success: all_local,
214 message: format!(
215 "{} health endpoint(s) — all local-only: {}",
216 checks.len(),
217 all_local
218 ),
219 data,
220 }
221 }
222
223 pub fn wsl_check(&self) -> ControlResult {
228 let status = wsl::detect_wsl2();
229 let health = wsl::check_wsl_health(&status);
230
231 let all_healthy = health.iter().all(|h| h.success);
232
233 let data = serde_json::json!({
234 "status": status,
235 "health": health,
236 });
237
238 ControlResult {
239 operation: ControlOperation::WslCheck,
240 success: all_healthy,
241 message: match &status {
242 wsl::WslStatus::NotInstalled => "WSL2 not installed".to_string(),
243 wsl::WslStatus::Available { distro, .. } => {
244 let healthy_count = health.iter().filter(|h| h.success).count();
245 format!(
246 "WSL2 available ({}) — {}/{} checks passed",
247 distro,
248 healthy_count,
249 health.len()
250 )
251 }
252 wsl::WslStatus::Error { message } => format!("WSL2 error: {}", message),
253 },
254 data,
255 }
256 }
257}
258
259#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
272 fn test_control_shell_status_check() {
273 let shell = ControlShell::auto();
274 let result = shell.status_check();
275 assert_eq!(result.operation, ControlOperation::StatusCheck);
276 assert!(result.success);
277 assert!(!result.message.is_empty());
278 }
279
280 #[test]
281 fn test_control_shell_status_check_has_backend_mode() {
282 let shell = ControlShell::auto();
283 let result = shell.status_check();
284 let data = &result.data;
285 assert!(data.get("backend_mode").is_some());
286 }
287
288 #[test]
289 fn test_control_shell_status_check_has_wsl_info() {
290 let shell = ControlShell::auto();
291 let result = shell.status_check();
292 let data = &result.data;
293 assert!(data.get("wsl2").is_some());
294 }
295
296 #[test]
297 fn test_control_shell_translate_path_windows() {
298 let shell = ControlShell::new(BackendMode::WindowsNative);
299 let result = shell.translate_path(r"C:\Users\alice");
300 assert_eq!(result.operation, ControlOperation::PathTranslate);
301 assert!(result.success);
302 assert!(result.message.contains("/mnt/c/"));
303 }
304
305 #[test]
306 fn test_control_shell_translate_path_linux() {
307 let shell = ControlShell::new(BackendMode::LinuxNative);
308 let result = shell.translate_path("/home/alice");
309 assert_eq!(result.operation, ControlOperation::PathTranslate);
310 assert!(result.success);
311 assert!(result.message.contains("wsl$"));
312 }
313
314 #[test]
315 fn test_control_shell_list_services_empty() {
316 let shell = ControlShell::auto();
317 let result = shell.list_services();
318 assert_eq!(result.operation, ControlOperation::ServiceList);
319 assert!(result.success);
320 assert!(result.message.contains("0 service"));
321 }
322
323 #[test]
324 fn test_control_shell_list_services_with_registered() {
325 let mut shell = ControlShell::auto();
326 shell
327 .register_service(ServiceConfig::new("api", 8080))
328 .unwrap();
329
330 let result = shell.list_services();
331 assert!(result.success);
332 assert!(result.message.contains("1 service"));
333 }
334
335 #[test]
336 fn test_control_shell_health_check_no_shell_subprocess() {
337 let mut shell = ControlShell::auto();
338 shell
339 .register_service(ServiceConfig::new("api", 8080))
340 .unwrap();
341
342 let result = shell.check_health();
343 assert_eq!(result.operation, ControlOperation::HealthCheck);
344 assert!(result.success);
345 let all_local = result
347 .data
348 .get("all_local_only")
349 .unwrap()
350 .as_bool()
351 .unwrap();
352 assert!(all_local);
353 }
354
355 #[test]
356 fn test_control_shell_wsl_check() {
357 let shell = ControlShell::auto();
358 let result = shell.wsl_check();
359 assert_eq!(result.operation, ControlOperation::WslCheck);
360 assert!(!result.message.is_empty());
363 }
364
365 #[test]
366 fn test_control_shell_backend_mode() {
367 let shell = ControlShell::new(BackendMode::WindowsNative);
368 assert_eq!(shell.backend_mode(), BackendMode::WindowsNative);
369 }
370
371 #[test]
376 fn test_control_result_serde_roundtrip() {
377 let result = ControlResult {
378 operation: ControlOperation::StatusCheck,
379 success: true,
380 message: "OK".to_string(),
381 data: serde_json::json!({"key": "value"}),
382 };
383 let json = serde_json::to_string(&result).unwrap();
384 let back: ControlResult = serde_json::from_str(&json).unwrap();
385 assert_eq!(result, back);
386 }
387
388 #[test]
389 fn test_control_operation_serde_roundtrip() {
390 for op in [
391 ControlOperation::StatusCheck,
392 ControlOperation::PathTranslate,
393 ControlOperation::ServiceList,
394 ControlOperation::HealthCheck,
395 ControlOperation::WslCheck,
396 ] {
397 let json = serde_json::to_string(&op).unwrap();
398 let back: ControlOperation = serde_json::from_str(&json).unwrap();
399 assert_eq!(op, back);
400 }
401 }
402
403 #[test]
408 fn test_control_shell_uses_shared_types() {
409 let shell = ControlShell::new(BackendMode::LinuxNative);
410
411 let status = shell.status_check();
413 assert!(status.data.get("backend_mode").is_some());
414
415 let translated = shell.translate_path("/home/user");
417 assert!(translated.data.get("translated").is_some());
418
419 let wsl = shell.wsl_check();
421 assert!(wsl.data.get("status").is_some());
422 }
423
424 #[test]
425 fn test_control_shell_all_operations_succeed() {
426 let mut shell = ControlShell::auto();
427 shell
428 .register_service(ServiceConfig::new("test", 9999))
429 .unwrap();
430
431 assert!(shell.status_check().success);
433 assert!(shell.translate_path(r"C:\test").success);
434 assert!(shell.list_services().success);
435 assert!(shell.check_health().success);
436 let _ = shell.wsl_check();
438 }
439}