1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum ReflexError {
5 #[error("Index not found. Run 'rfx index' to build the search index.")]
6 IndexNotFound,
7
8 #[error("Query syntax error: {0}")]
9 QuerySyntaxError(String),
10
11 #[error("I/O error: {0}")]
12 IoError(String),
13
14 #[error("Parse error: {0}")]
15 ParseError(String),
16
17 #[error("LLM error: {0}")]
18 LlmError(String),
19}
20
21impl ReflexError {
22 pub fn kind(&self) -> &'static str {
23 match self {
24 Self::IndexNotFound => "IndexNotFound",
25 Self::QuerySyntaxError(_) => "QuerySyntaxError",
26 Self::IoError(_) => "IoError",
27 Self::ParseError(_) => "ParseError",
28 Self::LlmError(_) => "LlmError",
29 }
30 }
31
32 pub fn exit_code(&self) -> i32 {
33 match self {
34 Self::IndexNotFound => 2,
35 Self::QuerySyntaxError(_) => 3,
36 Self::IoError(_) => 4,
37 Self::ParseError(_) => 5,
38 Self::LlmError(_) => 6,
39 }
40 }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46
47 #[test]
48 fn test_exit_codes() {
49 assert_eq!(ReflexError::IndexNotFound.exit_code(), 2);
50 assert_eq!(ReflexError::QuerySyntaxError("bad".into()).exit_code(), 3);
51 assert_eq!(ReflexError::IoError("fail".into()).exit_code(), 4);
52 assert_eq!(ReflexError::ParseError("oops".into()).exit_code(), 5);
53 assert_eq!(ReflexError::LlmError("timeout".into()).exit_code(), 6);
54 }
55
56 #[test]
57 fn test_kind_strings() {
58 assert_eq!(ReflexError::IndexNotFound.kind(), "IndexNotFound");
59 assert_eq!(ReflexError::QuerySyntaxError("x".into()).kind(), "QuerySyntaxError");
60 assert_eq!(ReflexError::IoError("x".into()).kind(), "IoError");
61 assert_eq!(ReflexError::ParseError("x".into()).kind(), "ParseError");
62 assert_eq!(ReflexError::LlmError("x".into()).kind(), "LlmError");
63 }
64
65 #[test]
66 fn test_mcp_json_error_shape() {
67 let err = ReflexError::IndexNotFound;
68 let kind = err.kind();
69 let message = err.to_string();
70 let json_data = serde_json::json!({ "kind": kind, "message": message });
71
72 assert_eq!(json_data["kind"], "IndexNotFound");
73 assert!(json_data["message"].as_str().unwrap().contains("rfx index"));
74 }
75
76 #[test]
77 fn test_http_json_error_shape() {
78 let err = ReflexError::QuerySyntaxError("invalid pattern".into());
79 let kind = err.kind();
80 let msg = err.to_string();
81 let body = serde_json::json!({ "error": { "kind": kind, "message": msg } });
82
83 assert_eq!(body["error"]["kind"], "QuerySyntaxError");
84 assert!(body["error"]["message"].as_str().unwrap().contains("invalid pattern"));
85 }
86
87 #[test]
88 fn test_anyhow_downcast() {
89 let err: anyhow::Error = ReflexError::IndexNotFound.into();
90 let downcasted = err.downcast_ref::<ReflexError>().unwrap();
91 assert_eq!(downcasted.exit_code(), 2);
92 assert_eq!(downcasted.kind(), "IndexNotFound");
93 }
94
95 #[test]
96 fn test_non_reflex_error_fallback() {
97 let err = anyhow::anyhow!("some other error");
98 let exit_code = if let Some(re) = err.downcast_ref::<ReflexError>() {
99 re.exit_code()
100 } else {
101 1
102 };
103 assert_eq!(exit_code, 1, "Non-ReflexError should fall back to exit code 1");
104 }
105}