1use rust_decimal::Decimal;
4use thiserror::Error;
5
6use ironflow_artifacts::error::ArtifactError;
7use ironflow_core::error::OperationError;
8use ironflow_store::error::StoreError;
9
10pub const RUN_BUDGET_EXCEEDED_CODE: &str = "RUN_BUDGET_EXCEEDED";
12
13pub const MONTHLY_BUDGET_EXCEEDED_CODE: &str = "MONTHLY_BUDGET_EXCEEDED";
15
16#[derive(Debug, Error)]
18pub enum EngineError {
19 #[error("operation failed: {0}")]
21 Operation(#[from] OperationError),
22
23 #[error("store error: {0}")]
25 Store(#[from] StoreError),
26
27 #[error("invalid workflow: {0}")]
29 InvalidWorkflow(String),
30
31 #[error("step config error: {0}")]
33 StepConfig(String),
34
35 #[error("serialization error: {0}")]
37 Serialization(#[from] serde_json::Error),
38
39 #[error(
45 "{RUN_BUDGET_EXCEEDED_CODE}: run {run_id} would exceed its cost cap \
46 (spent {spent_usd} USD + next step {step_budget_usd} USD > cap {limit_usd} USD)"
47 )]
48 RunBudgetExceeded {
49 run_id: uuid::Uuid,
51 limit_usd: Decimal,
53 spent_usd: Decimal,
55 step_budget_usd: Decimal,
57 },
58
59 #[error(
63 "{MONTHLY_BUDGET_EXCEEDED_CODE}: monthly cost quota exhausted \
64 ({spent_usd} USD spent of {limit_usd} USD)"
65 )]
66 MonthlyBudgetExceeded {
67 limit_usd: Decimal,
69 spent_usd: Decimal,
71 },
72
73 #[error("step {step:?} declared output {pattern:?} but no file matched")]
79 MissingArtifact {
80 step: String,
82 pattern: String,
84 },
85
86 #[error("no artifact {name:?} produced by step {step:?} before this point")]
88 ArtifactNotFound {
89 step: String,
91 name: String,
93 },
94
95 #[error("artifact storage is not configured: {0}")]
97 ArtifactsUnavailable(String),
98
99 #[error("artifact storage error: {0}")]
101 Artifact(#[from] ArtifactError),
102
103 #[error("approval required for run {run_id}, step {step_id}: {message}")]
105 ApprovalRequired {
106 run_id: uuid::Uuid,
108 step_id: uuid::Uuid,
110 message: String,
112 },
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn invalid_workflow_display() {
121 let err = EngineError::InvalidWorkflow("unknown-handler".to_string());
122 assert!(err.to_string().contains("invalid workflow"));
123 assert!(err.to_string().contains("unknown-handler"));
124 }
125
126 #[test]
127 fn step_config_display() {
128 let err = EngineError::StepConfig("bad shell config".to_string());
129 assert!(err.to_string().contains("step config error"));
130 assert!(err.to_string().contains("bad shell config"));
131 }
132
133 #[test]
134 fn store_error_from_conversion() {
135 let store_err = StoreError::RunNotFound(uuid::Uuid::nil());
136 let engine_err = EngineError::from(store_err);
137 assert!(engine_err.to_string().contains("store error"));
138 }
139
140 #[test]
141 fn run_budget_exceeded_display_carries_code_and_amounts() {
142 let err = EngineError::RunBudgetExceeded {
143 run_id: uuid::Uuid::nil(),
144 limit_usd: Decimal::new(200, 2),
145 spent_usd: Decimal::new(180, 2),
146 step_budget_usd: Decimal::new(50, 2),
147 };
148
149 let msg = err.to_string();
150 assert!(msg.contains(RUN_BUDGET_EXCEEDED_CODE));
151 assert!(msg.contains("2.00"));
152 assert!(msg.contains("1.80"));
153 assert!(msg.contains("0.50"));
154 }
155
156 #[test]
157 fn monthly_budget_exceeded_display_carries_code_and_amounts() {
158 let err = EngineError::MonthlyBudgetExceeded {
159 limit_usd: Decimal::new(10000, 2),
160 spent_usd: Decimal::new(10500, 2),
161 };
162
163 let msg = err.to_string();
164 assert!(msg.contains(MONTHLY_BUDGET_EXCEEDED_CODE));
165 assert!(msg.contains("100.00"));
166 assert!(msg.contains("105.00"));
167 }
168
169 #[test]
170 fn missing_artifact_display_names_the_step_and_pattern() {
171 let err = EngineError::MissingArtifact {
172 step: "build".to_string(),
173 pattern: "target/report.html".to_string(),
174 };
175
176 let msg = err.to_string();
177 assert!(msg.contains("\"build\""));
178 assert!(msg.contains("target/report.html"));
179 }
180
181 #[test]
182 fn artifact_not_found_display_names_the_producer() {
183 let err = EngineError::ArtifactNotFound {
184 step: "build".to_string(),
185 name: "report.html".to_string(),
186 };
187
188 let msg = err.to_string();
189 assert!(msg.contains("\"build\""));
190 assert!(msg.contains("report.html"));
191 }
192
193 #[test]
194 fn artifacts_unavailable_display() {
195 let err = EngineError::ArtifactsUnavailable("no blob store".to_string());
196 assert!(err.to_string().contains("not configured"));
197 }
198
199 #[test]
200 fn artifact_error_from_conversion() {
201 let engine_err = EngineError::from(ArtifactError::NotFound("a/b".to_string()));
202 assert!(engine_err.to_string().contains("artifact storage error"));
203 }
204
205 #[test]
206 fn serialization_error_from_conversion() {
207 let serde_err = serde_json::from_str::<String>("not json").unwrap_err();
208 let engine_err = EngineError::from(serde_err);
209 assert!(engine_err.to_string().contains("serialization error"));
210 }
211}