1use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, Clone, Error, PartialEq)]
10pub enum ValidationError {
11 #[error("Invalid blueprint: {0}")]
13 Blueprint(String),
14
15 #[error("Invalid stage '{stage}': {message}")]
17 Stage {
18 stage: String,
20 message: String,
22 },
23
24 #[error("Invalid region '{region}': {message}")]
26 Region {
27 region: String,
29 message: String,
31 },
32
33 #[error("Invalid layout: {0}")]
35 Layout(String),
36
37 #[error("Invalid graph: {0}")]
39 Graph(String),
40
41 #[error("Invalid transition from '{from}' to '{to}': {message}")]
43 Transition {
44 from: String,
46 to: String,
48 message: String,
50 },
51}
52
53#[derive(Error, Debug)]
55pub enum Error {
56 #[error("Region not found: {0}")]
58 RegionNotFound(String),
59
60 #[error("Region validation failed: {0}")]
62 ValidationFailed(String),
63
64 #[error("Content exceeds token budget: {used} > {max}")]
66 TokenBudgetExceeded {
67 used: usize,
69 max: usize,
71 },
72
73 #[error(
81 "Region '{region}' is full ({used}/{max} tokens) and does not evict automatically - \
82 release an entry before adding another"
83 )]
84 RegionFull {
85 region: String,
87 used: usize,
89 max: usize,
91 },
92
93 #[error("Region '{region}' refused the write: {reason}")]
101 RegionRefusedWrite {
102 region: String,
104 reason: String,
107 },
108
109 #[error("Pinned regions ({pinned_tokens}) exceed total budget ({total_budget})")]
111 PinnedRegionsOverBudget {
112 pinned_tokens: usize,
115 total_budget: usize,
117 },
118
119 #[error("Blueprint validation failed: {0}")]
121 BlueprintInvalid(String),
122
123 #[error("Layout validation failed: {0}")]
125 LayoutInvalid(String),
126
127 #[error("Context transform failed: {0}")]
129 TransformFailed(String),
130
131 #[error("Serialization error: {0}")]
133 SerializationError(#[from] serde_json::Error),
134
135 #[error("{0}")]
137 Other(String),
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
147 fn validation_error_blueprint() {
148 let e = ValidationError::Blueprint("missing name".into());
149 assert_eq!(e.to_string(), "Invalid blueprint: missing name");
150 }
151
152 #[test]
153 fn validation_error_stage() {
154 let e = ValidationError::Stage {
155 stage: "init".into(),
156 message: "no prompt".into(),
157 };
158 assert_eq!(e.to_string(), "Invalid stage 'init': no prompt");
159 }
160
161 #[test]
162 fn validation_error_region() {
163 let e = ValidationError::Region {
164 region: "context".into(),
165 message: "too large".into(),
166 };
167 assert_eq!(e.to_string(), "Invalid region 'context': too large");
168 }
169
170 #[test]
171 fn validation_error_layout() {
172 let e = ValidationError::Layout("overlapping regions".into());
173 assert_eq!(e.to_string(), "Invalid layout: overlapping regions");
174 }
175
176 #[test]
177 fn validation_error_graph() {
178 let e = ValidationError::Graph("cycle detected".into());
179 assert_eq!(e.to_string(), "Invalid graph: cycle detected");
180 }
181
182 #[test]
183 fn validation_error_transition() {
184 let e = ValidationError::Transition {
185 from: "A".into(),
186 to: "B".into(),
187 message: "missing condition".into(),
188 };
189 assert_eq!(
190 e.to_string(),
191 "Invalid transition from 'A' to 'B': missing condition"
192 );
193 }
194
195 #[test]
198 fn error_region_not_found() {
199 let e = Error::RegionNotFound("history".into());
200 assert_eq!(e.to_string(), "Region not found: history");
201 }
202
203 #[test]
204 fn error_validation_failed() {
205 let e = Error::ValidationFailed("bad input".into());
206 assert_eq!(e.to_string(), "Region validation failed: bad input");
207 }
208
209 #[test]
210 fn error_token_budget_exceeded() {
211 let e = Error::TokenBudgetExceeded {
212 used: 500,
213 max: 100,
214 };
215 assert_eq!(e.to_string(), "Content exceeds token budget: 500 > 100");
216 }
217
218 #[test]
219 fn error_region_refused_write() {
220 let e = Error::RegionRefusedWrite {
221 region: "claims".into(),
222 reason: "needs a source line".into(),
223 };
224 assert_eq!(
225 e.to_string(),
226 "Region 'claims' refused the write: needs a source line"
227 );
228 }
229
230 #[test]
231 fn error_pinned_regions_over_budget() {
232 let e = Error::PinnedRegionsOverBudget {
233 pinned_tokens: 2000,
234 total_budget: 1000,
235 };
236 assert_eq!(
237 e.to_string(),
238 "Pinned regions (2000) exceed total budget (1000)"
239 );
240 }
241
242 #[test]
243 fn error_blueprint_invalid() {
244 let e = Error::BlueprintInvalid("parse error".into());
245 assert_eq!(e.to_string(), "Blueprint validation failed: parse error");
246 }
247
248 #[test]
249 fn error_layout_invalid() {
250 let e = Error::LayoutInvalid("bad layout".into());
251 assert_eq!(e.to_string(), "Layout validation failed: bad layout");
252 }
253
254 #[test]
255 fn error_transform_failed() {
256 let e = Error::TransformFailed("script error".into());
257 assert_eq!(e.to_string(), "Context transform failed: script error");
258 }
259
260 #[test]
261 fn error_other() {
262 let e = Error::Other("misc".into());
263 assert_eq!(e.to_string(), "misc");
264 }
265
266 #[test]
267 fn error_from_serde_json() {
268 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
269 let e = Error::from(json_err);
270 assert!(e.to_string().contains("Serialization error"));
271 }
272
273 #[test]
276 fn validation_error_is_cloneable() {
277 let e = ValidationError::Graph("cycle".into());
278 let cloned = e.clone();
279 assert_eq!(e.to_string(), cloned.to_string());
280 }
281}