1use rust_decimal::Decimal;
4use thiserror::Error;
5
6use ironflow_core::error::OperationError;
7use ironflow_store::error::StoreError;
8
9pub const RUN_BUDGET_EXCEEDED_CODE: &str = "RUN_BUDGET_EXCEEDED";
11
12pub const MONTHLY_BUDGET_EXCEEDED_CODE: &str = "MONTHLY_BUDGET_EXCEEDED";
14
15#[derive(Debug, Error)]
17pub enum EngineError {
18 #[error("operation failed: {0}")]
20 Operation(#[from] OperationError),
21
22 #[error("store error: {0}")]
24 Store(#[from] StoreError),
25
26 #[error("invalid workflow: {0}")]
28 InvalidWorkflow(String),
29
30 #[error("step config error: {0}")]
32 StepConfig(String),
33
34 #[error("serialization error: {0}")]
36 Serialization(#[from] serde_json::Error),
37
38 #[error(
44 "{RUN_BUDGET_EXCEEDED_CODE}: run {run_id} would exceed its cost cap \
45 (spent {spent_usd} USD + next step {step_budget_usd} USD > cap {limit_usd} USD)"
46 )]
47 RunBudgetExceeded {
48 run_id: uuid::Uuid,
50 limit_usd: Decimal,
52 spent_usd: Decimal,
54 step_budget_usd: Decimal,
56 },
57
58 #[error(
62 "{MONTHLY_BUDGET_EXCEEDED_CODE}: monthly cost quota exhausted \
63 ({spent_usd} USD spent of {limit_usd} USD)"
64 )]
65 MonthlyBudgetExceeded {
66 limit_usd: Decimal,
68 spent_usd: Decimal,
70 },
71
72 #[error("approval required for run {run_id}, step {step_id}: {message}")]
74 ApprovalRequired {
75 run_id: uuid::Uuid,
77 step_id: uuid::Uuid,
79 message: String,
81 },
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn invalid_workflow_display() {
90 let err = EngineError::InvalidWorkflow("unknown-handler".to_string());
91 assert!(err.to_string().contains("invalid workflow"));
92 assert!(err.to_string().contains("unknown-handler"));
93 }
94
95 #[test]
96 fn step_config_display() {
97 let err = EngineError::StepConfig("bad shell config".to_string());
98 assert!(err.to_string().contains("step config error"));
99 assert!(err.to_string().contains("bad shell config"));
100 }
101
102 #[test]
103 fn store_error_from_conversion() {
104 let store_err = StoreError::RunNotFound(uuid::Uuid::nil());
105 let engine_err = EngineError::from(store_err);
106 assert!(engine_err.to_string().contains("store error"));
107 }
108
109 #[test]
110 fn run_budget_exceeded_display_carries_code_and_amounts() {
111 let err = EngineError::RunBudgetExceeded {
112 run_id: uuid::Uuid::nil(),
113 limit_usd: Decimal::new(200, 2),
114 spent_usd: Decimal::new(180, 2),
115 step_budget_usd: Decimal::new(50, 2),
116 };
117
118 let msg = err.to_string();
119 assert!(msg.contains(RUN_BUDGET_EXCEEDED_CODE));
120 assert!(msg.contains("2.00"));
121 assert!(msg.contains("1.80"));
122 assert!(msg.contains("0.50"));
123 }
124
125 #[test]
126 fn monthly_budget_exceeded_display_carries_code_and_amounts() {
127 let err = EngineError::MonthlyBudgetExceeded {
128 limit_usd: Decimal::new(10000, 2),
129 spent_usd: Decimal::new(10500, 2),
130 };
131
132 let msg = err.to_string();
133 assert!(msg.contains(MONTHLY_BUDGET_EXCEEDED_CODE));
134 assert!(msg.contains("100.00"));
135 assert!(msg.contains("105.00"));
136 }
137
138 #[test]
139 fn serialization_error_from_conversion() {
140 let serde_err = serde_json::from_str::<String>("not json").unwrap_err();
141 let engine_err = EngineError::from(serde_err);
142 assert!(engine_err.to_string().contains("serialization error"));
143 }
144}