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!(
60 ReflexError::QuerySyntaxError("x".into()).kind(),
61 "QuerySyntaxError"
62 );
63 assert_eq!(ReflexError::IoError("x".into()).kind(), "IoError");
64 assert_eq!(ReflexError::ParseError("x".into()).kind(), "ParseError");
65 assert_eq!(ReflexError::LlmError("x".into()).kind(), "LlmError");
66 }
67
68 #[test]
69 fn test_mcp_json_error_shape() {
70 let err = ReflexError::IndexNotFound;
71 let kind = err.kind();
72 let message = err.to_string();
73 let json_data = serde_json::json!({ "kind": kind, "message": message });
74
75 assert_eq!(json_data["kind"], "IndexNotFound");
76 assert!(json_data["message"].as_str().unwrap().contains("rfx index"));
77 }
78
79 #[test]
80 fn test_http_json_error_shape() {
81 let err = ReflexError::QuerySyntaxError("invalid pattern".into());
82 let kind = err.kind();
83 let msg = err.to_string();
84 let body = serde_json::json!({ "error": { "kind": kind, "message": msg } });
85
86 assert_eq!(body["error"]["kind"], "QuerySyntaxError");
87 assert!(
88 body["error"]["message"]
89 .as_str()
90 .unwrap()
91 .contains("invalid pattern")
92 );
93 }
94
95 #[test]
96 fn test_anyhow_downcast() {
97 let err: anyhow::Error = ReflexError::IndexNotFound.into();
98 let downcasted = err.downcast_ref::<ReflexError>().unwrap();
99 assert_eq!(downcasted.exit_code(), 2);
100 assert_eq!(downcasted.kind(), "IndexNotFound");
101 }
102
103 #[test]
104 fn test_non_reflex_error_fallback() {
105 let err = anyhow::anyhow!("some other error");
106 let exit_code = if let Some(re) = err.downcast_ref::<ReflexError>() {
107 re.exit_code()
108 } else {
109 1
110 };
111 assert_eq!(
112 exit_code, 1,
113 "Non-ReflexError should fall back to exit code 1"
114 );
115 }
116}