Skip to main content

durable_execution_sdk/
error.rs

1//! Error types for the AWS Durable Execution SDK.
2//!
3//! This module defines a comprehensive error hierarchy for handling
4//! different failure modes in durable execution workflows.
5//!
6//! # Type Aliases
7//!
8//! This module provides semantic type aliases for common result types:
9//!
10//! - [`DurableResult<T>`] - General result type for durable operations
11//! - [`StepResult<T>`] - Result type for step operations
12//! - [`CheckpointResult<T>`] - Result type for checkpoint operations
13//!
14//! These aliases improve code readability and make function signatures
15//! more self-documenting.
16
17use serde::{Deserialize, Serialize};
18use thiserror::Error;
19
20// =============================================================================
21// Result Type Aliases
22// =============================================================================
23
24/// Result type for durable operations.
25///
26/// This is a type alias for `Result<T, DurableError>`, providing a more
27/// semantic and concise way to express the return type of durable operations.
28///
29/// # Examples
30///
31/// ```rust
32/// use durable_execution_sdk::{DurableResult, DurableError};
33///
34/// fn process_data(data: &str) -> DurableResult<String> {
35///     if data.is_empty() {
36///         Err(DurableError::validation("Data cannot be empty"))
37///     } else {
38///         Ok(data.to_uppercase())
39///     }
40/// }
41/// ```
42///
43/// # Expanded Form
44///
45/// ```rust,ignore
46/// type DurableResult<T> = Result<T, DurableError>;
47/// ```
48pub type DurableResult<T> = Result<T, DurableError>;
49
50/// Result type for step operations.
51///
52/// This is a type alias for `Result<T, DurableError>`, specifically intended
53/// for step function return types. Using this alias makes it clear that a
54/// function is a step operation within a durable execution.
55///
56/// # Examples
57///
58/// ```rust
59/// use durable_execution_sdk::{StepResult, DurableError};
60///
61/// fn validate_input(input: &str) -> StepResult<String> {
62///     if input.len() > 100 {
63///         Err(DurableError::validation("Input too long"))
64///     } else {
65///         Ok(input.to_string())
66///     }
67/// }
68/// ```
69///
70/// # Expanded Form
71///
72/// ```rust,ignore
73/// type StepResult<T> = Result<T, DurableError>;
74/// ```
75pub type StepResult<T> = Result<T, DurableError>;
76
77/// Result type for checkpoint operations.
78///
79/// This is a type alias for `Result<T, DurableError>`, specifically intended
80/// for checkpoint-related operations. Using this alias makes it clear that a
81/// function performs checkpointing within a durable execution.
82///
83/// # Examples
84///
85/// ```rust
86/// use durable_execution_sdk::{CheckpointResult, DurableError};
87///
88/// fn save_checkpoint(data: &[u8]) -> CheckpointResult<()> {
89///     if data.len() > 1_000_000 {
90///         Err(DurableError::size_limit("Checkpoint data too large"))
91///     } else {
92///         Ok(())
93///     }
94/// }
95/// ```
96///
97/// # Expanded Form
98///
99/// ```rust,ignore
100/// type CheckpointResult<T> = Result<T, DurableError>;
101/// ```
102pub type CheckpointResult<T> = Result<T, DurableError>;
103
104/// The main error type for the AWS Durable Execution SDK.
105///
106/// This enum covers all possible error conditions that can occur
107/// during durable execution workflows.
108///
109/// # Examples
110///
111/// Creating common error types:
112///
113/// ```
114/// use durable_execution_sdk::DurableError;
115///
116/// // Execution error (fails workflow without retry)
117/// let exec_err = DurableError::execution("Order validation failed");
118/// assert!(!exec_err.is_retriable());
119///
120/// // Validation error
121/// let val_err = DurableError::validation("Invalid input format");
122///
123/// // Retriable checkpoint error
124/// let cp_err = DurableError::checkpoint_retriable("Temporary network issue");
125/// assert!(cp_err.is_retriable());
126/// ```
127///
128/// Checking error types:
129///
130/// ```
131/// use durable_execution_sdk::DurableError;
132///
133/// let err = DurableError::size_limit("Payload too large");
134/// assert!(err.is_size_limit());
135/// assert!(!err.is_throttling());
136///
137/// let throttle_err = DurableError::throttling("Rate limit exceeded");
138/// assert!(throttle_err.is_throttling());
139/// ```
140#[derive(Debug, Error)]
141pub enum DurableError {
142    /// Execution error that returns FAILED status without Lambda retry.
143    #[error("Execution error: {message}")]
144    Execution {
145        /// Error message describing what went wrong
146        message: String,
147        /// The reason for termination
148        termination_reason: TerminationReason,
149    },
150
151    /// Invocation error that triggers Lambda retry.
152    #[error("Invocation error: {message}")]
153    Invocation {
154        /// Error message describing what went wrong
155        message: String,
156        /// The reason for termination
157        termination_reason: TerminationReason,
158    },
159
160    /// Checkpoint error for checkpoint failures.
161    #[error("Checkpoint error: {message}")]
162    Checkpoint {
163        /// Error message describing what went wrong
164        message: String,
165        /// Whether this error is retriable
166        is_retriable: bool,
167        /// Optional underlying AWS error details
168        aws_error: Option<AwsError>,
169    },
170
171    /// Callback error for callback-specific failures.
172    #[error("Callback error: {message}")]
173    Callback {
174        /// Error message describing what went wrong
175        message: String,
176        /// The callback ID if available
177        callback_id: Option<String>,
178    },
179
180    /// Non-deterministic execution error for replay mismatches.
181    #[error("Non-deterministic execution: {message}")]
182    NonDeterministic {
183        /// Error message describing the mismatch
184        message: String,
185        /// The operation ID where the mismatch occurred
186        operation_id: Option<String>,
187    },
188
189    /// Validation error for invalid configuration or arguments.
190    #[error("Validation error: {message}")]
191    Validation {
192        /// Error message describing the validation failure
193        message: String,
194    },
195
196    /// Serialization/deserialization error.
197    #[error("Serialization error: {message}")]
198    SerDes {
199        /// Error message describing the serialization failure
200        message: String,
201    },
202
203    /// Suspend execution signal to pause and return control to Lambda runtime.
204    #[error("Suspend execution")]
205    Suspend {
206        /// Optional timestamp when execution should resume
207        scheduled_timestamp: Option<f64>,
208    },
209
210    /// Orphaned child error when a child operation's parent has completed.
211    #[error("Orphaned child: {message}")]
212    OrphanedChild {
213        /// Error message describing the orphaned state
214        message: String,
215        /// The operation ID of the orphaned child
216        operation_id: String,
217    },
218
219    /// User code error wrapping errors from user-provided closures.
220    #[error("User code error: {message}")]
221    UserCode {
222        /// Error message from the user code
223        message: String,
224        /// The type of error
225        error_type: String,
226        /// Optional stack trace
227        stack_trace: Option<String>,
228    },
229
230    /// Size limit exceeded error for payload size violations.
231    ///
232    /// This error occurs when:
233    /// - Checkpoint payload exceeds the maximum allowed size
234    /// - Response payload exceeds Lambda's 6MB limit
235    /// - History size exceeds service limits
236    ///
237    /// This error is NOT retriable - the operation should fail without retry.
238    #[error("Size limit exceeded: {message}")]
239    SizeLimit {
240        /// Error message describing the size limit violation
241        message: String,
242        /// The actual size that exceeded the limit (in bytes)
243        actual_size: Option<usize>,
244        /// The maximum allowed size (in bytes)
245        max_size: Option<usize>,
246    },
247
248    /// Throttling error for rate limit exceeded.
249    ///
250    /// This error occurs when the AWS API rate limit is exceeded.
251    /// This error IS retriable with exponential backoff.
252    #[error("Throttling: {message}")]
253    Throttling {
254        /// Error message describing the throttling condition
255        message: String,
256        /// Suggested retry delay in milliseconds (if provided by AWS)
257        retry_after_ms: Option<u64>,
258    },
259
260    /// Resource not found error for missing executions or operations.
261    ///
262    /// This error occurs when:
263    /// - The durable execution ARN does not exist
264    /// - The operation ID is not found
265    /// - The Lambda function does not exist
266    ///
267    /// This error is NOT retriable.
268    #[error("Resource not found: {message}")]
269    ResourceNotFound {
270        /// Error message describing what resource was not found
271        message: String,
272        /// The resource identifier that was not found
273        resource_id: Option<String>,
274    },
275
276    /// Configuration error for missing or invalid SDK configuration.
277    ///
278    /// This error occurs when:
279    /// - No AWS credentials provider is configured
280    /// - HTTP client construction fails
281    /// - Required configuration values are missing
282    ///
283    /// This error is NOT retriable.
284    #[error("Configuration error: {message}")]
285    Configuration {
286        /// Error message describing the configuration issue
287        message: String,
288    },
289}
290
291impl DurableError {
292    /// Creates a new Execution error.
293    pub fn execution(message: impl Into<String>) -> Self {
294        Self::Execution {
295            message: message.into(),
296            termination_reason: TerminationReason::ExecutionError,
297        }
298    }
299
300    /// Creates a new Invocation error.
301    pub fn invocation(message: impl Into<String>) -> Self {
302        Self::Invocation {
303            message: message.into(),
304            termination_reason: TerminationReason::InvocationError,
305        }
306    }
307
308    /// Creates a new retriable Checkpoint error.
309    pub fn checkpoint_retriable(message: impl Into<String>) -> Self {
310        Self::Checkpoint {
311            message: message.into(),
312            is_retriable: true,
313            aws_error: None,
314        }
315    }
316
317    /// Creates a new non-retriable Checkpoint error.
318    pub fn checkpoint_non_retriable(message: impl Into<String>) -> Self {
319        Self::Checkpoint {
320            message: message.into(),
321            is_retriable: false,
322            aws_error: None,
323        }
324    }
325
326    /// Creates a new Validation error.
327    pub fn validation(message: impl Into<String>) -> Self {
328        Self::Validation {
329            message: message.into(),
330        }
331    }
332
333    /// Creates a new SerDes error.
334    pub fn serdes(message: impl Into<String>) -> Self {
335        Self::SerDes {
336            message: message.into(),
337        }
338    }
339
340    /// Creates a new Suspend signal.
341    pub fn suspend() -> Self {
342        Self::Suspend {
343            scheduled_timestamp: None,
344        }
345    }
346
347    /// Creates a new Suspend signal with a scheduled timestamp.
348    pub fn suspend_until(timestamp: f64) -> Self {
349        Self::Suspend {
350            scheduled_timestamp: Some(timestamp),
351        }
352    }
353
354    /// Creates a new SizeLimit error.
355    ///
356    /// # Arguments
357    ///
358    /// * `message` - Description of the size limit violation
359    pub fn size_limit(message: impl Into<String>) -> Self {
360        Self::SizeLimit {
361            message: message.into(),
362            actual_size: None,
363            max_size: None,
364        }
365    }
366
367    /// Creates a new SizeLimit error with size details.
368    ///
369    /// # Arguments
370    ///
371    /// * `message` - Description of the size limit violation
372    /// * `actual_size` - The actual size that exceeded the limit
373    /// * `max_size` - The maximum allowed size
374    pub fn size_limit_with_details(
375        message: impl Into<String>,
376        actual_size: usize,
377        max_size: usize,
378    ) -> Self {
379        Self::SizeLimit {
380            message: message.into(),
381            actual_size: Some(actual_size),
382            max_size: Some(max_size),
383        }
384    }
385
386    /// Creates a new Throttling error.
387    ///
388    /// # Arguments
389    ///
390    /// * `message` - Description of the throttling condition
391    pub fn throttling(message: impl Into<String>) -> Self {
392        Self::Throttling {
393            message: message.into(),
394            retry_after_ms: None,
395        }
396    }
397
398    /// Creates a new Throttling error with retry delay.
399    ///
400    /// # Arguments
401    ///
402    /// * `message` - Description of the throttling condition
403    /// * `retry_after_ms` - Suggested retry delay in milliseconds
404    pub fn throttling_with_retry_delay(message: impl Into<String>, retry_after_ms: u64) -> Self {
405        Self::Throttling {
406            message: message.into(),
407            retry_after_ms: Some(retry_after_ms),
408        }
409    }
410
411    /// Creates a new ResourceNotFound error.
412    ///
413    /// # Arguments
414    ///
415    /// * `message` - Description of what resource was not found
416    pub fn resource_not_found(message: impl Into<String>) -> Self {
417        Self::ResourceNotFound {
418            message: message.into(),
419            resource_id: None,
420        }
421    }
422
423    /// Creates a new ResourceNotFound error with resource ID.
424    ///
425    /// # Arguments
426    ///
427    /// * `message` - Description of what resource was not found
428    /// * `resource_id` - The identifier of the resource that was not found
429    pub fn resource_not_found_with_id(
430        message: impl Into<String>,
431        resource_id: impl Into<String>,
432    ) -> Self {
433        Self::ResourceNotFound {
434            message: message.into(),
435            resource_id: Some(resource_id.into()),
436        }
437    }
438
439    /// Returns true if this is a Checkpoint error that is retriable.
440    pub fn is_retriable(&self) -> bool {
441        matches!(
442            self,
443            Self::Checkpoint {
444                is_retriable: true,
445                ..
446            }
447        )
448    }
449
450    /// Returns true if this is a Suspend signal.
451    pub fn is_suspend(&self) -> bool {
452        matches!(self, Self::Suspend { .. })
453    }
454
455    /// Returns true if this is an invalid checkpoint token error.
456    ///
457    /// Invalid checkpoint token errors occur when:
458    /// - The token has already been consumed by a previous checkpoint
459    /// - The token is malformed or expired
460    ///
461    /// These errors are retriable because Lambda will provide a fresh token
462    /// on the next invocation.
463    pub fn is_invalid_checkpoint_token(&self) -> bool {
464        match self {
465            Self::Checkpoint {
466                aws_error: Some(aws_error),
467                ..
468            } => {
469                aws_error.code == "InvalidParameterValueException"
470                    && aws_error.message.contains("Invalid checkpoint token")
471            }
472            Self::Checkpoint { message, .. } => message.contains("Invalid checkpoint token"),
473            _ => false,
474        }
475    }
476
477    /// Returns true if this is a SizeLimit error.
478    ///
479    /// Size limit errors are NOT retriable - the operation should fail.
480    pub fn is_size_limit(&self) -> bool {
481        matches!(self, Self::SizeLimit { .. })
482    }
483
484    /// Returns true if this is a Throttling error.
485    ///
486    /// Throttling errors ARE retriable with exponential backoff.
487    pub fn is_throttling(&self) -> bool {
488        matches!(self, Self::Throttling { .. })
489    }
490
491    /// Returns true if this is a ResourceNotFound error.
492    ///
493    /// Resource not found errors are NOT retriable.
494    pub fn is_resource_not_found(&self) -> bool {
495        matches!(self, Self::ResourceNotFound { .. })
496    }
497
498    /// Returns the suggested retry delay for throttling errors.
499    ///
500    /// Returns `None` if this is not a throttling error or if no retry delay was provided.
501    pub fn get_retry_after_ms(&self) -> Option<u64> {
502        match self {
503            Self::Throttling { retry_after_ms, .. } => *retry_after_ms,
504            _ => None,
505        }
506    }
507}
508
509/// Reason for execution termination.
510///
511/// This enum uses `#[repr(u8)]` for compact memory representation (1 byte).
512/// Explicit discriminant values ensure stability across versions.
513#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
514#[repr(u8)]
515pub enum TerminationReason {
516    /// Unhandled error in user code
517    #[default]
518    UnhandledError = 0,
519    /// Error during Lambda invocation
520    InvocationError = 1,
521    /// Explicit execution error
522    ExecutionError = 2,
523    /// Checkpoint operation failed
524    CheckpointFailed = 3,
525    /// Non-deterministic execution detected
526    NonDeterministicExecution = 4,
527    /// Step was interrupted
528    StepInterrupted = 5,
529    /// Callback operation failed
530    CallbackError = 6,
531    /// Serialization/deserialization failed
532    SerializationError = 7,
533    /// Size limit exceeded (payload, response, or history)
534    SizeLimitExceeded = 8,
535    /// Operation was explicitly terminated
536    OperationTerminated = 9,
537    /// A retry has been scheduled; execution will resume
538    RetryScheduled = 10,
539    /// A wait operation has been scheduled
540    WaitScheduled = 11,
541    /// A callback is pending external completion
542    CallbackPending = 12,
543    /// Context validation failed
544    ContextValidationError = 13,
545    /// Lambda timeout approaching — graceful shutdown
546    LambdaTimeoutApproaching = 14,
547}
548
549/// AWS error details for checkpoint failures.
550#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct AwsError {
552    /// The AWS error code
553    pub code: String,
554    /// The AWS error message
555    pub message: String,
556    /// The request ID if available
557    pub request_id: Option<String>,
558}
559
560/// Error object for serialization in Lambda responses.
561///
562/// # Examples
563///
564/// ```
565/// use durable_execution_sdk::ErrorObject;
566///
567/// // Basic error object
568/// let err = ErrorObject::new("ValidationError", "Invalid input");
569/// assert_eq!(err.error_type, "ValidationError");
570/// assert_eq!(err.error_message, "Invalid input");
571/// assert!(err.stack_trace.is_none());
572///
573/// // With stack trace
574/// let err_with_trace = ErrorObject::with_stack_trace(
575///     "RuntimeError",
576///     "Something went wrong",
577///     "at main.rs:42"
578/// );
579/// assert!(err_with_trace.stack_trace.is_some());
580/// ```
581///
582/// Serialization:
583///
584/// ```
585/// use durable_execution_sdk::ErrorObject;
586///
587/// let err = ErrorObject::new("TestError", "Test message");
588/// let json = serde_json::to_string(&err).unwrap();
589/// assert!(json.contains("ErrorType"));
590/// assert!(json.contains("ErrorMessage"));
591/// ```
592#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct ErrorObject {
594    /// The error type/name
595    #[serde(rename = "ErrorType")]
596    pub error_type: String,
597    /// The error message
598    #[serde(rename = "ErrorMessage")]
599    pub error_message: String,
600    /// Optional stack trace
601    #[serde(rename = "StackTrace", skip_serializing_if = "Option::is_none")]
602    pub stack_trace: Option<String>,
603}
604
605impl ErrorObject {
606    /// Creates a new ErrorObject.
607    pub fn new(error_type: impl Into<String>, error_message: impl Into<String>) -> Self {
608        Self {
609            error_type: error_type.into(),
610            error_message: error_message.into(),
611            stack_trace: None,
612        }
613    }
614
615    /// Creates a new ErrorObject with a stack trace.
616    pub fn with_stack_trace(
617        error_type: impl Into<String>,
618        error_message: impl Into<String>,
619        stack_trace: impl Into<String>,
620    ) -> Self {
621        Self {
622            error_type: error_type.into(),
623            error_message: error_message.into(),
624            stack_trace: Some(stack_trace.into()),
625        }
626    }
627}
628
629impl From<&DurableError> for ErrorObject {
630    fn from(error: &DurableError) -> Self {
631        match error {
632            DurableError::Execution { message, .. } => ErrorObject::new("ExecutionError", message),
633            DurableError::Invocation { message, .. } => {
634                ErrorObject::new("InvocationError", message)
635            }
636            DurableError::Checkpoint { message, .. } => {
637                ErrorObject::new("CheckpointError", message)
638            }
639            DurableError::Callback { message, .. } => ErrorObject::new("CallbackError", message),
640            DurableError::NonDeterministic { message, .. } => {
641                ErrorObject::new("NonDeterministicExecutionError", message)
642            }
643            DurableError::Validation { message } => ErrorObject::new("ValidationError", message),
644            DurableError::SerDes { message } => ErrorObject::new("SerDesError", message),
645            DurableError::Suspend { .. } => {
646                ErrorObject::new("SuspendExecution", "Execution suspended")
647            }
648            DurableError::OrphanedChild { message, .. } => {
649                ErrorObject::new("OrphanedChildError", message)
650            }
651            DurableError::UserCode {
652                message,
653                error_type,
654                stack_trace,
655            } => {
656                let mut obj = ErrorObject::new(error_type, message);
657                obj.stack_trace = stack_trace.clone();
658                obj
659            }
660            DurableError::SizeLimit {
661                message,
662                actual_size,
663                max_size,
664            } => {
665                let detailed_message = match (actual_size, max_size) {
666                    (Some(actual), Some(max)) => {
667                        format!("{} (actual: {} bytes, max: {} bytes)", message, actual, max)
668                    }
669                    _ => message.clone(),
670                };
671                ErrorObject::new("SizeLimitExceededError", detailed_message)
672            }
673            DurableError::Throttling {
674                message,
675                retry_after_ms,
676            } => {
677                let detailed_message = match retry_after_ms {
678                    Some(ms) => format!("{} (retry after: {}ms)", message, ms),
679                    None => message.clone(),
680                };
681                ErrorObject::new("ThrottlingError", detailed_message)
682            }
683            DurableError::ResourceNotFound {
684                message,
685                resource_id,
686            } => {
687                let detailed_message = match resource_id {
688                    Some(id) => format!("{} (resource: {})", message, id),
689                    None => message.clone(),
690                };
691                ErrorObject::new("ResourceNotFoundError", detailed_message)
692            }
693            DurableError::Configuration { message } => {
694                ErrorObject::new("ConfigurationError", message)
695            }
696        }
697    }
698}
699
700// Implement From conversions for common error types
701
702impl From<serde_json::Error> for DurableError {
703    fn from(error: serde_json::Error) -> Self {
704        Self::SerDes {
705            message: error.to_string(),
706        }
707    }
708}
709
710impl From<std::io::Error> for DurableError {
711    fn from(error: std::io::Error) -> Self {
712        Self::Execution {
713            message: error.to_string(),
714            termination_reason: TerminationReason::UnhandledError,
715        }
716    }
717}
718
719impl From<Box<dyn std::error::Error + Send + Sync>> for DurableError {
720    fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
721        Self::UserCode {
722            message: error.to_string(),
723            error_type: "UserCodeError".to_string(),
724            stack_trace: None,
725        }
726    }
727}
728
729impl From<Box<dyn std::error::Error>> for DurableError {
730    fn from(error: Box<dyn std::error::Error>) -> Self {
731        Self::UserCode {
732            message: error.to_string(),
733            error_type: "UserCodeError".to_string(),
734            stack_trace: None,
735        }
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742    use proptest::prelude::*;
743
744    // ============================================================================
745    // Proptest Strategies
746    // ============================================================================
747
748    /// Strategy for generating non-empty strings (for error messages)
749    fn non_empty_string_strategy() -> impl Strategy<Value = String> {
750        "[a-zA-Z0-9_ ]{1,64}".prop_map(|s| s)
751    }
752
753    /// Strategy for generating optional non-empty strings
754    fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
755        prop_oneof![Just(None), non_empty_string_strategy().prop_map(Some),]
756    }
757
758    /// Strategy for generating optional usize values
759    fn optional_usize_strategy() -> impl Strategy<Value = Option<usize>> {
760        prop_oneof![Just(None), (1usize..10_000_000usize).prop_map(Some),]
761    }
762
763    /// Strategy for generating optional u64 values (for retry_after_ms)
764    fn optional_u64_strategy() -> impl Strategy<Value = Option<u64>> {
765        prop_oneof![Just(None), (1u64..100_000u64).prop_map(Some),]
766    }
767
768    /// Strategy for generating DurableError::Execution variants
769    fn execution_error_strategy() -> impl Strategy<Value = DurableError> {
770        non_empty_string_strategy().prop_map(|message| DurableError::Execution {
771            message,
772            termination_reason: TerminationReason::ExecutionError,
773        })
774    }
775
776    /// Strategy for generating DurableError::Invocation variants
777    fn invocation_error_strategy() -> impl Strategy<Value = DurableError> {
778        non_empty_string_strategy().prop_map(|message| DurableError::Invocation {
779            message,
780            termination_reason: TerminationReason::InvocationError,
781        })
782    }
783
784    /// Strategy for generating DurableError::Checkpoint variants
785    fn checkpoint_error_strategy() -> impl Strategy<Value = DurableError> {
786        (non_empty_string_strategy(), any::<bool>()).prop_map(|(message, is_retriable)| {
787            DurableError::Checkpoint {
788                message,
789                is_retriable,
790                aws_error: None,
791            }
792        })
793    }
794
795    /// Strategy for generating DurableError::Callback variants
796    fn callback_error_strategy() -> impl Strategy<Value = DurableError> {
797        (non_empty_string_strategy(), optional_string_strategy()).prop_map(
798            |(message, callback_id)| DurableError::Callback {
799                message,
800                callback_id,
801            },
802        )
803    }
804
805    /// Strategy for generating DurableError::NonDeterministic variants
806    fn non_deterministic_error_strategy() -> impl Strategy<Value = DurableError> {
807        (non_empty_string_strategy(), optional_string_strategy()).prop_map(
808            |(message, operation_id)| DurableError::NonDeterministic {
809                message,
810                operation_id,
811            },
812        )
813    }
814
815    /// Strategy for generating DurableError::Validation variants
816    fn validation_error_strategy() -> impl Strategy<Value = DurableError> {
817        non_empty_string_strategy().prop_map(|message| DurableError::Validation { message })
818    }
819
820    /// Strategy for generating DurableError::SerDes variants
821    fn serdes_error_strategy() -> impl Strategy<Value = DurableError> {
822        non_empty_string_strategy().prop_map(|message| DurableError::SerDes { message })
823    }
824
825    /// Strategy for generating DurableError::Suspend variants
826    fn suspend_error_strategy() -> impl Strategy<Value = DurableError> {
827        prop_oneof![
828            // None case - use prop_map to avoid Clone requirement
829            Just(()).prop_map(|_| DurableError::Suspend {
830                scheduled_timestamp: None
831            }),
832            (0.0f64..1e15f64).prop_map(|ts| DurableError::Suspend {
833                scheduled_timestamp: Some(ts)
834            }),
835        ]
836    }
837
838    /// Strategy for generating DurableError::OrphanedChild variants
839    fn orphaned_child_error_strategy() -> impl Strategy<Value = DurableError> {
840        (non_empty_string_strategy(), non_empty_string_strategy()).prop_map(
841            |(message, operation_id)| DurableError::OrphanedChild {
842                message,
843                operation_id,
844            },
845        )
846    }
847
848    /// Strategy for generating DurableError::UserCode variants
849    fn user_code_error_strategy() -> impl Strategy<Value = DurableError> {
850        (
851            non_empty_string_strategy(),
852            non_empty_string_strategy(),
853            optional_string_strategy(),
854        )
855            .prop_map(
856                |(message, error_type, stack_trace)| DurableError::UserCode {
857                    message,
858                    error_type,
859                    stack_trace,
860                },
861            )
862    }
863
864    /// Strategy for generating DurableError::SizeLimit variants
865    fn size_limit_error_strategy() -> impl Strategy<Value = DurableError> {
866        (
867            non_empty_string_strategy(),
868            optional_usize_strategy(),
869            optional_usize_strategy(),
870        )
871            .prop_map(|(message, actual_size, max_size)| DurableError::SizeLimit {
872                message,
873                actual_size,
874                max_size,
875            })
876    }
877
878    /// Strategy for generating DurableError::Throttling variants
879    fn throttling_error_strategy() -> impl Strategy<Value = DurableError> {
880        (non_empty_string_strategy(), optional_u64_strategy()).prop_map(
881            |(message, retry_after_ms)| DurableError::Throttling {
882                message,
883                retry_after_ms,
884            },
885        )
886    }
887
888    /// Strategy for generating DurableError::ResourceNotFound variants
889    fn resource_not_found_error_strategy() -> impl Strategy<Value = DurableError> {
890        (non_empty_string_strategy(), optional_string_strategy()).prop_map(
891            |(message, resource_id)| DurableError::ResourceNotFound {
892                message,
893                resource_id,
894            },
895        )
896    }
897
898    /// Strategy for generating any DurableError variant
899    fn durable_error_strategy() -> impl Strategy<Value = DurableError> {
900        prop_oneof![
901            execution_error_strategy(),
902            invocation_error_strategy(),
903            checkpoint_error_strategy(),
904            callback_error_strategy(),
905            non_deterministic_error_strategy(),
906            validation_error_strategy(),
907            serdes_error_strategy(),
908            suspend_error_strategy(),
909            orphaned_child_error_strategy(),
910            user_code_error_strategy(),
911            size_limit_error_strategy(),
912            throttling_error_strategy(),
913            resource_not_found_error_strategy(),
914        ]
915    }
916
917    // ============================================================================
918    // Property-Based Tests
919    // ============================================================================
920
921    proptest! {
922        /// Feature: rust-sdk-test-suite, Property 6: DurableError to ErrorObject Conversion
923        /// For any DurableError variant, converting to ErrorObject SHALL produce an ErrorObject
924        /// with non-empty error_type and error_message fields.
925        /// **Validates: Requirements 3.1, 3.2**
926        #[test]
927        fn prop_durable_error_to_error_object_produces_valid_fields(error in durable_error_strategy()) {
928            let error_object: ErrorObject = (&error).into();
929
930            // Verify error_type is non-empty
931            prop_assert!(
932                !error_object.error_type.is_empty(),
933                "ErrorObject.error_type should be non-empty for {:?}",
934                error
935            );
936
937            // Verify error_message is non-empty
938            prop_assert!(
939                !error_object.error_message.is_empty(),
940                "ErrorObject.error_message should be non-empty for {:?}",
941                error
942            );
943        }
944
945        /// Feature: rust-sdk-test-suite, Property 7: Error Type Classification Consistency (SizeLimit)
946        /// For any SizeLimit error, is_size_limit() SHALL return true and is_retriable() SHALL return false.
947        /// **Validates: Requirements 3.3**
948        #[test]
949        fn prop_size_limit_error_classification(error in size_limit_error_strategy()) {
950            prop_assert!(
951                error.is_size_limit(),
952                "SizeLimit error should return true for is_size_limit()"
953            );
954            prop_assert!(
955                !error.is_retriable(),
956                "SizeLimit error should return false for is_retriable()"
957            );
958            prop_assert!(
959                !error.is_throttling(),
960                "SizeLimit error should return false for is_throttling()"
961            );
962            prop_assert!(
963                !error.is_resource_not_found(),
964                "SizeLimit error should return false for is_resource_not_found()"
965            );
966        }
967
968        /// Feature: rust-sdk-test-suite, Property 7: Error Type Classification Consistency (Throttling)
969        /// For any Throttling error, is_throttling() SHALL return true.
970        /// **Validates: Requirements 3.4**
971        #[test]
972        fn prop_throttling_error_classification(error in throttling_error_strategy()) {
973            prop_assert!(
974                error.is_throttling(),
975                "Throttling error should return true for is_throttling()"
976            );
977            prop_assert!(
978                !error.is_size_limit(),
979                "Throttling error should return false for is_size_limit()"
980            );
981            prop_assert!(
982                !error.is_resource_not_found(),
983                "Throttling error should return false for is_resource_not_found()"
984            );
985        }
986
987        /// Feature: rust-sdk-test-suite, Property 7: Error Type Classification Consistency (ResourceNotFound)
988        /// For any ResourceNotFound error, is_resource_not_found() SHALL return true and is_retriable() SHALL return false.
989        /// **Validates: Requirements 3.5**
990        #[test]
991        fn prop_resource_not_found_error_classification(error in resource_not_found_error_strategy()) {
992            prop_assert!(
993                error.is_resource_not_found(),
994                "ResourceNotFound error should return true for is_resource_not_found()"
995            );
996            prop_assert!(
997                !error.is_retriable(),
998                "ResourceNotFound error should return false for is_retriable()"
999            );
1000            prop_assert!(
1001                !error.is_size_limit(),
1002                "ResourceNotFound error should return false for is_size_limit()"
1003            );
1004            prop_assert!(
1005                !error.is_throttling(),
1006                "ResourceNotFound error should return false for is_throttling()"
1007            );
1008        }
1009
1010        /// Feature: rust-sdk-test-suite, Property 7: Error Type Classification Consistency (Retriable Checkpoint)
1011        /// For any Checkpoint error with is_retriable=true, is_retriable() SHALL return true.
1012        /// **Validates: Requirements 3.6**
1013        #[test]
1014        fn prop_retriable_checkpoint_error_classification(message in non_empty_string_strategy()) {
1015            let error = DurableError::Checkpoint {
1016                message,
1017                is_retriable: true,
1018                aws_error: None,
1019            };
1020            prop_assert!(
1021                error.is_retriable(),
1022                "Checkpoint error with is_retriable=true should return true for is_retriable()"
1023            );
1024        }
1025
1026        /// Feature: rust-sdk-test-suite, Property 7: Error Type Classification Consistency (Non-Retriable Checkpoint)
1027        /// For any Checkpoint error with is_retriable=false, is_retriable() SHALL return false.
1028        /// **Validates: Requirements 3.6**
1029        #[test]
1030        fn prop_non_retriable_checkpoint_error_classification(message in non_empty_string_strategy()) {
1031            let error = DurableError::Checkpoint {
1032                message,
1033                is_retriable: false,
1034                aws_error: None,
1035            };
1036            prop_assert!(
1037                !error.is_retriable(),
1038                "Checkpoint error with is_retriable=false should return false for is_retriable()"
1039            );
1040        }
1041
1042        /// Feature: rust-sdk-test-suite, Property 6: ErrorObject Field Validation
1043        /// For any ErrorObject created from DurableError, the error_type field SHALL match
1044        /// the expected type name for that error variant.
1045        /// **Validates: Requirements 3.1**
1046        #[test]
1047        fn prop_error_object_type_matches_variant(error in durable_error_strategy()) {
1048            let error_object: ErrorObject = (&error).into();
1049
1050            let expected_type = match &error {
1051                DurableError::Execution { .. } => "ExecutionError",
1052                DurableError::Invocation { .. } => "InvocationError",
1053                DurableError::Checkpoint { .. } => "CheckpointError",
1054                DurableError::Callback { .. } => "CallbackError",
1055                DurableError::NonDeterministic { .. } => "NonDeterministicExecutionError",
1056                DurableError::Validation { .. } => "ValidationError",
1057                DurableError::SerDes { .. } => "SerDesError",
1058                DurableError::Suspend { .. } => "SuspendExecution",
1059                DurableError::OrphanedChild { .. } => "OrphanedChildError",
1060                DurableError::UserCode { error_type, .. } => error_type.as_str(),
1061                DurableError::SizeLimit { .. } => "SizeLimitExceededError",
1062                DurableError::Throttling { .. } => "ThrottlingError",
1063                DurableError::ResourceNotFound { .. } => "ResourceNotFoundError",
1064                DurableError::Configuration { .. } => "ConfigurationError",
1065            };
1066
1067            prop_assert_eq!(
1068                error_object.error_type,
1069                expected_type,
1070                "ErrorObject.error_type should match expected type for {:?}",
1071                error
1072            );
1073        }
1074    }
1075
1076    // ============================================================================
1077    // Unit Tests
1078    // ============================================================================
1079
1080    #[test]
1081    fn test_execution_error() {
1082        let error = DurableError::execution("test error");
1083        assert!(matches!(error, DurableError::Execution { .. }));
1084        assert!(!error.is_retriable());
1085        assert!(!error.is_suspend());
1086    }
1087
1088    #[test]
1089    fn test_checkpoint_retriable() {
1090        let error = DurableError::checkpoint_retriable("test error");
1091        assert!(error.is_retriable());
1092    }
1093
1094    #[test]
1095    fn test_checkpoint_non_retriable() {
1096        let error = DurableError::checkpoint_non_retriable("test error");
1097        assert!(!error.is_retriable());
1098    }
1099
1100    #[test]
1101    fn test_suspend() {
1102        let error = DurableError::suspend();
1103        assert!(error.is_suspend());
1104    }
1105
1106    #[test]
1107    fn test_suspend_until() {
1108        let error = DurableError::suspend_until(1234567890.0);
1109        assert!(error.is_suspend());
1110        if let DurableError::Suspend {
1111            scheduled_timestamp,
1112        } = error
1113        {
1114            assert_eq!(scheduled_timestamp, Some(1234567890.0));
1115        }
1116    }
1117
1118    #[test]
1119    fn test_error_object_from_durable_error() {
1120        let error = DurableError::validation("invalid input");
1121        let obj: ErrorObject = (&error).into();
1122        assert_eq!(obj.error_type, "ValidationError");
1123        assert_eq!(obj.error_message, "invalid input");
1124    }
1125
1126    #[test]
1127    fn test_from_serde_json_error() {
1128        let json_error = serde_json::from_str::<String>("invalid").unwrap_err();
1129        let error: DurableError = json_error.into();
1130        assert!(matches!(error, DurableError::SerDes { .. }));
1131    }
1132
1133    #[test]
1134    fn test_is_invalid_checkpoint_token_with_aws_error() {
1135        let error = DurableError::Checkpoint {
1136            message: "Checkpoint API returned 400: Invalid checkpoint token".to_string(),
1137            is_retriable: true,
1138            aws_error: Some(AwsError {
1139                code: "InvalidParameterValueException".to_string(),
1140                message: "Invalid checkpoint token: token has been consumed".to_string(),
1141                request_id: None,
1142            }),
1143        };
1144        assert!(error.is_invalid_checkpoint_token());
1145        assert!(error.is_retriable());
1146    }
1147
1148    #[test]
1149    fn test_is_invalid_checkpoint_token_without_aws_error() {
1150        let error = DurableError::Checkpoint {
1151            message: "Invalid checkpoint token: token expired".to_string(),
1152            is_retriable: true,
1153            aws_error: None,
1154        };
1155        assert!(error.is_invalid_checkpoint_token());
1156    }
1157
1158    #[test]
1159    fn test_is_not_invalid_checkpoint_token() {
1160        let error = DurableError::Checkpoint {
1161            message: "Network error".to_string(),
1162            is_retriable: true,
1163            aws_error: None,
1164        };
1165        assert!(!error.is_invalid_checkpoint_token());
1166    }
1167
1168    #[test]
1169    fn test_is_invalid_checkpoint_token_wrong_error_type() {
1170        let error = DurableError::Validation {
1171            message: "Invalid checkpoint token".to_string(),
1172        };
1173        assert!(!error.is_invalid_checkpoint_token());
1174    }
1175
1176    #[test]
1177    fn test_is_invalid_checkpoint_token_wrong_aws_error_code() {
1178        let error = DurableError::Checkpoint {
1179            message: "Some error".to_string(),
1180            is_retriable: false,
1181            aws_error: Some(AwsError {
1182                code: "ServiceException".to_string(),
1183                message: "Invalid checkpoint token".to_string(),
1184                request_id: None,
1185            }),
1186        };
1187        // Should be false because the code is not InvalidParameterValueException
1188        assert!(!error.is_invalid_checkpoint_token());
1189    }
1190
1191    #[test]
1192    fn test_size_limit_error() {
1193        let error = DurableError::size_limit("Payload too large");
1194        assert!(error.is_size_limit());
1195        assert!(!error.is_retriable());
1196        assert!(!error.is_throttling());
1197        assert!(!error.is_resource_not_found());
1198    }
1199
1200    #[test]
1201    fn test_size_limit_error_with_details() {
1202        let error =
1203            DurableError::size_limit_with_details("Payload too large", 7_000_000, 6_000_000);
1204        assert!(error.is_size_limit());
1205        if let DurableError::SizeLimit {
1206            actual_size,
1207            max_size,
1208            ..
1209        } = error
1210        {
1211            assert_eq!(actual_size, Some(7_000_000));
1212            assert_eq!(max_size, Some(6_000_000));
1213        } else {
1214            panic!("Expected SizeLimit error");
1215        }
1216    }
1217
1218    #[test]
1219    fn test_throttling_error() {
1220        let error = DurableError::throttling("Rate limit exceeded");
1221        assert!(error.is_throttling());
1222        assert!(!error.is_retriable());
1223        assert!(!error.is_size_limit());
1224        assert!(!error.is_resource_not_found());
1225        assert_eq!(error.get_retry_after_ms(), None);
1226    }
1227
1228    #[test]
1229    fn test_throttling_error_with_retry_delay() {
1230        let error = DurableError::throttling_with_retry_delay("Rate limit exceeded", 5000);
1231        assert!(error.is_throttling());
1232        assert_eq!(error.get_retry_after_ms(), Some(5000));
1233    }
1234
1235    #[test]
1236    fn test_resource_not_found_error() {
1237        let error = DurableError::resource_not_found("Execution not found");
1238        assert!(error.is_resource_not_found());
1239        assert!(!error.is_retriable());
1240        assert!(!error.is_size_limit());
1241        assert!(!error.is_throttling());
1242    }
1243
1244    #[test]
1245    fn test_resource_not_found_error_with_id() {
1246        let error = DurableError::resource_not_found_with_id(
1247            "Execution not found",
1248            "arn:aws:lambda:us-east-1:123456789012:function:test",
1249        );
1250        assert!(error.is_resource_not_found());
1251        if let DurableError::ResourceNotFound { resource_id, .. } = error {
1252            assert_eq!(
1253                resource_id,
1254                Some("arn:aws:lambda:us-east-1:123456789012:function:test".to_string())
1255            );
1256        } else {
1257            panic!("Expected ResourceNotFound error");
1258        }
1259    }
1260
1261    #[test]
1262    fn test_error_object_from_size_limit_error() {
1263        let error =
1264            DurableError::size_limit_with_details("Payload too large", 7_000_000, 6_000_000);
1265        let obj: ErrorObject = (&error).into();
1266        assert_eq!(obj.error_type, "SizeLimitExceededError");
1267        assert!(obj.error_message.contains("Payload too large"));
1268        assert!(obj.error_message.contains("7000000"));
1269        assert!(obj.error_message.contains("6000000"));
1270    }
1271
1272    #[test]
1273    fn test_error_object_from_throttling_error() {
1274        let error = DurableError::throttling_with_retry_delay("Rate limit exceeded", 5000);
1275        let obj: ErrorObject = (&error).into();
1276        assert_eq!(obj.error_type, "ThrottlingError");
1277        assert!(obj.error_message.contains("Rate limit exceeded"));
1278        assert!(obj.error_message.contains("5000ms"));
1279    }
1280
1281    #[test]
1282    fn test_error_object_from_resource_not_found_error() {
1283        let error = DurableError::resource_not_found_with_id("Execution not found", "test-arn");
1284        let obj: ErrorObject = (&error).into();
1285        assert_eq!(obj.error_type, "ResourceNotFoundError");
1286        assert!(obj.error_message.contains("Execution not found"));
1287        assert!(obj.error_message.contains("test-arn"));
1288    }
1289
1290    #[test]
1291    fn test_get_retry_after_ms_non_throttling() {
1292        let error = DurableError::validation("test");
1293        assert_eq!(error.get_retry_after_ms(), None);
1294    }
1295
1296    // Size verification test for enum discriminant optimization
1297    // Requirements: 6.7 - Verify TerminationReason is 1 byte after optimization
1298
1299    #[test]
1300    fn test_termination_reason_size_is_one_byte() {
1301        assert_eq!(
1302            std::mem::size_of::<TerminationReason>(),
1303            1,
1304            "TerminationReason should be 1 byte with #[repr(u8)]"
1305        );
1306    }
1307
1308    // Requirements: 11.1 - Verify existing discriminant values (0-8) are unchanged
1309    #[test]
1310    fn test_termination_reason_discriminant_values() {
1311        // Existing variants — discriminants MUST NOT change for backward compatibility
1312        assert_eq!(TerminationReason::UnhandledError as u8, 0);
1313        assert_eq!(TerminationReason::InvocationError as u8, 1);
1314        assert_eq!(TerminationReason::ExecutionError as u8, 2);
1315        assert_eq!(TerminationReason::CheckpointFailed as u8, 3);
1316        assert_eq!(TerminationReason::NonDeterministicExecution as u8, 4);
1317        assert_eq!(TerminationReason::StepInterrupted as u8, 5);
1318        assert_eq!(TerminationReason::CallbackError as u8, 6);
1319        assert_eq!(TerminationReason::SerializationError as u8, 7);
1320        assert_eq!(TerminationReason::SizeLimitExceeded as u8, 8);
1321
1322        // New variants
1323        assert_eq!(TerminationReason::OperationTerminated as u8, 9);
1324        assert_eq!(TerminationReason::RetryScheduled as u8, 10);
1325        assert_eq!(TerminationReason::WaitScheduled as u8, 11);
1326        assert_eq!(TerminationReason::CallbackPending as u8, 12);
1327        assert_eq!(TerminationReason::ContextValidationError as u8, 13);
1328        assert_eq!(TerminationReason::LambdaTimeoutApproaching as u8, 14);
1329    }
1330
1331    // Serde compatibility tests for enum discriminant optimization
1332    // Requirements: 6.6 - Verify JSON serialization uses string representations
1333
1334    #[test]
1335    fn test_termination_reason_serde_uses_string_representation() {
1336        // Verify serialization produces string values, not numeric discriminants
1337        let reason = TerminationReason::UnhandledError;
1338        let json = serde_json::to_string(&reason).unwrap();
1339        assert_eq!(json, "\"UnhandledError\"");
1340
1341        let reason = TerminationReason::InvocationError;
1342        let json = serde_json::to_string(&reason).unwrap();
1343        assert_eq!(json, "\"InvocationError\"");
1344
1345        let reason = TerminationReason::ExecutionError;
1346        let json = serde_json::to_string(&reason).unwrap();
1347        assert_eq!(json, "\"ExecutionError\"");
1348
1349        let reason = TerminationReason::CheckpointFailed;
1350        let json = serde_json::to_string(&reason).unwrap();
1351        assert_eq!(json, "\"CheckpointFailed\"");
1352
1353        let reason = TerminationReason::NonDeterministicExecution;
1354        let json = serde_json::to_string(&reason).unwrap();
1355        assert_eq!(json, "\"NonDeterministicExecution\"");
1356
1357        let reason = TerminationReason::StepInterrupted;
1358        let json = serde_json::to_string(&reason).unwrap();
1359        assert_eq!(json, "\"StepInterrupted\"");
1360
1361        let reason = TerminationReason::CallbackError;
1362        let json = serde_json::to_string(&reason).unwrap();
1363        assert_eq!(json, "\"CallbackError\"");
1364
1365        let reason = TerminationReason::SerializationError;
1366        let json = serde_json::to_string(&reason).unwrap();
1367        assert_eq!(json, "\"SerializationError\"");
1368
1369        let reason = TerminationReason::SizeLimitExceeded;
1370        let json = serde_json::to_string(&reason).unwrap();
1371        assert_eq!(json, "\"SizeLimitExceeded\"");
1372
1373        let reason = TerminationReason::OperationTerminated;
1374        let json = serde_json::to_string(&reason).unwrap();
1375        assert_eq!(json, "\"OperationTerminated\"");
1376
1377        let reason = TerminationReason::RetryScheduled;
1378        let json = serde_json::to_string(&reason).unwrap();
1379        assert_eq!(json, "\"RetryScheduled\"");
1380
1381        let reason = TerminationReason::WaitScheduled;
1382        let json = serde_json::to_string(&reason).unwrap();
1383        assert_eq!(json, "\"WaitScheduled\"");
1384
1385        let reason = TerminationReason::CallbackPending;
1386        let json = serde_json::to_string(&reason).unwrap();
1387        assert_eq!(json, "\"CallbackPending\"");
1388
1389        let reason = TerminationReason::ContextValidationError;
1390        let json = serde_json::to_string(&reason).unwrap();
1391        assert_eq!(json, "\"ContextValidationError\"");
1392
1393        let reason = TerminationReason::LambdaTimeoutApproaching;
1394        let json = serde_json::to_string(&reason).unwrap();
1395        assert_eq!(json, "\"LambdaTimeoutApproaching\"");
1396    }
1397
1398    #[test]
1399    fn test_termination_reason_serde_round_trip() {
1400        let reasons = [
1401            TerminationReason::UnhandledError,
1402            TerminationReason::InvocationError,
1403            TerminationReason::ExecutionError,
1404            TerminationReason::CheckpointFailed,
1405            TerminationReason::NonDeterministicExecution,
1406            TerminationReason::StepInterrupted,
1407            TerminationReason::CallbackError,
1408            TerminationReason::SerializationError,
1409            TerminationReason::SizeLimitExceeded,
1410            TerminationReason::OperationTerminated,
1411            TerminationReason::RetryScheduled,
1412            TerminationReason::WaitScheduled,
1413            TerminationReason::CallbackPending,
1414            TerminationReason::ContextValidationError,
1415            TerminationReason::LambdaTimeoutApproaching,
1416        ];
1417
1418        for reason in reasons {
1419            let json = serde_json::to_string(&reason).unwrap();
1420            let deserialized: TerminationReason = serde_json::from_str(&json).unwrap();
1421            assert_eq!(reason, deserialized, "Round-trip failed for {:?}", reason);
1422        }
1423    }
1424}