Skip to main content

reflex/
errors.rs

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}