//! Mission Control error types //! //! Provides structured errors for IPC commands, enabling the frontend //! to handle errors programmatically rather than parsing strings. use serde::Serialize; use specta::Type; /// Structured error type for Tauri IPC commands. /// /// This replaces string-based errors (`Result`) with typed errors /// that the frontend can handle programmatically. #[derive(Debug, Clone, Serialize, Type)] pub struct McError { /// Machine-readable error code (e.g., "LORE_UNAVAILABLE", "BRIDGE_LOCKED") pub code: McErrorCode, /// Human-readable error message pub message: String, /// Whether this error is recoverable (user can retry) pub recoverable: bool, } /// Error codes for frontend handling #[derive(Debug, Clone, Serialize, PartialEq, Eq, Type)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum McErrorCode { // Lore errors LoreUnavailable, LoreUnhealthy, LoreFetchFailed, // Bridge errors BridgeLocked, BridgeMapCorrupted, BridgeSyncFailed, // Beads errors BeadsUnavailable, BeadsCreateFailed, BeadsCloseFailed, // Bv errors BvUnavailable, BvTriageFailed, // General errors IoError, InternalError, InvalidInput, } impl McError { /// Create a new error with the given code and message pub fn new(code: McErrorCode, message: impl Into) -> Self { let recoverable = matches!( code, McErrorCode::LoreUnavailable | McErrorCode::BeadsUnavailable | McErrorCode::BvUnavailable | McErrorCode::BridgeLocked | McErrorCode::IoError ); Self { code, message: message.into(), recoverable, } } /// Create a lore unavailable error pub fn lore_unavailable() -> Self { Self::new( McErrorCode::LoreUnavailable, "lore CLI not found -- is it installed?", ) } /// Create a lore unhealthy error pub fn lore_unhealthy() -> Self { Self::new( McErrorCode::LoreUnhealthy, "lore health check failed -- run 'lore index --full'", ) } /// Create a bridge locked error pub fn bridge_locked() -> Self { Self::new( McErrorCode::BridgeLocked, "Another Mission Control instance is running", ) } /// Create an internal error with context pub fn internal(context: impl Into) -> Self { Self::new(McErrorCode::InternalError, context) } /// Create a beads unavailable error pub fn beads_unavailable() -> Self { Self::new( McErrorCode::BeadsUnavailable, "br CLI not found -- is beads installed?", ) } /// Create an IO error with context pub fn io_error(context: impl Into) -> Self { Self::new(McErrorCode::IoError, context) } /// Create a bv unavailable error pub fn bv_unavailable() -> Self { Self::new( McErrorCode::BvUnavailable, "bv CLI not found -- is beads installed?", ) } /// Create an invalid input error pub fn invalid_input(message: impl Into) -> Self { Self { code: McErrorCode::InvalidInput, message: message.into(), recoverable: false, } } } impl std::fmt::Display for McError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.message) } } impl std::error::Error for McError {} // Conversion from bridge errors impl From for McError { fn from(err: crate::data::bridge::BridgeError) -> Self { use crate::data::bridge::BridgeError; match err { BridgeError::InstanceLocked => Self::bridge_locked(), BridgeError::Lore(e) => Self::new( McErrorCode::LoreFetchFailed, format!("Lore error: {}", e), ), BridgeError::Beads(e) => Self::new( McErrorCode::BeadsCreateFailed, format!("Beads error: {}", e), ), BridgeError::Io(e) => Self::new(McErrorCode::IoError, format!("IO error: {}", e)), BridgeError::Json(e) => Self::new( McErrorCode::BridgeMapCorrupted, format!("Map file corrupted: {}", e), ), } } } // Conversion from lore errors impl From for McError { fn from(err: crate::data::lore::LoreError) -> Self { use crate::data::lore::LoreError; match err { LoreError::ExecutionFailed(_) => Self::lore_unavailable(), LoreError::CommandFailed(msg) => Self::new( McErrorCode::LoreFetchFailed, format!("lore command failed: {}", msg), ), LoreError::ParseFailed(msg) => Self::new( McErrorCode::LoreFetchFailed, format!("Failed to parse lore response: {}", msg), ), } } } // Conversion from beads errors impl From for McError { fn from(err: crate::data::beads::BeadsError) -> Self { use crate::data::beads::BeadsError; match err { BeadsError::ExecutionFailed(_) => Self::beads_unavailable(), BeadsError::CommandFailed(msg) => Self::new( McErrorCode::BeadsCreateFailed, format!("br command failed: {}", msg), ), BeadsError::ParseFailed(msg) => Self::new( McErrorCode::BeadsCreateFailed, format!("Failed to parse br response: {}", msg), ), } } } // Conversion from bv errors impl From for McError { fn from(err: crate::data::bv::BvError) -> Self { use crate::data::bv::BvError; match err { BvError::ExecutionFailed(_) => Self::bv_unavailable(), BvError::CommandFailed(msg) => Self::new( McErrorCode::BvTriageFailed, format!("bv command failed: {}", msg), ), BvError::ParseFailed(msg) => Self::new( McErrorCode::BvTriageFailed, format!("Failed to parse bv response: {}", msg), ), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_error_serialization() { let err = McError::lore_unavailable(); let json = serde_json::to_string(&err).unwrap(); assert!(json.contains("LORE_UNAVAILABLE")); assert!(json.contains("recoverable")); } #[test] fn test_recoverable_errors() { assert!(McError::lore_unavailable().recoverable); assert!(McError::bridge_locked().recoverable); assert!(!McError::internal("test").recoverable); } #[test] fn test_error_codes_are_screaming_snake_case() { let err = McError::new(McErrorCode::BridgeMapCorrupted, "test"); let json = serde_json::to_string(&err).unwrap(); assert!(json.contains("BRIDGE_MAP_CORRUPTED")); } #[test] fn test_beads_unavailable_is_recoverable() { assert!(McError::beads_unavailable().recoverable); } #[test] fn test_beads_error_conversion() { use crate::data::beads::BeadsError; // ExecutionFailed -> BeadsUnavailable (recoverable) let err: McError = BeadsError::ExecutionFailed("not found".to_string()).into(); assert_eq!(err.code, McErrorCode::BeadsUnavailable); assert!(err.recoverable); // CommandFailed -> BeadsCreateFailed (not recoverable) let err: McError = BeadsError::CommandFailed("failed".to_string()).into(); assert_eq!(err.code, McErrorCode::BeadsCreateFailed); assert!(!err.recoverable); // ParseFailed -> BeadsCreateFailed (not recoverable) let err: McError = BeadsError::ParseFailed("bad json".to_string()).into(); assert_eq!(err.code, McErrorCode::BeadsCreateFailed); assert!(!err.recoverable); } #[test] fn test_lore_error_conversion() { use crate::data::lore::LoreError; // ExecutionFailed -> LoreUnavailable (recoverable) let err: McError = LoreError::ExecutionFailed("not found".to_string()).into(); assert_eq!(err.code, McErrorCode::LoreUnavailable); assert!(err.recoverable); // CommandFailed -> LoreFetchFailed (not recoverable) let err: McError = LoreError::CommandFailed("failed".to_string()).into(); assert_eq!(err.code, McErrorCode::LoreFetchFailed); assert!(!err.recoverable); } #[test] fn test_bridge_error_conversion() { use crate::data::bridge::BridgeError; // InstanceLocked -> BridgeLocked (recoverable) let err: McError = BridgeError::InstanceLocked.into(); assert_eq!(err.code, McErrorCode::BridgeLocked); assert!(err.recoverable); // Io -> IoError (recoverable) let err: McError = BridgeError::Io(std::io::Error::new( std::io::ErrorKind::NotFound, "file not found", )) .into(); assert_eq!(err.code, McErrorCode::IoError); assert!(err.recoverable); // Json -> BridgeMapCorrupted (not recoverable) let json_err = serde_json::from_str::<()>("invalid").unwrap_err(); let err: McError = BridgeError::Json(json_err).into(); assert_eq!(err.code, McErrorCode::BridgeMapCorrupted); assert!(!err.recoverable); } #[test] fn test_bv_error_conversion() { use crate::data::bv::BvError; // ExecutionFailed -> BvUnavailable (recoverable) let err: McError = BvError::ExecutionFailed("not found".to_string()).into(); assert_eq!(err.code, McErrorCode::BvUnavailable); assert!(err.recoverable); // CommandFailed -> BvTriageFailed (not recoverable) let err: McError = BvError::CommandFailed("failed".to_string()).into(); assert_eq!(err.code, McErrorCode::BvTriageFailed); assert!(!err.recoverable); // ParseFailed -> BvTriageFailed (not recoverable) let err: McError = BvError::ParseFailed("bad json".to_string()).into(); assert_eq!(err.code, McErrorCode::BvTriageFailed); assert!(!err.recoverable); } }