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 { stage: String, message: String },
18
19    /// Region-level validation failure
20    #[error("Invalid region '{region}': {message}")]
21    Region { region: String, message: String },
22
23    /// Layout-level validation failure
24    #[error("Invalid layout: {0}")]
25    Layout(String),
26
27    /// Graph structure validation failure
28    #[error("Invalid graph: {0}")]
29    Graph(String),
30
31    /// Transition validation failure
32    #[error("Invalid transition from '{from}' to '{to}': {message}")]
33    Transition {
34        from: String,
35        to: String,
36        message: String,
37    },
38}
39
40/// Core error types for Leviath.
41#[derive(Error, Debug)]
42pub enum Error {
43    /// Region with the specified name was not found
44    #[error("Region not found: {0}")]
45    RegionNotFound(String),
46
47    /// Region validation failed
48    #[error("Region validation failed: {0}")]
49    ValidationFailed(String),
50
51    /// Content exceeds region's token budget
52    #[error("Content exceeds token budget: {used} > {max}")]
53    TokenBudgetExceeded { used: usize, max: usize },
54
55    /// Pinned regions alone exceed total token budget
56    #[error("Pinned regions ({pinned_tokens}) exceed total budget ({total_budget})")]
57    PinnedRegionsOverBudget {
58        pinned_tokens: usize,
59        total_budget: usize,
60    },
61
62    /// Blueprint validation failed
63    #[error("Blueprint validation failed: {0}")]
64    BlueprintInvalid(String),
65
66    /// Layout validation failed
67    #[error("Layout validation failed: {0}")]
68    LayoutInvalid(String),
69
70    /// Context transform failed
71    #[error("Context transform failed: {0}")]
72    TransformFailed(String),
73
74    /// Serialization error
75    #[error("Serialization error: {0}")]
76    SerializationError(#[from] serde_json::Error),
77
78    /// Generic error
79    #[error("{0}")]
80    Other(String),
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    // ─── ValidationError Display ────────────────────────────────────────────
88
89    #[test]
90    fn validation_error_blueprint() {
91        let e = ValidationError::Blueprint("missing name".into());
92        assert_eq!(e.to_string(), "Invalid blueprint: missing name");
93    }
94
95    #[test]
96    fn validation_error_stage() {
97        let e = ValidationError::Stage {
98            stage: "init".into(),
99            message: "no prompt".into(),
100        };
101        assert_eq!(e.to_string(), "Invalid stage 'init': no prompt");
102    }
103
104    #[test]
105    fn validation_error_region() {
106        let e = ValidationError::Region {
107            region: "context".into(),
108            message: "too large".into(),
109        };
110        assert_eq!(e.to_string(), "Invalid region 'context': too large");
111    }
112
113    #[test]
114    fn validation_error_layout() {
115        let e = ValidationError::Layout("overlapping regions".into());
116        assert_eq!(e.to_string(), "Invalid layout: overlapping regions");
117    }
118
119    #[test]
120    fn validation_error_graph() {
121        let e = ValidationError::Graph("cycle detected".into());
122        assert_eq!(e.to_string(), "Invalid graph: cycle detected");
123    }
124
125    #[test]
126    fn validation_error_transition() {
127        let e = ValidationError::Transition {
128            from: "A".into(),
129            to: "B".into(),
130            message: "missing condition".into(),
131        };
132        assert_eq!(
133            e.to_string(),
134            "Invalid transition from 'A' to 'B': missing condition"
135        );
136    }
137
138    // ─── Error Display ──────────────────────────────────────────────────────
139
140    #[test]
141    fn error_region_not_found() {
142        let e = Error::RegionNotFound("history".into());
143        assert_eq!(e.to_string(), "Region not found: history");
144    }
145
146    #[test]
147    fn error_validation_failed() {
148        let e = Error::ValidationFailed("bad input".into());
149        assert_eq!(e.to_string(), "Region validation failed: bad input");
150    }
151
152    #[test]
153    fn error_token_budget_exceeded() {
154        let e = Error::TokenBudgetExceeded {
155            used: 500,
156            max: 100,
157        };
158        assert_eq!(e.to_string(), "Content exceeds token budget: 500 > 100");
159    }
160
161    #[test]
162    fn error_pinned_regions_over_budget() {
163        let e = Error::PinnedRegionsOverBudget {
164            pinned_tokens: 2000,
165            total_budget: 1000,
166        };
167        assert_eq!(
168            e.to_string(),
169            "Pinned regions (2000) exceed total budget (1000)"
170        );
171    }
172
173    #[test]
174    fn error_blueprint_invalid() {
175        let e = Error::BlueprintInvalid("parse error".into());
176        assert_eq!(e.to_string(), "Blueprint validation failed: parse error");
177    }
178
179    #[test]
180    fn error_layout_invalid() {
181        let e = Error::LayoutInvalid("bad layout".into());
182        assert_eq!(e.to_string(), "Layout validation failed: bad layout");
183    }
184
185    #[test]
186    fn error_transform_failed() {
187        let e = Error::TransformFailed("script error".into());
188        assert_eq!(e.to_string(), "Context transform failed: script error");
189    }
190
191    #[test]
192    fn error_other() {
193        let e = Error::Other("misc".into());
194        assert_eq!(e.to_string(), "misc");
195    }
196
197    #[test]
198    fn error_from_serde_json() {
199        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
200        let e = Error::from(json_err);
201        assert!(e.to_string().contains("Serialization error"));
202    }
203
204    // ─── Clone for ValidationError ──────────────────────────────────────────
205
206    #[test]
207    fn validation_error_is_cloneable() {
208        let e = ValidationError::Graph("cycle".into());
209        let cloned = e.clone();
210        assert_eq!(e.to_string(), cloned.to_string());
211    }
212}