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 #[error("{0}")]
25 InvalidParams(String),
26
27 #[error(
32 "Cache appears to be corrupted: {0}. Run 'rfx clear' followed by 'rfx index' to rebuild."
33 )]
34 CacheCorrupted(String),
35
36 #[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 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}