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!(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}