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    /// A tool/API call carried an argument set the server cannot accept:
21    /// unknown key, wrong type, or a required key that is absent. The payload
22    /// is the full human-readable diagnostic (received keys, valid keys,
23    /// nearest-match suggestion). Maps to JSON-RPC `-32602` in the MCP layer.
24    #[error("{0}")]
25    InvalidParams(String),
26
27    /// The on-disk cache failed structural validation (bad magic bytes, short
28    /// file, broken SQLite). The payload is the inner finding, e.g.
29    /// `content.bin is too small - appears to be corrupted`. The Display text
30    /// keeps the historical wording so string-matching callers stay valid.
31    #[error(
32        "Cache appears to be corrupted: {0}. Run 'rfx clear' followed by 'rfx index' to rebuild."
33    )]
34    CacheCorrupted(String),
35
36    /// Another process holds the workspace index lock (`.reflex/index.lock`).
37    /// The payload names the lock path.
38    #[error(
39        "Another indexer is already running on this workspace ({0}). Wait for it to finish and retry."
40    )]
41    IndexLocked(String),
42}
43
44impl ReflexError {
45    pub fn kind(&self) -> &'static str {
46        match self {
47            Self::IndexNotFound => "IndexNotFound",
48            Self::QuerySyntaxError(_) => "QuerySyntaxError",
49            Self::IoError(_) => "IoError",
50            Self::ParseError(_) => "ParseError",
51            Self::LlmError(_) => "LlmError",
52            Self::InvalidParams(_) => "InvalidParams",
53            Self::CacheCorrupted(_) => "CacheCorrupted",
54            Self::IndexLocked(_) => "IndexLocked",
55        }
56    }
57
58    pub fn exit_code(&self) -> i32 {
59        match self {
60            Self::IndexNotFound => 2,
61            Self::QuerySyntaxError(_) => 3,
62            Self::IoError(_) => 4,
63            Self::ParseError(_) => 5,
64            Self::LlmError(_) => 6,
65            Self::InvalidParams(_) => 3,
66            Self::CacheCorrupted(_) => 2,
67            Self::IndexLocked(_) => 7,
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_exit_codes() {
78        assert_eq!(ReflexError::IndexNotFound.exit_code(), 2);
79        assert_eq!(ReflexError::QuerySyntaxError("bad".into()).exit_code(), 3);
80        assert_eq!(ReflexError::IoError("fail".into()).exit_code(), 4);
81        assert_eq!(ReflexError::ParseError("oops".into()).exit_code(), 5);
82        assert_eq!(ReflexError::LlmError("timeout".into()).exit_code(), 6);
83        assert_eq!(ReflexError::InvalidParams("bad".into()).exit_code(), 3);
84        assert_eq!(ReflexError::CacheCorrupted("x".into()).exit_code(), 2);
85        assert_eq!(ReflexError::IndexLocked("x".into()).exit_code(), 7);
86    }
87
88    #[test]
89    fn test_new_variant_kinds_and_display() {
90        assert_eq!(
91            ReflexError::InvalidParams("x".into()).kind(),
92            "InvalidParams"
93        );
94        assert_eq!(
95            ReflexError::CacheCorrupted("x".into()).kind(),
96            "CacheCorrupted"
97        );
98        assert_eq!(ReflexError::IndexLocked("x".into()).kind(), "IndexLocked");
99        // Display text of CacheCorrupted must keep the historical wording.
100        let msg = ReflexError::CacheCorrupted(
101            "content.bin is too small - appears to be corrupted".into(),
102        )
103        .to_string();
104        assert_eq!(
105            msg,
106            "Cache appears to be corrupted: content.bin is too small - appears to be corrupted. \
107             Run 'rfx clear' followed by 'rfx index' to rebuild."
108        );
109        assert_eq!(
110            ReflexError::InvalidParams("Unknown argument \"q\"".into()).to_string(),
111            "Unknown argument \"q\""
112        );
113    }
114
115    #[test]
116    fn test_kind_strings() {
117        assert_eq!(ReflexError::IndexNotFound.kind(), "IndexNotFound");
118        assert_eq!(
119            ReflexError::QuerySyntaxError("x".into()).kind(),
120            "QuerySyntaxError"
121        );
122        assert_eq!(ReflexError::IoError("x".into()).kind(), "IoError");
123        assert_eq!(ReflexError::ParseError("x".into()).kind(), "ParseError");
124        assert_eq!(ReflexError::LlmError("x".into()).kind(), "LlmError");
125    }
126
127    #[test]
128    fn test_mcp_json_error_shape() {
129        let err = ReflexError::IndexNotFound;
130        let kind = err.kind();
131        let message = err.to_string();
132        let json_data = serde_json::json!({ "kind": kind, "message": message });
133
134        assert_eq!(json_data["kind"], "IndexNotFound");
135        assert!(json_data["message"].as_str().unwrap().contains("rfx index"));
136    }
137
138    #[test]
139    fn test_http_json_error_shape() {
140        let err = ReflexError::QuerySyntaxError("invalid pattern".into());
141        let kind = err.kind();
142        let msg = err.to_string();
143        let body = serde_json::json!({ "error": { "kind": kind, "message": msg } });
144
145        assert_eq!(body["error"]["kind"], "QuerySyntaxError");
146        assert!(
147            body["error"]["message"]
148                .as_str()
149                .unwrap()
150                .contains("invalid pattern")
151        );
152    }
153
154    #[test]
155    fn test_anyhow_downcast() {
156        let err: anyhow::Error = ReflexError::IndexNotFound.into();
157        let downcasted = err.downcast_ref::<ReflexError>().unwrap();
158        assert_eq!(downcasted.exit_code(), 2);
159        assert_eq!(downcasted.kind(), "IndexNotFound");
160    }
161
162    #[test]
163    fn test_non_reflex_error_fallback() {
164        let err = anyhow::anyhow!("some other error");
165        let exit_code = if let Some(re) = err.downcast_ref::<ReflexError>() {
166            re.exit_code()
167        } else {
168            1
169        };
170        assert_eq!(
171            exit_code, 1,
172            "Non-ReflexError should fall back to exit code 1"
173        );
174    }
175}