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 #[error(
48 "symbol indexing in progress (pid {pid}, started {started_at}, {processed}/{total} files)"
49 )]
50 SymbolIndexingInProgress {
51 pid: u32,
52 started_at: String,
54 processed: usize,
55 total: usize,
56 },
57
58 #[error(
65 "this .reflex/ was written by reflex {owner_version}{owner_sha}; this binary is {this_version}. \
66 Rebuild with force, or run the matching binary."
67 )]
68 CacheVersionMismatch {
69 owner_version: String,
70 owner_sha: String,
72 this_version: String,
73 },
74}
75
76impl ReflexError {
77 pub fn kind(&self) -> &'static str {
78 match self {
79 Self::IndexNotFound => "IndexNotFound",
80 Self::QuerySyntaxError(_) => "QuerySyntaxError",
81 Self::IoError(_) => "IoError",
82 Self::ParseError(_) => "ParseError",
83 Self::LlmError(_) => "LlmError",
84 Self::InvalidParams(_) => "InvalidParams",
85 Self::CacheCorrupted(_) => "CacheCorrupted",
86 Self::IndexLocked(_) => "IndexLocked",
87 Self::SymbolIndexingInProgress { .. } => "SymbolIndexingInProgress",
88 Self::CacheVersionMismatch { .. } => "CacheVersionMismatch",
89 }
90 }
91
92 pub fn exit_code(&self) -> i32 {
93 match self {
94 Self::IndexNotFound => 2,
95 Self::QuerySyntaxError(_) => 3,
96 Self::IoError(_) => 4,
97 Self::ParseError(_) => 5,
98 Self::LlmError(_) => 6,
99 Self::InvalidParams(_) => 3,
100 Self::CacheCorrupted(_) => 2,
101 Self::IndexLocked(_) => 7,
102 Self::SymbolIndexingInProgress { .. } => 7,
103 Self::CacheVersionMismatch { .. } => 2,
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_exit_codes() {
114 assert_eq!(ReflexError::IndexNotFound.exit_code(), 2);
115 assert_eq!(ReflexError::QuerySyntaxError("bad".into()).exit_code(), 3);
116 assert_eq!(ReflexError::IoError("fail".into()).exit_code(), 4);
117 assert_eq!(ReflexError::ParseError("oops".into()).exit_code(), 5);
118 assert_eq!(ReflexError::LlmError("timeout".into()).exit_code(), 6);
119 assert_eq!(ReflexError::InvalidParams("bad".into()).exit_code(), 3);
120 assert_eq!(ReflexError::CacheCorrupted("x".into()).exit_code(), 2);
121 assert_eq!(ReflexError::IndexLocked("x".into()).exit_code(), 7);
122 }
123
124 #[test]
125 fn test_new_variant_kinds_and_display() {
126 assert_eq!(
127 ReflexError::InvalidParams("x".into()).kind(),
128 "InvalidParams"
129 );
130 assert_eq!(
131 ReflexError::CacheCorrupted("x".into()).kind(),
132 "CacheCorrupted"
133 );
134 assert_eq!(ReflexError::IndexLocked("x".into()).kind(), "IndexLocked");
135 let msg = ReflexError::CacheCorrupted(
137 "content.bin is too small - appears to be corrupted".into(),
138 )
139 .to_string();
140 assert_eq!(
141 msg,
142 "Cache appears to be corrupted: content.bin is too small - appears to be corrupted. \
143 Run 'rfx clear' followed by 'rfx index' to rebuild."
144 );
145 assert_eq!(
146 ReflexError::InvalidParams("Unknown argument \"q\"".into()).to_string(),
147 "Unknown argument \"q\""
148 );
149 }
150
151 #[test]
152 fn test_kind_strings() {
153 assert_eq!(ReflexError::IndexNotFound.kind(), "IndexNotFound");
154 assert_eq!(
155 ReflexError::QuerySyntaxError("x".into()).kind(),
156 "QuerySyntaxError"
157 );
158 assert_eq!(ReflexError::IoError("x".into()).kind(), "IoError");
159 assert_eq!(ReflexError::ParseError("x".into()).kind(), "ParseError");
160 assert_eq!(ReflexError::LlmError("x".into()).kind(), "LlmError");
161 }
162
163 #[test]
164 fn test_mcp_json_error_shape() {
165 let err = ReflexError::IndexNotFound;
166 let kind = err.kind();
167 let message = err.to_string();
168 let json_data = serde_json::json!({ "kind": kind, "message": message });
169
170 assert_eq!(json_data["kind"], "IndexNotFound");
171 assert!(json_data["message"].as_str().unwrap().contains("rfx index"));
172 }
173
174 #[test]
175 fn test_http_json_error_shape() {
176 let err = ReflexError::QuerySyntaxError("invalid pattern".into());
177 let kind = err.kind();
178 let msg = err.to_string();
179 let body = serde_json::json!({ "error": { "kind": kind, "message": msg } });
180
181 assert_eq!(body["error"]["kind"], "QuerySyntaxError");
182 assert!(
183 body["error"]["message"]
184 .as_str()
185 .unwrap()
186 .contains("invalid pattern")
187 );
188 }
189
190 #[test]
191 fn test_anyhow_downcast() {
192 let err: anyhow::Error = ReflexError::IndexNotFound.into();
193 let downcasted = err.downcast_ref::<ReflexError>().unwrap();
194 assert_eq!(downcasted.exit_code(), 2);
195 assert_eq!(downcasted.kind(), "IndexNotFound");
196 }
197
198 #[test]
199 fn test_non_reflex_error_fallback() {
200 let err = anyhow::anyhow!("some other error");
201 let exit_code = if let Some(re) = err.downcast_ref::<ReflexError>() {
202 re.exit_code()
203 } else {
204 1
205 };
206 assert_eq!(
207 exit_code, 1,
208 "Non-ReflexError should fall back to exit code 1"
209 );
210 }
211}