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 dependency '{name}': {message}")]
35 Dependency {
36 name: String,
38 message: String,
40 },
41
42 #[error("Invalid layout: {0}")]
44 Layout(String),
45
46 #[error("Invalid graph: {0}")]
48 Graph(String),
49
50 #[error("Invalid transition from '{from}' to '{to}': {message}")]
52 Transition {
53 from: String,
55 to: String,
57 message: String,
59 },
60}
61
62#[derive(Error, Debug)]
64pub enum Error {
65 #[error("Region not found: {0}")]
67 RegionNotFound(String),
68
69 #[error("Region validation failed: {0}")]
71 ValidationFailed(String),
72
73 #[error("Content exceeds token budget: {used} > {max}")]
75 TokenBudgetExceeded {
76 used: usize,
78 max: usize,
80 },
81
82 #[error(
90 "Region '{region}' is full ({used}/{max} tokens) and does not evict automatically - \
91 release an entry before adding another"
92 )]
93 RegionFull {
94 region: String,
96 used: usize,
98 max: usize,
100 },
101
102 #[error("Region '{region}' refused the write: {reason}")]
110 RegionRefusedWrite {
111 region: String,
113 reason: String,
116 },
117
118 #[error("Pinned regions ({pinned_tokens}) exceed total budget ({total_budget})")]
120 PinnedRegionsOverBudget {
121 pinned_tokens: usize,
124 total_budget: usize,
126 },
127
128 #[error("Blueprint validation failed: {0}")]
130 BlueprintInvalid(String),
131
132 #[error("Layout validation failed: {0}")]
134 LayoutInvalid(String),
135
136 #[error("Context transform failed: {0}")]
138 TransformFailed(String),
139
140 #[error("Serialization error: {0}")]
142 SerializationError(#[from] serde_json::Error),
143
144 #[error("{0}")]
146 Other(String),
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
156 fn validation_error_blueprint() {
157 let e = ValidationError::Blueprint("missing name".into());
158 assert_eq!(e.to_string(), "Invalid blueprint: missing name");
159 }
160
161 #[test]
162 fn validation_error_stage() {
163 let e = ValidationError::Stage {
164 stage: "init".into(),
165 message: "no prompt".into(),
166 };
167 assert_eq!(e.to_string(), "Invalid stage 'init': no prompt");
168 }
169
170 #[test]
171 fn validation_error_region() {
172 let e = ValidationError::Region {
173 region: "context".into(),
174 message: "too large".into(),
175 };
176 assert_eq!(e.to_string(), "Invalid region 'context': too large");
177 }
178
179 #[test]
180 fn validation_error_layout() {
181 let e = ValidationError::Layout("overlapping regions".into());
182 assert_eq!(e.to_string(), "Invalid layout: overlapping regions");
183 }
184
185 #[test]
186 fn validation_error_graph() {
187 let e = ValidationError::Graph("cycle detected".into());
188 assert_eq!(e.to_string(), "Invalid graph: cycle detected");
189 }
190
191 #[test]
192 fn validation_error_transition() {
193 let e = ValidationError::Transition {
194 from: "A".into(),
195 to: "B".into(),
196 message: "missing condition".into(),
197 };
198 assert_eq!(
199 e.to_string(),
200 "Invalid transition from 'A' to 'B': missing condition"
201 );
202 }
203
204 #[test]
207 fn error_region_not_found() {
208 let e = Error::RegionNotFound("history".into());
209 assert_eq!(e.to_string(), "Region not found: history");
210 }
211
212 #[test]
213 fn error_validation_failed() {
214 let e = Error::ValidationFailed("bad input".into());
215 assert_eq!(e.to_string(), "Region validation failed: bad input");
216 }
217
218 #[test]
219 fn error_token_budget_exceeded() {
220 let e = Error::TokenBudgetExceeded {
221 used: 500,
222 max: 100,
223 };
224 assert_eq!(e.to_string(), "Content exceeds token budget: 500 > 100");
225 }
226
227 #[test]
228 fn error_region_refused_write() {
229 let e = Error::RegionRefusedWrite {
230 region: "claims".into(),
231 reason: "needs a source line".into(),
232 };
233 assert_eq!(
234 e.to_string(),
235 "Region 'claims' refused the write: needs a source line"
236 );
237 }
238
239 #[test]
240 fn error_pinned_regions_over_budget() {
241 let e = Error::PinnedRegionsOverBudget {
242 pinned_tokens: 2000,
243 total_budget: 1000,
244 };
245 assert_eq!(
246 e.to_string(),
247 "Pinned regions (2000) exceed total budget (1000)"
248 );
249 }
250
251 #[test]
252 fn error_blueprint_invalid() {
253 let e = Error::BlueprintInvalid("parse error".into());
254 assert_eq!(e.to_string(), "Blueprint validation failed: parse error");
255 }
256
257 #[test]
258 fn error_layout_invalid() {
259 let e = Error::LayoutInvalid("bad layout".into());
260 assert_eq!(e.to_string(), "Layout validation failed: bad layout");
261 }
262
263 #[test]
264 fn error_transform_failed() {
265 let e = Error::TransformFailed("script error".into());
266 assert_eq!(e.to_string(), "Context transform failed: script error");
267 }
268
269 #[test]
270 fn error_other() {
271 let e = Error::Other("misc".into());
272 assert_eq!(e.to_string(), "misc");
273 }
274
275 #[test]
276 fn error_from_serde_json() {
277 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
278 let e = Error::from(json_err);
279 assert!(e.to_string().contains("Serialization error"));
280 }
281
282 #[test]
285 fn validation_error_is_cloneable() {
286 let e = ValidationError::Graph("cycle".into());
287 let cloned = e.clone();
288 assert_eq!(e.to_string(), cloned.to_string());
289 }
290}