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