Skip to main content

ironflow_store/
error.rs

1//! Storage error types.
2//!
3//! [`StoreError`] covers all failure modes for [`RunStore`](crate::store::RunStore)
4//! implementations, from missing records to invalid FSM transitions.
5
6use thiserror::Error;
7use uuid::Uuid;
8
9use crate::entities::{ConcurrencyLimitError, RunStatus, WorkerTagError};
10
11/// Errors produced by [`RunStore`](crate::store::RunStore) operations.
12///
13/// # Examples
14///
15/// ```
16/// use ironflow_store::error::StoreError;
17/// use uuid::Uuid;
18///
19/// let err = StoreError::RunNotFound(Uuid::nil());
20/// assert!(err.to_string().contains("run not found"));
21/// ```
22#[derive(Debug, Error)]
23pub enum StoreError {
24    /// The requested run does not exist.
25    #[error("run not found: {0}")]
26    RunNotFound(Uuid),
27
28    /// The requested step does not exist.
29    #[error("step not found: {0}")]
30    StepNotFound(Uuid),
31
32    /// Attempted an illegal FSM transition.
33    #[error("invalid status transition: {from} -> {to}")]
34    InvalidTransition {
35        /// Current status.
36        from: RunStatus,
37        /// Attempted target status.
38        to: RunStatus,
39    },
40
41    /// Email is already taken by another user.
42    #[error("email already exists: {0}")]
43    DuplicateEmail(String),
44
45    /// Username is already taken by another user.
46    #[error("username already exists: {0}")]
47    DuplicateUsername(String),
48
49    /// The requested user does not exist.
50    #[error("user not found: {0}")]
51    UserNotFound(Uuid),
52
53    /// The worker no longer holds the lease on this run.
54    ///
55    /// Either another worker took it over after the lease expired, or the run
56    /// left the `Running` state (cancelled, failed, awaiting approval).
57    #[error("lease lost on run {run_id}")]
58    LeaseLost {
59        /// Run whose lease was lost.
60        run_id: Uuid,
61        /// Worker currently holding the lease, if any.
62        held_by: Option<String>,
63    },
64
65    /// The step already holds an artifact with this name.
66    #[error("artifact {name:?} already exists on step {step_id}")]
67    DuplicateArtifact {
68        /// Step that already owns the name.
69        step_id: Uuid,
70        /// The conflicting artifact name.
71        name: String,
72    },
73
74    /// Another non-terminal run already holds this concurrency key.
75    #[error("concurrency key {key:?} is held by active run {run_id}")]
76    ConcurrencyConflict {
77        /// The contested key.
78        key: String,
79        /// The run holding it.
80        run_id: Uuid,
81    },
82
83    /// The concurrency limits of a run are invalid.
84    #[error("invalid concurrency limit: {0}")]
85    InvalidConcurrencyLimit(#[from] ConcurrencyLimitError),
86
87    /// The worker tags of a run are invalid.
88    #[error("invalid worker tag: {0}")]
89    InvalidWorkerTag(#[from] WorkerTagError),
90
91    /// The requested schedule does not exist.
92    #[error("schedule not found: {0}")]
93    ScheduleNotFound(Uuid),
94
95    /// The requested approval delegation does not exist.
96    #[error("approval delegation not found: {0}")]
97    DelegationNotFound(Uuid),
98
99    /// The requested provider account does not exist.
100    #[error("provider account not found: {0}")]
101    ProviderAccountNotFound(Uuid),
102
103    /// A provider account with this name already exists.
104    #[error("provider account already exists: {0}")]
105    DuplicateProviderAccount(String),
106
107    /// The requested agent role was never seen by the store.
108    #[error("agent role '{0}' not found")]
109    AgentRoleNotFound(String),
110
111    /// A database or I/O error from the backing store.
112    #[error("database error: {0}")]
113    Database(String),
114
115    /// JSON serialization or deserialization failed.
116    #[error("serialization error: {0}")]
117    Serialization(#[from] serde_json::Error),
118
119    /// A cryptographic operation failed (encryption or decryption).
120    #[error("crypto error: {0}")]
121    Crypto(String),
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn run_not_found_display() {
130        let id = Uuid::nil();
131        let err = StoreError::RunNotFound(id);
132        assert_eq!(err.to_string(), format!("run not found: {id}"));
133    }
134
135    #[test]
136    fn agent_role_not_found_display() {
137        let err = StoreError::AgentRoleNotFound("reviewer".to_string());
138        assert_eq!(err.to_string(), "agent role 'reviewer' not found");
139    }
140
141    #[test]
142    fn step_not_found_display() {
143        let id = Uuid::nil();
144        let err = StoreError::StepNotFound(id);
145        assert_eq!(err.to_string(), format!("step not found: {id}"));
146    }
147
148    #[test]
149    fn invalid_transition_display() {
150        let err = StoreError::InvalidTransition {
151            from: RunStatus::Pending,
152            to: RunStatus::Completed,
153        };
154        assert!(err.to_string().contains("Pending"));
155        assert!(err.to_string().contains("Completed"));
156    }
157
158    #[test]
159    fn concurrency_conflict_display() {
160        let run_id = Uuid::now_v7();
161        let err = StoreError::ConcurrencyConflict {
162            key: "issue:12".to_string(),
163            run_id,
164        };
165        let message = err.to_string();
166        assert!(message.contains("issue:12"));
167        assert!(message.contains(&run_id.to_string()));
168    }
169
170    #[test]
171    fn invalid_concurrency_limit_display() {
172        let err = StoreError::from(ConcurrencyLimitError::EmptyGroup);
173        assert_eq!(
174            err.to_string(),
175            "invalid concurrency limit: concurrency group must not be empty"
176        );
177    }
178
179    #[test]
180    fn invalid_worker_tag_display() {
181        let err = StoreError::from(WorkerTagError::Empty);
182        assert_eq!(
183            err.to_string(),
184            "invalid worker tag: worker tag must not be empty"
185        );
186    }
187
188    #[test]
189    fn delegation_not_found_display() {
190        let id = Uuid::nil();
191        let err = StoreError::DelegationNotFound(id);
192        assert_eq!(
193            err.to_string(),
194            format!("approval delegation not found: {id}")
195        );
196    }
197
198    #[test]
199    fn database_error_display() {
200        let err = StoreError::Database("connection refused".to_string());
201        assert!(err.to_string().contains("connection refused"));
202    }
203
204    #[test]
205    fn serialization_error_from_serde() {
206        let serde_err = serde_json::from_str::<String>("not json").unwrap_err();
207        let err = StoreError::from(serde_err);
208        assert!(err.to_string().contains("serialization error"));
209    }
210}