Skip to main content

leviath_core/
error.rs

1//! Error types for Leviath Core.
2
3use thiserror::Error;
4
5/// Result type alias using Leviath's Error type.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Errors from blueprint, stage, region, and layout validation.
9#[derive(Debug, Clone, Error, PartialEq)]
10pub enum ValidationError {
11    /// Blueprint-level validation failure
12    #[error("Invalid blueprint: {0}")]
13    Blueprint(String),
14
15    /// Stage-level validation failure
16    #[error("Invalid stage '{stage}': {message}")]
17    Stage {
18        /// The offending stage's name.
19        stage: String,
20        /// What is wrong with it.
21        message: String,
22    },
23
24    /// Region-level validation failure
25    #[error("Invalid region '{region}': {message}")]
26    Region {
27        /// The offending region's name.
28        region: String,
29        /// What is wrong with it.
30        message: String,
31    },
32
33    /// Layout-level validation failure
34    #[error("Invalid layout: {0}")]
35    Layout(String),
36
37    /// Graph structure validation failure
38    #[error("Invalid graph: {0}")]
39    Graph(String),
40
41    /// Transition validation failure
42    #[error("Invalid transition from '{from}' to '{to}': {message}")]
43    Transition {
44        /// The stage the edge leaves.
45        from: String,
46        /// The stage the edge names as its target, which may not exist.
47        to: String,
48        /// What is wrong with it.
49        message: String,
50    },
51}
52
53/// Core error types for Leviath.
54#[derive(Error, Debug)]
55pub enum Error {
56    /// Region with the specified name was not found
57    #[error("Region not found: {0}")]
58    RegionNotFound(String),
59
60    /// Region validation failed
61    #[error("Region validation failed: {0}")]
62    ValidationFailed(String),
63
64    /// Content exceeds region's token budget
65    #[error("Content exceeds token budget: {used} > {max}")]
66    TokenBudgetExceeded {
67        /// Tokens the write would have brought the region to.
68        used: usize,
69        /// The region's ceiling.
70        max: usize,
71    },
72
73    /// Pinned regions alone exceed total token budget
74    #[error("Pinned regions ({pinned_tokens}) exceed total budget ({total_budget})")]
75    PinnedRegionsOverBudget {
76        /// Tokens held by regions that can never be evicted, which is what makes
77        /// this unrecoverable rather than a matter of dropping something.
78        pinned_tokens: usize,
79        /// The whole window's budget.
80        total_budget: usize,
81    },
82
83    /// Blueprint validation failed
84    #[error("Blueprint validation failed: {0}")]
85    BlueprintInvalid(String),
86
87    /// Layout validation failed
88    #[error("Layout validation failed: {0}")]
89    LayoutInvalid(String),
90
91    /// Context transform failed
92    #[error("Context transform failed: {0}")]
93    TransformFailed(String),
94
95    /// Serialization error
96    #[error("Serialization error: {0}")]
97    SerializationError(#[from] serde_json::Error),
98
99    /// Generic error
100    #[error("{0}")]
101    Other(String),
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    // ─── ValidationError Display ────────────────────────────────────────────
109
110    #[test]
111    fn validation_error_blueprint() {
112        let e = ValidationError::Blueprint("missing name".into());
113        assert_eq!(e.to_string(), "Invalid blueprint: missing name");
114    }
115
116    #[test]
117    fn validation_error_stage() {
118        let e = ValidationError::Stage {
119            stage: "init".into(),
120            message: "no prompt".into(),
121        };
122        assert_eq!(e.to_string(), "Invalid stage 'init': no prompt");
123    }
124
125    #[test]
126    fn validation_error_region() {
127        let e = ValidationError::Region {
128            region: "context".into(),
129            message: "too large".into(),
130        };
131        assert_eq!(e.to_string(), "Invalid region 'context': too large");
132    }
133
134    #[test]
135    fn validation_error_layout() {
136        let e = ValidationError::Layout("overlapping regions".into());
137        assert_eq!(e.to_string(), "Invalid layout: overlapping regions");
138    }
139
140    #[test]
141    fn validation_error_graph() {
142        let e = ValidationError::Graph("cycle detected".into());
143        assert_eq!(e.to_string(), "Invalid graph: cycle detected");
144    }
145
146    #[test]
147    fn validation_error_transition() {
148        let e = ValidationError::Transition {
149            from: "A".into(),
150            to: "B".into(),
151            message: "missing condition".into(),
152        };
153        assert_eq!(
154            e.to_string(),
155            "Invalid transition from 'A' to 'B': missing condition"
156        );
157    }
158
159    // ─── Error Display ──────────────────────────────────────────────────────
160
161    #[test]
162    fn error_region_not_found() {
163        let e = Error::RegionNotFound("history".into());
164        assert_eq!(e.to_string(), "Region not found: history");
165    }
166
167    #[test]
168    fn error_validation_failed() {
169        let e = Error::ValidationFailed("bad input".into());
170        assert_eq!(e.to_string(), "Region validation failed: bad input");
171    }
172
173    #[test]
174    fn error_token_budget_exceeded() {
175        let e = Error::TokenBudgetExceeded {
176            used: 500,
177            max: 100,
178        };
179        assert_eq!(e.to_string(), "Content exceeds token budget: 500 > 100");
180    }
181
182    #[test]
183    fn error_pinned_regions_over_budget() {
184        let e = Error::PinnedRegionsOverBudget {
185            pinned_tokens: 2000,
186            total_budget: 1000,
187        };
188        assert_eq!(
189            e.to_string(),
190            "Pinned regions (2000) exceed total budget (1000)"
191        );
192    }
193
194    #[test]
195    fn error_blueprint_invalid() {
196        let e = Error::BlueprintInvalid("parse error".into());
197        assert_eq!(e.to_string(), "Blueprint validation failed: parse error");
198    }
199
200    #[test]
201    fn error_layout_invalid() {
202        let e = Error::LayoutInvalid("bad layout".into());
203        assert_eq!(e.to_string(), "Layout validation failed: bad layout");
204    }
205
206    #[test]
207    fn error_transform_failed() {
208        let e = Error::TransformFailed("script error".into());
209        assert_eq!(e.to_string(), "Context transform failed: script error");
210    }
211
212    #[test]
213    fn error_other() {
214        let e = Error::Other("misc".into());
215        assert_eq!(e.to_string(), "misc");
216    }
217
218    #[test]
219    fn error_from_serde_json() {
220        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
221        let e = Error::from(json_err);
222        assert!(e.to_string().contains("Serialization error"));
223    }
224
225    // ─── Clone for ValidationError ──────────────────────────────────────────
226
227    #[test]
228    fn validation_error_is_cloneable() {
229        let e = ValidationError::Graph("cycle".into());
230        let cloned = e.clone();
231        assert_eq!(e.to_string(), cloned.to_string());
232    }
233}