feat: add structured error types for Tauri IPC commands
Introduces McError and McErrorCode to replace string-based errors in Tauri commands. This enables the frontend to handle errors programmatically rather than parsing error messages. Key design decisions: - Error codes use SCREAMING_SNAKE_CASE for frontend pattern matching - Each error indicates whether it's recoverable (user can retry) - Automatic conversion from LoreError, BeadsError, and BridgeError - Errors serialize to JSON for consistent IPC transport Error categories: - Lore: LORE_UNAVAILABLE, LORE_UNHEALTHY, LORE_FETCH_FAILED - Bridge: BRIDGE_LOCKED, BRIDGE_MAP_CORRUPTED, BRIDGE_SYNC_FAILED - Beads: BEADS_UNAVAILABLE, BEADS_CREATE_FAILED, BEADS_CLOSE_FAILED - General: IO_ERROR, INTERNAL_ERROR Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
//! - br CLI (beads task management)
|
//! - br CLI (beads task management)
|
||||||
//! - MC local state (mapping, decisions, settings)
|
//! - MC local state (mapping, decisions, settings)
|
||||||
|
|
||||||
pub mod lore;
|
|
||||||
pub mod beads;
|
pub mod beads;
|
||||||
|
pub mod bridge;
|
||||||
|
pub mod lore;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|||||||
264
src-tauri/src/error.rs
Normal file
264
src-tauri/src/error.rs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
//! Mission Control error types
|
||||||
|
//!
|
||||||
|
//! Provides structured errors for IPC commands, enabling the frontend
|
||||||
|
//! to handle errors programmatically rather than parsing strings.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// Structured error type for Tauri IPC commands.
|
||||||
|
///
|
||||||
|
/// This replaces string-based errors (`Result<T, String>`) with typed errors
|
||||||
|
/// that the frontend can handle programmatically.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
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)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum McErrorCode {
|
||||||
|
// Lore errors
|
||||||
|
LoreUnavailable,
|
||||||
|
LoreUnhealthy,
|
||||||
|
LoreFetchFailed,
|
||||||
|
|
||||||
|
// Bridge errors
|
||||||
|
BridgeLocked,
|
||||||
|
BridgeMapCorrupted,
|
||||||
|
BridgeSyncFailed,
|
||||||
|
|
||||||
|
// Beads errors
|
||||||
|
BeadsUnavailable,
|
||||||
|
BeadsCreateFailed,
|
||||||
|
BeadsCloseFailed,
|
||||||
|
|
||||||
|
// General errors
|
||||||
|
IoError,
|
||||||
|
InternalError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McError {
|
||||||
|
/// Create a new error with the given code and message
|
||||||
|
pub fn new(code: McErrorCode, message: impl Into<String>) -> Self {
|
||||||
|
let recoverable = matches!(
|
||||||
|
code,
|
||||||
|
McErrorCode::LoreUnavailable
|
||||||
|
| McErrorCode::BeadsUnavailable
|
||||||
|
| 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<String>) -> 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?",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<crate::data::bridge::BridgeError> 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<crate::data::lore::LoreError> 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<crate::data::beads::BeadsError> 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),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user