use thiserror::Error;
#[derive(Error, Debug)]
pub enum ClawError {
#[error("Claude Code CLI not found. Install it or set cli_path.")]
CliNotFound,
#[error("Invalid Claude CLI version: expected >= 2.0.0, found {version}")]
InvalidCliVersion {
version: String,
},
#[error("Failed to connect to Claude Code CLI: {0}")]
Connection(String),
#[error("CLI process exited with code {code}: {stderr}")]
Process {
code: i32,
stderr: String,
},
#[error("Failed to parse JSON from CLI: {0}")]
JsonDecode(#[from] serde_json::Error),
#[error("Failed to parse message: {reason}")]
MessageParse {
reason: String,
raw: String,
},
#[error("Control protocol timeout waiting for {subtype}")]
ControlTimeout {
subtype: String,
},
#[error("Control protocol error: {0}")]
ControlError(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Tool execution failed: {0}")]
ToolExecution(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cli_not_found_message() {
let err = ClawError::CliNotFound;
assert_eq!(
err.to_string(),
"Claude Code CLI not found. Install it or set cli_path."
);
}
#[test]
fn test_invalid_cli_version_message() {
let err = ClawError::InvalidCliVersion {
version: "1.5.2".to_string(),
};
assert_eq!(
err.to_string(),
"Invalid Claude CLI version: expected >= 2.0.0, found 1.5.2"
);
}
#[test]
fn test_connection_error_message() {
let err = ClawError::Connection("timeout".to_string());
assert_eq!(
err.to_string(),
"Failed to connect to Claude Code CLI: timeout"
);
}
#[test]
fn test_process_error_message() {
let err = ClawError::Process {
code: 1,
stderr: "permission denied".to_string(),
};
assert!(err.to_string().contains("code 1"));
assert!(err.to_string().contains("permission denied"));
}
#[test]
fn test_message_parse_error() {
let err = ClawError::MessageParse {
reason: "missing required field".to_string(),
raw: r#"{"incomplete": true}"#.to_string(),
};
assert!(err.to_string().contains("missing required field"));
}
#[test]
fn test_control_timeout_error() {
let err = ClawError::ControlTimeout {
subtype: "prompt_response".to_string(),
};
assert!(err.to_string().contains("prompt_response"));
}
#[test]
fn test_control_error() {
let err = ClawError::ControlError("permission denied".to_string());
assert!(err.to_string().contains("permission denied"));
}
#[test]
fn test_tool_execution_error() {
let err = ClawError::ToolExecution("handler panicked".to_string());
assert!(err.to_string().contains("handler panicked"));
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let claw_err: ClawError = io_err.into();
assert!(claw_err.to_string().contains("file not found"));
}
#[test]
fn test_json_error_conversion() {
let json_str = "{ invalid json }";
let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
let claw_err: ClawError = json_err.into();
assert!(claw_err.to_string().contains("parse"));
}
#[test]
fn test_result_with_question_mark_io() {
fn read_file() -> Result<String, ClawError> {
Ok(std::fs::read_to_string("/nonexistent/file.txt")?)
}
let result = read_file();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ClawError::Io(_)));
}
#[test]
fn test_result_with_question_mark_json() {
fn parse_json() -> Result<serde_json::Value, ClawError> {
Ok(serde_json::from_str("{ invalid }")?)
}
let result = parse_json();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ClawError::JsonDecode(_)));
}
}