Skip to main content

durable_execution_sdk/
operation.rs

1//! Operation types for the AWS Durable Execution SDK.
2//!
3//! This module defines the core operation types used for checkpointing
4//! and replay in durable execution workflows.
5
6use chrono::{DateTime, FixedOffset, NaiveDateTime, TimeZone, Utc};
7use serde::{Deserialize, Deserializer, Serialize};
8
9use crate::error::ErrorObject;
10
11/// Custom deserializer for timestamp fields that can be either i64 or ISO 8601 string.
12fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
13where
14    D: Deserializer<'de>,
15{
16    use serde::de::{self, Visitor};
17
18    struct TimestampVisitor;
19
20    impl<'de> Visitor<'de> for TimestampVisitor {
21        type Value = Option<i64>;
22
23        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
24            formatter.write_str("an integer timestamp or ISO 8601 string")
25        }
26
27        fn visit_none<E>(self) -> Result<Self::Value, E>
28        where
29            E: de::Error,
30        {
31            Ok(None)
32        }
33
34        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
35        where
36            D: Deserializer<'de>,
37        {
38            deserializer.deserialize_any(TimestampValueVisitor)
39        }
40
41        fn visit_unit<E>(self) -> Result<Self::Value, E>
42        where
43            E: de::Error,
44        {
45            Ok(None)
46        }
47    }
48
49    struct TimestampValueVisitor;
50
51    impl<'de> Visitor<'de> for TimestampValueVisitor {
52        type Value = Option<i64>;
53
54        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
55            formatter
56                .write_str("an integer timestamp, floating point timestamp, or ISO 8601 string")
57        }
58
59        fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
60        where
61            E: de::Error,
62        {
63            Ok(Some(value))
64        }
65
66        fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
67        where
68            E: de::Error,
69        {
70            Ok(Some(value as i64))
71        }
72
73        fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
74        where
75            E: de::Error,
76        {
77            // Floating point timestamps are typically in seconds with fractional milliseconds
78            // Convert to milliseconds by multiplying by 1000
79            // The value 1768279889.004 represents seconds since epoch
80            // Use round() before casting to avoid precision loss for large timestamps
81            Ok(Some((value * 1000.0).round() as i64))
82        }
83
84        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
85        where
86            E: de::Error,
87        {
88            // Try to parse as ISO 8601 datetime string using chrono
89            parse_iso8601_to_millis(value).map(Some).map_err(|e| {
90                de::Error::custom(format!("invalid timestamp string '{}': {}", value, e))
91            })
92        }
93    }
94
95    deserializer.deserialize_option(TimestampVisitor)
96}
97
98/// Parse an ISO 8601 datetime string to milliseconds since epoch using chrono.
99fn parse_iso8601_to_millis(s: &str) -> Result<i64, String> {
100    // Normalize space separator to 'T' for ISO 8601 compliance
101    let normalized = s.replace(' ', "T");
102
103    // Try parsing as DateTime with timezone (e.g., "2026-01-13T04:10:18.841055+00:00")
104    if let Ok(dt) = DateTime::parse_from_rfc3339(&normalized) {
105        return Ok(dt.timestamp_millis());
106    }
107
108    // Try parsing with various timezone formats
109    if let Ok(dt) = DateTime::<FixedOffset>::parse_from_str(&normalized, "%Y-%m-%dT%H:%M:%S%.f%:z")
110    {
111        return Ok(dt.timestamp_millis());
112    }
113
114    if let Ok(dt) = DateTime::<FixedOffset>::parse_from_str(&normalized, "%Y-%m-%dT%H:%M:%S%:z") {
115        return Ok(dt.timestamp_millis());
116    }
117
118    // Try parsing as naive datetime (no timezone) and assume UTC
119    if let Ok(naive) = NaiveDateTime::parse_from_str(&normalized, "%Y-%m-%dT%H:%M:%S%.f") {
120        return Ok(Utc.from_utc_datetime(&naive).timestamp_millis());
121    }
122
123    if let Ok(naive) = NaiveDateTime::parse_from_str(&normalized, "%Y-%m-%dT%H:%M:%S") {
124        return Ok(Utc.from_utc_datetime(&naive).timestamp_millis());
125    }
126
127    Err("unable to parse as ISO 8601 datetime".to_string())
128}
129
130/// Represents a checkpointed operation in a durable execution.
131///
132/// Operations are the fundamental unit of state in durable executions.
133/// Each operation has a unique ID and tracks its type, status, and result.
134///
135/// # Examples
136///
137/// Creating a new operation:
138///
139/// ```
140/// use durable_execution_sdk::operation::{Operation, OperationType, OperationStatus};
141///
142/// let op = Operation::new("step-001", OperationType::Step);
143/// assert_eq!(op.operation_id, "step-001");
144/// assert_eq!(op.operation_type, OperationType::Step);
145/// assert_eq!(op.status, OperationStatus::Started);
146/// ```
147///
148/// Serializing and deserializing operations:
149///
150/// ```
151/// use durable_execution_sdk::operation::{Operation, OperationType, OperationStatus};
152///
153/// let mut op = Operation::new("wait-001", OperationType::Wait);
154/// op.status = OperationStatus::Succeeded;
155/// op.result = Some("done".to_string());
156///
157/// let json = serde_json::to_string(&op).unwrap();
158/// let restored: Operation = serde_json::from_str(&json).unwrap();
159///
160/// assert_eq!(restored.operation_id, "wait-001");
161/// assert_eq!(restored.status, OperationStatus::Succeeded);
162/// ```
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct Operation {
165    /// Unique identifier for this operation
166    #[serde(rename = "Id", alias = "OperationId")]
167    pub operation_id: String,
168
169    /// The type of operation (Step, Wait, Callback, etc.)
170    #[serde(rename = "Type", alias = "OperationType")]
171    pub operation_type: OperationType,
172
173    /// Current status of the operation
174    #[serde(rename = "Status")]
175    pub status: OperationStatus,
176
177    /// Serialized result if the operation succeeded (legacy field, prefer type-specific details)
178    #[serde(rename = "Result", skip_serializing_if = "Option::is_none")]
179    pub result: Option<String>,
180
181    /// Error details if the operation failed
182    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
183    pub error: Option<ErrorObject>,
184
185    /// Parent operation ID for nested operations
186    #[serde(rename = "ParentId", skip_serializing_if = "Option::is_none")]
187    pub parent_id: Option<String>,
188
189    /// Optional human-readable name for the operation
190    #[serde(rename = "Name", skip_serializing_if = "Option::is_none")]
191    pub name: Option<String>,
192
193    /// SDK-level categorization of the operation (e.g., "map", "parallel", "wait_for_condition")
194    /// Requirements: 23.3, 23.4
195    #[serde(rename = "SubType", skip_serializing_if = "Option::is_none")]
196    pub sub_type: Option<String>,
197
198    /// Start timestamp of the operation (milliseconds since epoch)
199    #[serde(
200        rename = "StartTimestamp",
201        skip_serializing_if = "Option::is_none",
202        default,
203        deserialize_with = "deserialize_timestamp"
204    )]
205    pub start_timestamp: Option<i64>,
206
207    /// End timestamp of the operation (milliseconds since epoch)
208    #[serde(
209        rename = "EndTimestamp",
210        skip_serializing_if = "Option::is_none",
211        default,
212        deserialize_with = "deserialize_timestamp"
213    )]
214    pub end_timestamp: Option<i64>,
215
216    /// Execution details for EXECUTION type operations
217    #[serde(rename = "ExecutionDetails", skip_serializing_if = "Option::is_none")]
218    pub execution_details: Option<ExecutionDetails>,
219
220    /// Step details for STEP type operations
221    #[serde(rename = "StepDetails", skip_serializing_if = "Option::is_none")]
222    pub step_details: Option<StepDetails>,
223
224    /// Wait details for WAIT type operations
225    #[serde(rename = "WaitDetails", skip_serializing_if = "Option::is_none")]
226    pub wait_details: Option<WaitDetails>,
227
228    /// Callback details for CALLBACK type operations
229    #[serde(rename = "CallbackDetails", skip_serializing_if = "Option::is_none")]
230    pub callback_details: Option<CallbackDetails>,
231
232    /// Chained invoke details for CHAINED_INVOKE type operations
233    #[serde(
234        rename = "ChainedInvokeDetails",
235        skip_serializing_if = "Option::is_none"
236    )]
237    pub chained_invoke_details: Option<ChainedInvokeDetails>,
238
239    /// Context details for CONTEXT type operations
240    #[serde(rename = "ContextDetails", skip_serializing_if = "Option::is_none")]
241    pub context_details: Option<ContextDetails>,
242}
243
244/// Details specific to EXECUTION type operations
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct ExecutionDetails {
247    /// The input payload for the execution
248    #[serde(rename = "InputPayload", skip_serializing_if = "Option::is_none")]
249    pub input_payload: Option<String>,
250}
251
252/// Details specific to STEP type operations
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct StepDetails {
255    /// The result payload if the step succeeded
256    #[serde(rename = "Result", skip_serializing_if = "Option::is_none")]
257    pub result: Option<String>,
258    /// The current retry attempt (0-indexed)
259    #[serde(rename = "Attempt", skip_serializing_if = "Option::is_none")]
260    pub attempt: Option<u32>,
261    /// Timestamp for the next retry attempt
262    #[serde(
263        rename = "NextAttemptTimestamp",
264        skip_serializing_if = "Option::is_none",
265        default,
266        deserialize_with = "deserialize_timestamp"
267    )]
268    pub next_attempt_timestamp: Option<i64>,
269    /// Error details if the step failed
270    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
271    pub error: Option<ErrorObject>,
272    /// Payload for RETRY action - stores state for wait-for-condition pattern
273    /// Requirements: 4.9
274    #[serde(rename = "Payload", skip_serializing_if = "Option::is_none")]
275    pub payload: Option<String>,
276}
277
278/// Details specific to WAIT type operations
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct WaitDetails {
281    /// Timestamp when the wait is scheduled to end
282    #[serde(
283        rename = "ScheduledEndTimestamp",
284        skip_serializing_if = "Option::is_none",
285        default,
286        deserialize_with = "deserialize_timestamp"
287    )]
288    pub scheduled_end_timestamp: Option<i64>,
289}
290
291/// Details specific to CALLBACK type operations
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct CallbackDetails {
294    /// The callback ID for external systems to use
295    #[serde(rename = "CallbackId", skip_serializing_if = "Option::is_none")]
296    pub callback_id: Option<String>,
297    /// The result payload if the callback succeeded
298    #[serde(rename = "Result", skip_serializing_if = "Option::is_none")]
299    pub result: Option<String>,
300    /// Error details if the callback failed
301    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
302    pub error: Option<ErrorObject>,
303}
304
305/// Details specific to CHAINED_INVOKE type operations
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ChainedInvokeDetails {
308    /// The result payload if the invocation succeeded
309    #[serde(rename = "Result", skip_serializing_if = "Option::is_none")]
310    pub result: Option<String>,
311    /// Error details if the invocation failed
312    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
313    pub error: Option<ErrorObject>,
314}
315
316/// Details specific to CONTEXT type operations
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ContextDetails {
319    /// The result payload if the context succeeded
320    #[serde(rename = "Result", skip_serializing_if = "Option::is_none")]
321    pub result: Option<String>,
322    /// Whether to replay children when loading state
323    #[serde(rename = "ReplayChildren", skip_serializing_if = "Option::is_none")]
324    pub replay_children: Option<bool>,
325    /// Error details if the context failed
326    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
327    pub error: Option<ErrorObject>,
328}
329
330impl Operation {
331    /// Creates a new Operation with the given ID and type.
332    pub fn new(operation_id: impl Into<String>, operation_type: OperationType) -> Self {
333        Self {
334            operation_id: operation_id.into(),
335            operation_type,
336            status: OperationStatus::Started,
337            result: None,
338            error: None,
339            parent_id: None,
340            name: None,
341            sub_type: None,
342            start_timestamp: None,
343            end_timestamp: None,
344            execution_details: None,
345            step_details: None,
346            wait_details: None,
347            callback_details: None,
348            chained_invoke_details: None,
349            context_details: None,
350        }
351    }
352
353    /// Sets the parent ID for this operation.
354    pub fn with_parent_id(mut self, parent_id: impl Into<String>) -> Self {
355        self.parent_id = Some(parent_id.into());
356        self
357    }
358
359    /// Sets the name for this operation.
360    pub fn with_name(mut self, name: impl Into<String>) -> Self {
361        self.name = Some(name.into());
362        self
363    }
364
365    /// Sets the sub-type for this operation.
366    /// Requirements: 23.3, 23.4
367    pub fn with_sub_type(mut self, sub_type: impl Into<String>) -> Self {
368        self.sub_type = Some(sub_type.into());
369        self
370    }
371
372    /// Returns true if the operation has completed (succeeded or failed).
373    pub fn is_completed(&self) -> bool {
374        matches!(
375            self.status,
376            OperationStatus::Succeeded
377                | OperationStatus::Failed
378                | OperationStatus::Cancelled
379                | OperationStatus::TimedOut
380                | OperationStatus::Stopped
381        )
382    }
383
384    /// Returns true if the operation succeeded.
385    pub fn is_succeeded(&self) -> bool {
386        matches!(self.status, OperationStatus::Succeeded)
387    }
388
389    /// Returns true if the operation failed.
390    pub fn is_failed(&self) -> bool {
391        matches!(
392            self.status,
393            OperationStatus::Failed | OperationStatus::Cancelled | OperationStatus::TimedOut
394        )
395    }
396
397    /// Gets the result from the appropriate details field based on operation type.
398    pub fn get_result(&self) -> Option<&str> {
399        // First check type-specific details
400        match self.operation_type {
401            OperationType::Step => {
402                if let Some(ref details) = self.step_details {
403                    if details.result.is_some() {
404                        return details.result.as_deref();
405                    }
406                }
407            }
408            OperationType::Callback => {
409                if let Some(ref details) = self.callback_details {
410                    if details.result.is_some() {
411                        return details.result.as_deref();
412                    }
413                }
414            }
415            OperationType::Invoke => {
416                if let Some(ref details) = self.chained_invoke_details {
417                    if details.result.is_some() {
418                        return details.result.as_deref();
419                    }
420                }
421            }
422            OperationType::Context => {
423                if let Some(ref details) = self.context_details {
424                    if details.result.is_some() {
425                        return details.result.as_deref();
426                    }
427                }
428            }
429            _ => {}
430        }
431        // Fall back to legacy result field
432        self.result.as_deref()
433    }
434
435    /// Gets the retry payload from StepDetails for STEP operations.
436    ///
437    /// This is used for the wait-for-condition pattern where state is passed
438    /// between retry attempts via the Payload field.
439    ///
440    /// # Returns
441    ///
442    /// The payload string if this is a STEP operation with a payload, None otherwise.
443    ///
444    /// # Requirements
445    ///
446    /// - 4.9: THE Step_Operation SHALL support RETRY action with Payload for wait-for-condition pattern
447    pub fn get_retry_payload(&self) -> Option<&str> {
448        if self.operation_type == OperationType::Step {
449            if let Some(ref details) = self.step_details {
450                return details.payload.as_deref();
451            }
452        }
453        None
454    }
455
456    /// Gets the current attempt number from StepDetails for STEP operations.
457    ///
458    /// # Returns
459    ///
460    /// The attempt number (0-indexed) if this is a STEP operation with attempt tracking, None otherwise.
461    ///
462    /// # Requirements
463    ///
464    /// - 4.8: THE Step_Operation SHALL track attempt numbers in StepDetails.Attempt
465    pub fn get_attempt(&self) -> Option<u32> {
466        if self.operation_type == OperationType::Step {
467            if let Some(ref details) = self.step_details {
468                return details.attempt;
469            }
470        }
471        None
472    }
473}
474
475/// The type of operation in a durable execution.
476///
477/// This enum uses `#[repr(u8)]` for compact memory representation (1 byte).
478/// Explicit discriminant values ensure stability across versions.
479///
480/// # Examples
481///
482/// ```
483/// use durable_execution_sdk::operation::OperationType;
484///
485/// let step = OperationType::Step;
486/// let wait = OperationType::Wait;
487///
488/// // Serialization uses uppercase names
489/// let json = serde_json::to_string(&step).unwrap();
490/// assert_eq!(json, "\"STEP\"");
491///
492/// // Display uses title case
493/// assert_eq!(format!("{}", step), "Step");
494/// assert_eq!(format!("{}", wait), "Wait");
495/// ```
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
497#[repr(u8)]
498pub enum OperationType {
499    /// The root execution operation
500    #[serde(rename = "EXECUTION")]
501    Execution = 0,
502    /// A step operation (unit of work)
503    #[serde(rename = "STEP")]
504    Step = 1,
505    /// A wait/sleep operation
506    #[serde(rename = "WAIT")]
507    Wait = 2,
508    /// A callback operation waiting for external signal
509    #[serde(rename = "CALLBACK")]
510    Callback = 3,
511    /// An invoke operation calling another Lambda function
512    #[serde(rename = "INVOKE")]
513    Invoke = 4,
514    /// A context operation for nested child contexts
515    #[serde(rename = "CONTEXT")]
516    Context = 5,
517}
518
519impl std::fmt::Display for OperationType {
520    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521        match self {
522            Self::Execution => write!(f, "Execution"),
523            Self::Step => write!(f, "Step"),
524            Self::Wait => write!(f, "Wait"),
525            Self::Callback => write!(f, "Callback"),
526            Self::Invoke => write!(f, "Invoke"),
527            Self::Context => write!(f, "Context"),
528        }
529    }
530}
531
532/// The status of an operation in a durable execution.
533///
534/// This enum uses `#[repr(u8)]` for compact memory representation (1 byte).
535/// Explicit discriminant values ensure stability across versions.
536///
537/// # Examples
538///
539/// ```
540/// use durable_execution_sdk::operation::OperationStatus;
541///
542/// let succeeded = OperationStatus::Succeeded;
543/// let pending = OperationStatus::Pending;
544///
545/// // Check terminal status
546/// assert!(succeeded.is_terminal());
547/// assert!(!pending.is_terminal());
548///
549/// // Check success/failure
550/// assert!(succeeded.is_success());
551/// assert!(!succeeded.is_failure());
552///
553/// // Serialization uses uppercase names
554/// let json = serde_json::to_string(&succeeded).unwrap();
555/// assert_eq!(json, "\"SUCCEEDED\"");
556/// ```
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
558#[repr(u8)]
559pub enum OperationStatus {
560    /// Operation has started but not completed
561    #[serde(rename = "STARTED")]
562    Started = 0,
563    /// Operation is pending (e.g., step waiting for retry)
564    /// Requirements: 3.7, 4.7
565    #[serde(rename = "PENDING")]
566    Pending = 1,
567    /// Operation is ready to resume execution (e.g., after retry delay)
568    /// Requirements: 3.7, 4.7
569    #[serde(rename = "READY")]
570    Ready = 2,
571    /// Operation completed successfully
572    #[serde(rename = "SUCCEEDED")]
573    Succeeded = 3,
574    /// Operation failed with an error
575    #[serde(rename = "FAILED")]
576    Failed = 4,
577    /// Operation was cancelled
578    #[serde(rename = "CANCELLED")]
579    Cancelled = 5,
580    /// Operation timed out
581    #[serde(rename = "TIMED_OUT")]
582    TimedOut = 6,
583    /// Operation was stopped externally
584    #[serde(rename = "STOPPED")]
585    Stopped = 7,
586}
587
588impl OperationStatus {
589    /// Returns true if this status represents a terminal state.
590    pub fn is_terminal(&self) -> bool {
591        !matches!(self, Self::Started | Self::Pending | Self::Ready)
592    }
593
594    /// Returns true if this status represents a successful completion.
595    pub fn is_success(&self) -> bool {
596        matches!(self, Self::Succeeded)
597    }
598
599    /// Returns true if this status represents a failure.
600    pub fn is_failure(&self) -> bool {
601        matches!(
602            self,
603            Self::Failed | Self::Cancelled | Self::TimedOut | Self::Stopped
604        )
605    }
606
607    /// Returns true if this status indicates the operation is pending (waiting for retry).
608    /// Requirements: 3.7, 4.7
609    pub fn is_pending(&self) -> bool {
610        matches!(self, Self::Pending)
611    }
612
613    /// Returns true if this status indicates the operation is ready to resume.
614    /// Requirements: 3.7, 4.7
615    pub fn is_ready(&self) -> bool {
616        matches!(self, Self::Ready)
617    }
618
619    /// Returns true if this status indicates the operation can be resumed.
620    /// This includes both PENDING and READY statuses.
621    /// Requirements: 3.7
622    pub fn is_resumable(&self) -> bool {
623        matches!(self, Self::Started | Self::Pending | Self::Ready)
624    }
625}
626
627impl std::fmt::Display for OperationStatus {
628    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
629        match self {
630            Self::Started => write!(f, "Started"),
631            Self::Pending => write!(f, "Pending"),
632            Self::Ready => write!(f, "Ready"),
633            Self::Succeeded => write!(f, "Succeeded"),
634            Self::Failed => write!(f, "Failed"),
635            Self::Cancelled => write!(f, "Cancelled"),
636            Self::TimedOut => write!(f, "TimedOut"),
637            Self::Stopped => write!(f, "Stopped"),
638        }
639    }
640}
641
642/// Action to perform on an operation during checkpoint.
643///
644/// This enum uses `#[repr(u8)]` for compact memory representation (1 byte).
645/// Explicit discriminant values ensure stability across versions.
646#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
647#[repr(u8)]
648pub enum OperationAction {
649    /// Start a new operation
650    #[serde(rename = "START")]
651    Start = 0,
652    /// Mark operation as succeeded
653    #[serde(rename = "SUCCEED")]
654    Succeed = 1,
655    /// Mark operation as failed
656    #[serde(rename = "FAIL")]
657    Fail = 2,
658    /// Cancel an operation (e.g., cancel a wait)
659    /// Requirements: 5.5
660    #[serde(rename = "CANCEL")]
661    Cancel = 3,
662    /// Retry an operation with optional payload (state) for wait-for-condition pattern
663    /// Requirements: 4.7, 4.8, 4.9
664    #[serde(rename = "RETRY")]
665    Retry = 4,
666}
667
668impl std::fmt::Display for OperationAction {
669    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
670        match self {
671            Self::Start => write!(f, "Start"),
672            Self::Succeed => write!(f, "Succeed"),
673            Self::Fail => write!(f, "Fail"),
674            Self::Cancel => write!(f, "Cancel"),
675            Self::Retry => write!(f, "Retry"),
676        }
677    }
678}
679
680/// Options for WAIT operations
681#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct WaitOptions {
683    /// Number of seconds to wait
684    #[serde(rename = "WaitSeconds")]
685    pub wait_seconds: u64,
686}
687
688/// Options for STEP operations
689#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct StepOptions {
691    /// Delay in seconds before the next retry attempt
692    #[serde(
693        rename = "NextAttemptDelaySeconds",
694        skip_serializing_if = "Option::is_none"
695    )]
696    pub next_attempt_delay_seconds: Option<u64>,
697}
698
699/// Options for CALLBACK operations
700#[derive(Debug, Clone, Serialize, Deserialize)]
701pub struct CallbackOptions {
702    /// Timeout in seconds for the callback
703    #[serde(rename = "TimeoutSeconds", skip_serializing_if = "Option::is_none")]
704    pub timeout_seconds: Option<u64>,
705    /// Heartbeat timeout in seconds
706    #[serde(
707        rename = "HeartbeatTimeoutSeconds",
708        skip_serializing_if = "Option::is_none"
709    )]
710    pub heartbeat_timeout_seconds: Option<u64>,
711}
712
713/// Options for CHAINED_INVOKE operations
714#[derive(Debug, Clone, Serialize, Deserialize)]
715pub struct ChainedInvokeOptions {
716    /// The function name or ARN to invoke
717    #[serde(rename = "FunctionName")]
718    pub function_name: String,
719    /// Optional tenant ID for multi-tenant scenarios
720    #[serde(rename = "TenantId", skip_serializing_if = "Option::is_none")]
721    pub tenant_id: Option<String>,
722}
723
724/// Options for CONTEXT operations
725#[derive(Debug, Clone, Serialize, Deserialize)]
726pub struct ContextOptions {
727    /// Whether to replay children when the context is loaded
728    #[serde(rename = "ReplayChildren", skip_serializing_if = "Option::is_none")]
729    pub replay_children: Option<bool>,
730}
731
732/// Represents an update to be checkpointed for an operation.
733///
734/// This struct is used to send checkpoint requests to the Lambda service.
735/// Field names match the CheckpointDurableExecution API format.
736#[derive(Debug, Clone, Serialize, Deserialize)]
737pub struct OperationUpdate {
738    /// Unique identifier for this operation
739    #[serde(rename = "Id")]
740    pub operation_id: String,
741
742    /// The action to perform (Start, Succeed, Fail)
743    #[serde(rename = "Action")]
744    pub action: OperationAction,
745
746    /// The type of operation
747    #[serde(rename = "Type")]
748    pub operation_type: OperationType,
749
750    /// Serialized result if succeeding (called "Payload" in the API)
751    #[serde(rename = "Payload", skip_serializing_if = "Option::is_none")]
752    pub result: Option<String>,
753
754    /// Error details if failing
755    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
756    pub error: Option<ErrorObject>,
757
758    /// Parent operation ID for nested operations
759    #[serde(rename = "ParentId", skip_serializing_if = "Option::is_none")]
760    pub parent_id: Option<String>,
761
762    /// Optional human-readable name for the operation
763    #[serde(rename = "Name", skip_serializing_if = "Option::is_none")]
764    pub name: Option<String>,
765
766    /// SDK-level categorization of the operation (e.g., "map", "parallel", "wait_for_condition")
767    /// Requirements: 23.3, 23.4
768    #[serde(rename = "SubType", skip_serializing_if = "Option::is_none")]
769    pub sub_type: Option<String>,
770
771    /// Options for WAIT operations
772    #[serde(rename = "WaitOptions", skip_serializing_if = "Option::is_none")]
773    pub wait_options: Option<WaitOptions>,
774
775    /// Options for STEP operations
776    #[serde(rename = "StepOptions", skip_serializing_if = "Option::is_none")]
777    pub step_options: Option<StepOptions>,
778
779    /// Options for CALLBACK operations
780    #[serde(rename = "CallbackOptions", skip_serializing_if = "Option::is_none")]
781    pub callback_options: Option<CallbackOptions>,
782
783    /// Options for CHAINED_INVOKE operations
784    #[serde(
785        rename = "ChainedInvokeOptions",
786        skip_serializing_if = "Option::is_none"
787    )]
788    pub chained_invoke_options: Option<ChainedInvokeOptions>,
789
790    /// Options for CONTEXT operations
791    #[serde(rename = "ContextOptions", skip_serializing_if = "Option::is_none")]
792    pub context_options: Option<ContextOptions>,
793}
794
795impl OperationUpdate {
796    /// Creates a new OperationUpdate to start an operation.
797    pub fn start(operation_id: impl Into<String>, operation_type: OperationType) -> Self {
798        Self {
799            operation_id: operation_id.into(),
800            action: OperationAction::Start,
801            operation_type,
802            result: None,
803            error: None,
804            parent_id: None,
805            name: None,
806            sub_type: None,
807            wait_options: None,
808            step_options: None,
809            callback_options: None,
810            chained_invoke_options: None,
811            context_options: None,
812        }
813    }
814
815    /// Creates a new OperationUpdate to start a WAIT operation with the required WaitOptions.
816    pub fn start_wait(operation_id: impl Into<String>, wait_seconds: u64) -> Self {
817        Self {
818            operation_id: operation_id.into(),
819            action: OperationAction::Start,
820            operation_type: OperationType::Wait,
821            result: None,
822            error: None,
823            parent_id: None,
824            name: None,
825            sub_type: None,
826            wait_options: Some(WaitOptions { wait_seconds }),
827            step_options: None,
828            callback_options: None,
829            chained_invoke_options: None,
830            context_options: None,
831        }
832    }
833
834    /// Creates a new OperationUpdate to mark an operation as succeeded.
835    pub fn succeed(
836        operation_id: impl Into<String>,
837        operation_type: OperationType,
838        result: Option<String>,
839    ) -> Self {
840        Self {
841            operation_id: operation_id.into(),
842            action: OperationAction::Succeed,
843            operation_type,
844            result,
845            error: None,
846            parent_id: None,
847            name: None,
848            sub_type: None,
849            wait_options: None,
850            step_options: None,
851            callback_options: None,
852            chained_invoke_options: None,
853            context_options: None,
854        }
855    }
856
857    /// Creates a new OperationUpdate to mark an operation as failed.
858    pub fn fail(
859        operation_id: impl Into<String>,
860        operation_type: OperationType,
861        error: ErrorObject,
862    ) -> Self {
863        Self {
864            operation_id: operation_id.into(),
865            action: OperationAction::Fail,
866            operation_type,
867            result: None,
868            error: Some(error),
869            parent_id: None,
870            name: None,
871            sub_type: None,
872            wait_options: None,
873            step_options: None,
874            callback_options: None,
875            chained_invoke_options: None,
876            context_options: None,
877        }
878    }
879
880    /// Creates a new OperationUpdate to cancel an operation.
881    ///
882    /// This is primarily used for cancelling WAIT operations.
883    ///
884    /// # Arguments
885    ///
886    /// * `operation_id` - The ID of the operation to cancel
887    /// * `operation_type` - The type of operation being cancelled
888    ///
889    /// # Requirements
890    ///
891    /// - 5.5: THE Wait_Operation SHALL support cancellation of active waits via CANCEL action
892    pub fn cancel(operation_id: impl Into<String>, operation_type: OperationType) -> Self {
893        Self {
894            operation_id: operation_id.into(),
895            action: OperationAction::Cancel,
896            operation_type,
897            result: None,
898            error: None,
899            parent_id: None,
900            name: None,
901            sub_type: None,
902            wait_options: None,
903            step_options: None,
904            callback_options: None,
905            chained_invoke_options: None,
906            context_options: None,
907        }
908    }
909
910    /// Creates a new OperationUpdate to retry an operation with optional payload.
911    ///
912    /// This is used for the wait-for-condition pattern where state needs to be
913    /// passed between retry attempts. The payload contains the state to preserve
914    /// across retries, not an error.
915    ///
916    /// # Arguments
917    ///
918    /// * `operation_id` - The ID of the operation to retry
919    /// * `operation_type` - The type of operation being retried
920    /// * `payload` - Optional state payload to preserve across retries
921    /// * `next_attempt_delay_seconds` - Optional delay before the next retry attempt
922    ///
923    /// # Requirements
924    ///
925    /// - 4.7: THE Step_Operation SHALL support RETRY action with NextAttemptDelaySeconds for backoff
926    /// - 4.8: THE Step_Operation SHALL track attempt numbers in StepDetails.Attempt
927    /// - 4.9: THE Step_Operation SHALL support RETRY action with Payload (not just Error) for wait-for-condition pattern
928    pub fn retry(
929        operation_id: impl Into<String>,
930        operation_type: OperationType,
931        payload: Option<String>,
932        next_attempt_delay_seconds: Option<u64>,
933    ) -> Self {
934        Self {
935            operation_id: operation_id.into(),
936            action: OperationAction::Retry,
937            operation_type,
938            result: payload,
939            error: None,
940            parent_id: None,
941            name: None,
942            sub_type: None,
943            wait_options: None,
944            step_options: Some(StepOptions {
945                next_attempt_delay_seconds,
946            }),
947            callback_options: None,
948            chained_invoke_options: None,
949            context_options: None,
950        }
951    }
952
953    /// Creates a new OperationUpdate to retry an operation with an error.
954    ///
955    /// This is used for traditional retry scenarios where the operation failed
956    /// and needs to be retried after a delay.
957    ///
958    /// # Arguments
959    ///
960    /// * `operation_id` - The ID of the operation to retry
961    /// * `operation_type` - The type of operation being retried
962    /// * `error` - The error that caused the retry
963    /// * `next_attempt_delay_seconds` - Optional delay before the next retry attempt
964    ///
965    /// # Requirements
966    ///
967    /// - 4.7: THE Step_Operation SHALL support RETRY action with NextAttemptDelaySeconds for backoff
968    pub fn retry_with_error(
969        operation_id: impl Into<String>,
970        operation_type: OperationType,
971        error: ErrorObject,
972        next_attempt_delay_seconds: Option<u64>,
973    ) -> Self {
974        Self {
975            operation_id: operation_id.into(),
976            action: OperationAction::Retry,
977            operation_type,
978            result: None,
979            error: Some(error),
980            parent_id: None,
981            name: None,
982            sub_type: None,
983            wait_options: None,
984            step_options: Some(StepOptions {
985                next_attempt_delay_seconds,
986            }),
987            callback_options: None,
988            chained_invoke_options: None,
989            context_options: None,
990        }
991    }
992
993    /// Sets the parent ID for this operation update.
994    pub fn with_parent_id(mut self, parent_id: impl Into<String>) -> Self {
995        self.parent_id = Some(parent_id.into());
996        self
997    }
998
999    /// Sets the name for this operation update.
1000    pub fn with_name(mut self, name: impl Into<String>) -> Self {
1001        self.name = Some(name.into());
1002        self
1003    }
1004
1005    /// Sets the sub-type for this operation update.
1006    /// Requirements: 23.3, 23.4
1007    pub fn with_sub_type(mut self, sub_type: impl Into<String>) -> Self {
1008        self.sub_type = Some(sub_type.into());
1009        self
1010    }
1011
1012    /// Sets the wait options for this operation update.
1013    pub fn with_wait_options(mut self, wait_seconds: u64) -> Self {
1014        self.wait_options = Some(WaitOptions { wait_seconds });
1015        self
1016    }
1017
1018    /// Sets the step options for this operation update.
1019    pub fn with_step_options(mut self, next_attempt_delay_seconds: Option<u64>) -> Self {
1020        self.step_options = Some(StepOptions {
1021            next_attempt_delay_seconds,
1022        });
1023        self
1024    }
1025
1026    /// Sets the callback options for this operation update.
1027    pub fn with_callback_options(
1028        mut self,
1029        timeout_seconds: Option<u64>,
1030        heartbeat_timeout_seconds: Option<u64>,
1031    ) -> Self {
1032        self.callback_options = Some(CallbackOptions {
1033            timeout_seconds,
1034            heartbeat_timeout_seconds,
1035        });
1036        self
1037    }
1038
1039    /// Sets the chained invoke options for this operation update.
1040    pub fn with_chained_invoke_options(
1041        mut self,
1042        function_name: impl Into<String>,
1043        tenant_id: Option<String>,
1044    ) -> Self {
1045        self.chained_invoke_options = Some(ChainedInvokeOptions {
1046            function_name: function_name.into(),
1047            tenant_id,
1048        });
1049        self
1050    }
1051
1052    /// Sets the context options for this operation update.
1053    pub fn with_context_options(mut self, replay_children: Option<bool>) -> Self {
1054        self.context_options = Some(ContextOptions { replay_children });
1055        self
1056    }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061    use super::*;
1062    use proptest::prelude::*;
1063
1064    // ============================================================================
1065    // Proptest Strategies
1066    // ============================================================================
1067
1068    /// Strategy for generating valid OperationType values
1069    /// Feature: rust-sdk-test-suite, Property 1: OperationType Serialization Round-Trip
1070    fn operation_type_strategy() -> impl Strategy<Value = OperationType> {
1071        prop_oneof![
1072            Just(OperationType::Execution),
1073            Just(OperationType::Step),
1074            Just(OperationType::Wait),
1075            Just(OperationType::Callback),
1076            Just(OperationType::Invoke),
1077            Just(OperationType::Context),
1078        ]
1079    }
1080
1081    /// Strategy for generating valid OperationStatus values
1082    /// Feature: rust-sdk-test-suite, Property 2: OperationStatus Serialization Round-Trip
1083    fn operation_status_strategy() -> impl Strategy<Value = OperationStatus> {
1084        prop_oneof![
1085            Just(OperationStatus::Started),
1086            Just(OperationStatus::Pending),
1087            Just(OperationStatus::Ready),
1088            Just(OperationStatus::Succeeded),
1089            Just(OperationStatus::Failed),
1090            Just(OperationStatus::Cancelled),
1091            Just(OperationStatus::TimedOut),
1092            Just(OperationStatus::Stopped),
1093        ]
1094    }
1095
1096    /// Strategy for generating valid OperationAction values
1097    /// Feature: rust-sdk-test-suite, Property 3: OperationAction Serialization Round-Trip
1098    fn operation_action_strategy() -> impl Strategy<Value = OperationAction> {
1099        prop_oneof![
1100            Just(OperationAction::Start),
1101            Just(OperationAction::Succeed),
1102            Just(OperationAction::Fail),
1103            Just(OperationAction::Cancel),
1104            Just(OperationAction::Retry),
1105        ]
1106    }
1107
1108    /// Strategy for generating non-empty strings (for IDs and names)
1109    fn non_empty_string_strategy() -> impl Strategy<Value = String> {
1110        "[a-zA-Z0-9_-]{1,64}".prop_map(|s| s)
1111    }
1112
1113    /// Strategy for generating optional strings
1114    fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
1115        prop_oneof![Just(None), non_empty_string_strategy().prop_map(Some),]
1116    }
1117
1118    /// Strategy for generating optional JSON result strings
1119    fn optional_result_strategy() -> impl Strategy<Value = Option<String>> {
1120        prop_oneof![
1121            Just(None),
1122            Just(Some(r#"{"value": 42}"#.to_string())),
1123            Just(Some(r#""simple string""#.to_string())),
1124            Just(Some("123".to_string())),
1125            Just(Some("true".to_string())),
1126            Just(Some("null".to_string())),
1127        ]
1128    }
1129
1130    /// Strategy for generating optional ErrorObject
1131    fn optional_error_strategy() -> impl Strategy<Value = Option<ErrorObject>> {
1132        prop_oneof![
1133            Just(None),
1134            (non_empty_string_strategy(), non_empty_string_strategy())
1135                .prop_map(|(error_type, message)| Some(ErrorObject::new(error_type, message))),
1136        ]
1137    }
1138
1139    /// Strategy for generating optional timestamps
1140    fn optional_timestamp_strategy() -> impl Strategy<Value = Option<i64>> {
1141        prop_oneof![
1142            Just(None),
1143            // Generate timestamps in a reasonable range (2020-2030)
1144            (1577836800000i64..1893456000000i64).prop_map(Some),
1145        ]
1146    }
1147
1148    /// Strategy for generating valid Operation instances
1149    /// Feature: rust-sdk-test-suite, Property 5: Operation Serialization Round-Trip
1150    fn operation_strategy() -> impl Strategy<Value = Operation> {
1151        (
1152            non_empty_string_strategy(),   // operation_id
1153            operation_type_strategy(),     // operation_type
1154            operation_status_strategy(),   // status
1155            optional_result_strategy(),    // result
1156            optional_error_strategy(),     // error
1157            optional_string_strategy(),    // parent_id
1158            optional_string_strategy(),    // name
1159            optional_string_strategy(),    // sub_type
1160            optional_timestamp_strategy(), // start_timestamp
1161            optional_timestamp_strategy(), // end_timestamp
1162        )
1163            .prop_map(
1164                |(
1165                    operation_id,
1166                    operation_type,
1167                    status,
1168                    result,
1169                    error,
1170                    parent_id,
1171                    name,
1172                    sub_type,
1173                    start_timestamp,
1174                    end_timestamp,
1175                )| {
1176                    Operation {
1177                        operation_id,
1178                        operation_type,
1179                        status,
1180                        result,
1181                        error,
1182                        parent_id,
1183                        name,
1184                        sub_type,
1185                        start_timestamp,
1186                        end_timestamp,
1187                        execution_details: None,
1188                        step_details: None,
1189                        wait_details: None,
1190                        callback_details: None,
1191                        chained_invoke_details: None,
1192                        context_details: None,
1193                    }
1194                },
1195            )
1196    }
1197
1198    // ============================================================================
1199    // Property-Based Tests
1200    // ============================================================================
1201
1202    proptest! {
1203        /// Feature: rust-sdk-test-suite, Property 1: OperationType Serialization Round-Trip
1204        /// For any OperationType value, serializing to JSON then deserializing SHALL produce the same value.
1205        /// **Validates: Requirements 2.1**
1206        #[test]
1207        fn prop_operation_type_serialization_round_trip(op_type in operation_type_strategy()) {
1208            let json = serde_json::to_string(&op_type).expect("serialization should succeed");
1209            let deserialized: OperationType = serde_json::from_str(&json).expect("deserialization should succeed");
1210            prop_assert_eq!(op_type, deserialized, "Round-trip failed for {:?}", op_type);
1211        }
1212
1213        /// Feature: rust-sdk-test-suite, Property 2: OperationStatus Serialization Round-Trip
1214        /// For any OperationStatus value, serializing to JSON then deserializing SHALL produce the same value.
1215        /// **Validates: Requirements 2.2**
1216        #[test]
1217        fn prop_operation_status_serialization_round_trip(status in operation_status_strategy()) {
1218            let json = serde_json::to_string(&status).expect("serialization should succeed");
1219            let deserialized: OperationStatus = serde_json::from_str(&json).expect("deserialization should succeed");
1220            prop_assert_eq!(status, deserialized, "Round-trip failed for {:?}", status);
1221        }
1222
1223        /// Feature: rust-sdk-test-suite, Property 3: OperationAction Serialization Round-Trip
1224        /// For any OperationAction value, serializing to JSON then deserializing SHALL produce the same value.
1225        /// **Validates: Requirements 2.3**
1226        #[test]
1227        fn prop_operation_action_serialization_round_trip(action in operation_action_strategy()) {
1228            let json = serde_json::to_string(&action).expect("serialization should succeed");
1229            let deserialized: OperationAction = serde_json::from_str(&json).expect("deserialization should succeed");
1230            prop_assert_eq!(action, deserialized, "Round-trip failed for {:?}", action);
1231        }
1232
1233        /// Feature: rust-sdk-test-suite, Property 4: Terminal Status Classification
1234        /// For any OperationStatus that is terminal (Succeeded, Failed, Cancelled, TimedOut, Stopped),
1235        /// is_terminal() SHALL return true, and for non-terminal statuses (Started, Pending, Ready),
1236        /// is_terminal() SHALL return false.
1237        /// **Validates: Requirements 2.4, 2.5**
1238        #[test]
1239        fn prop_terminal_status_classification(status in operation_status_strategy()) {
1240            let is_terminal = status.is_terminal();
1241            let expected_terminal = matches!(
1242                status,
1243                OperationStatus::Succeeded
1244                    | OperationStatus::Failed
1245                    | OperationStatus::Cancelled
1246                    | OperationStatus::TimedOut
1247                    | OperationStatus::Stopped
1248            );
1249            prop_assert_eq!(
1250                is_terminal, expected_terminal,
1251                "Terminal classification mismatch for {:?}: got {}, expected {}",
1252                status, is_terminal, expected_terminal
1253            );
1254        }
1255
1256        /// Feature: rust-sdk-test-suite, Property 5: Operation Serialization Round-Trip
1257        /// For any Operation instance with valid fields, serializing to JSON then deserializing
1258        /// SHALL produce an equivalent Operation.
1259        /// **Validates: Requirements 2.6, 9.2**
1260        #[test]
1261        fn prop_operation_serialization_round_trip(op in operation_strategy()) {
1262            let json = serde_json::to_string(&op).expect("serialization should succeed");
1263            let deserialized: Operation = serde_json::from_str(&json).expect("deserialization should succeed");
1264
1265            // Compare all fields
1266            prop_assert_eq!(op.operation_id, deserialized.operation_id, "operation_id mismatch");
1267            prop_assert_eq!(op.operation_type, deserialized.operation_type, "operation_type mismatch");
1268            prop_assert_eq!(op.status, deserialized.status, "status mismatch");
1269            prop_assert_eq!(op.result, deserialized.result, "result mismatch");
1270            prop_assert_eq!(op.parent_id, deserialized.parent_id, "parent_id mismatch");
1271            prop_assert_eq!(op.name, deserialized.name, "name mismatch");
1272            prop_assert_eq!(op.sub_type, deserialized.sub_type, "sub_type mismatch");
1273            prop_assert_eq!(op.start_timestamp, deserialized.start_timestamp, "start_timestamp mismatch");
1274            prop_assert_eq!(op.end_timestamp, deserialized.end_timestamp, "end_timestamp mismatch");
1275
1276            // Compare error if present
1277            match (&op.error, &deserialized.error) {
1278                (Some(e1), Some(e2)) => {
1279                    prop_assert_eq!(&e1.error_type, &e2.error_type, "error_type mismatch");
1280                    prop_assert_eq!(&e1.error_message, &e2.error_message, "error_message mismatch");
1281                }
1282                (None, None) => {}
1283                _ => prop_assert!(false, "error presence mismatch"),
1284            }
1285        }
1286    }
1287
1288    // ============================================================================
1289    // Unit Tests
1290    // ============================================================================
1291
1292    #[test]
1293    fn test_operation_new() {
1294        let op = Operation::new("op-123", OperationType::Step);
1295        assert_eq!(op.operation_id, "op-123");
1296        assert_eq!(op.operation_type, OperationType::Step);
1297        assert_eq!(op.status, OperationStatus::Started);
1298        assert!(op.result.is_none());
1299        assert!(op.error.is_none());
1300        assert!(op.parent_id.is_none());
1301        assert!(op.name.is_none());
1302    }
1303
1304    #[test]
1305    fn test_operation_with_parent_and_name() {
1306        let op = Operation::new("op-123", OperationType::Step)
1307            .with_parent_id("parent-456")
1308            .with_name("my-step");
1309        assert_eq!(op.parent_id, Some("parent-456".to_string()));
1310        assert_eq!(op.name, Some("my-step".to_string()));
1311    }
1312
1313    #[test]
1314    fn test_operation_is_completed() {
1315        let mut op = Operation::new("op-123", OperationType::Step);
1316        assert!(!op.is_completed());
1317
1318        op.status = OperationStatus::Succeeded;
1319        assert!(op.is_completed());
1320
1321        op.status = OperationStatus::Failed;
1322        assert!(op.is_completed());
1323
1324        op.status = OperationStatus::Cancelled;
1325        assert!(op.is_completed());
1326
1327        op.status = OperationStatus::TimedOut;
1328        assert!(op.is_completed());
1329
1330        op.status = OperationStatus::Stopped;
1331        assert!(op.is_completed());
1332    }
1333
1334    #[test]
1335    fn test_operation_is_succeeded() {
1336        let mut op = Operation::new("op-123", OperationType::Step);
1337        assert!(!op.is_succeeded());
1338
1339        op.status = OperationStatus::Succeeded;
1340        assert!(op.is_succeeded());
1341
1342        op.status = OperationStatus::Failed;
1343        assert!(!op.is_succeeded());
1344    }
1345
1346    #[test]
1347    fn test_operation_is_failed() {
1348        let mut op = Operation::new("op-123", OperationType::Step);
1349        assert!(!op.is_failed());
1350
1351        op.status = OperationStatus::Failed;
1352        assert!(op.is_failed());
1353
1354        op.status = OperationStatus::Cancelled;
1355        assert!(op.is_failed());
1356
1357        op.status = OperationStatus::TimedOut;
1358        assert!(op.is_failed());
1359
1360        op.status = OperationStatus::Succeeded;
1361        assert!(!op.is_failed());
1362    }
1363
1364    #[test]
1365    fn test_operation_type_display() {
1366        assert_eq!(OperationType::Execution.to_string(), "Execution");
1367        assert_eq!(OperationType::Step.to_string(), "Step");
1368        assert_eq!(OperationType::Wait.to_string(), "Wait");
1369        assert_eq!(OperationType::Callback.to_string(), "Callback");
1370        assert_eq!(OperationType::Invoke.to_string(), "Invoke");
1371        assert_eq!(OperationType::Context.to_string(), "Context");
1372    }
1373
1374    #[test]
1375    fn test_operation_status_is_terminal() {
1376        assert!(!OperationStatus::Started.is_terminal());
1377        assert!(!OperationStatus::Pending.is_terminal());
1378        assert!(!OperationStatus::Ready.is_terminal());
1379        assert!(OperationStatus::Succeeded.is_terminal());
1380        assert!(OperationStatus::Failed.is_terminal());
1381        assert!(OperationStatus::Cancelled.is_terminal());
1382        assert!(OperationStatus::TimedOut.is_terminal());
1383        assert!(OperationStatus::Stopped.is_terminal());
1384    }
1385
1386    #[test]
1387    fn test_operation_status_is_success() {
1388        assert!(!OperationStatus::Started.is_success());
1389        assert!(!OperationStatus::Pending.is_success());
1390        assert!(!OperationStatus::Ready.is_success());
1391        assert!(OperationStatus::Succeeded.is_success());
1392        assert!(!OperationStatus::Failed.is_success());
1393    }
1394
1395    #[test]
1396    fn test_operation_status_is_failure() {
1397        assert!(!OperationStatus::Started.is_failure());
1398        assert!(!OperationStatus::Pending.is_failure());
1399        assert!(!OperationStatus::Ready.is_failure());
1400        assert!(!OperationStatus::Succeeded.is_failure());
1401        assert!(OperationStatus::Failed.is_failure());
1402        assert!(OperationStatus::Cancelled.is_failure());
1403        assert!(OperationStatus::TimedOut.is_failure());
1404        assert!(OperationStatus::Stopped.is_failure());
1405    }
1406
1407    #[test]
1408    fn test_operation_status_is_pending() {
1409        assert!(!OperationStatus::Started.is_pending());
1410        assert!(OperationStatus::Pending.is_pending());
1411        assert!(!OperationStatus::Ready.is_pending());
1412        assert!(!OperationStatus::Succeeded.is_pending());
1413        assert!(!OperationStatus::Failed.is_pending());
1414    }
1415
1416    #[test]
1417    fn test_operation_status_is_ready() {
1418        assert!(!OperationStatus::Started.is_ready());
1419        assert!(!OperationStatus::Pending.is_ready());
1420        assert!(OperationStatus::Ready.is_ready());
1421        assert!(!OperationStatus::Succeeded.is_ready());
1422        assert!(!OperationStatus::Failed.is_ready());
1423    }
1424
1425    #[test]
1426    fn test_operation_status_is_resumable() {
1427        assert!(OperationStatus::Started.is_resumable());
1428        assert!(OperationStatus::Pending.is_resumable());
1429        assert!(OperationStatus::Ready.is_resumable());
1430        assert!(!OperationStatus::Succeeded.is_resumable());
1431        assert!(!OperationStatus::Failed.is_resumable());
1432        assert!(!OperationStatus::Cancelled.is_resumable());
1433        assert!(!OperationStatus::TimedOut.is_resumable());
1434        assert!(!OperationStatus::Stopped.is_resumable());
1435    }
1436
1437    #[test]
1438    fn test_operation_update_start() {
1439        let update = OperationUpdate::start("op-123", OperationType::Step);
1440        assert_eq!(update.operation_id, "op-123");
1441        assert_eq!(update.action, OperationAction::Start);
1442        assert_eq!(update.operation_type, OperationType::Step);
1443        assert!(update.result.is_none());
1444        assert!(update.error.is_none());
1445    }
1446
1447    #[test]
1448    fn test_operation_update_succeed() {
1449        let update = OperationUpdate::succeed(
1450            "op-123",
1451            OperationType::Step,
1452            Some(r#"{"value": 42}"#.to_string()),
1453        );
1454        assert_eq!(update.operation_id, "op-123");
1455        assert_eq!(update.action, OperationAction::Succeed);
1456        assert_eq!(update.result, Some(r#"{"value": 42}"#.to_string()));
1457        assert!(update.error.is_none());
1458    }
1459
1460    #[test]
1461    fn test_operation_update_fail() {
1462        let error = ErrorObject::new("TestError", "Something went wrong");
1463        let update = OperationUpdate::fail("op-123", OperationType::Step, error);
1464        assert_eq!(update.operation_id, "op-123");
1465        assert_eq!(update.action, OperationAction::Fail);
1466        assert!(update.result.is_none());
1467        assert!(update.error.is_some());
1468        assert_eq!(update.error.as_ref().unwrap().error_type, "TestError");
1469    }
1470
1471    #[test]
1472    fn test_operation_update_with_parent_and_name() {
1473        let update = OperationUpdate::start("op-123", OperationType::Step)
1474            .with_parent_id("parent-456")
1475            .with_name("my-step");
1476        assert_eq!(update.parent_id, Some("parent-456".to_string()));
1477        assert_eq!(update.name, Some("my-step".to_string()));
1478    }
1479
1480    #[test]
1481    fn test_operation_serialization() {
1482        let op = Operation::new("op-123", OperationType::Step)
1483            .with_parent_id("parent-456")
1484            .with_name("my-step");
1485
1486        let json = serde_json::to_string(&op).unwrap();
1487        assert!(json.contains("\"Id\":\"op-123\""));
1488        assert!(json.contains("\"Type\":\"STEP\""));
1489        assert!(json.contains("\"Status\":\"STARTED\""));
1490        assert!(json.contains("\"ParentId\":\"parent-456\""));
1491        assert!(json.contains("\"Name\":\"my-step\""));
1492    }
1493
1494    #[test]
1495    fn test_operation_deserialization() {
1496        // Test with new API field names (Id, Type)
1497        let json = r#"{
1498            "Id": "op-123",
1499            "Type": "STEP",
1500            "Status": "SUCCEEDED",
1501            "Result": "{\"value\": 42}",
1502            "ParentId": "parent-456",
1503            "Name": "my-step"
1504        }"#;
1505
1506        let op: Operation = serde_json::from_str(json).unwrap();
1507        assert_eq!(op.operation_id, "op-123");
1508        assert_eq!(op.operation_type, OperationType::Step);
1509        assert_eq!(op.status, OperationStatus::Succeeded);
1510        assert_eq!(op.result, Some(r#"{"value": 42}"#.to_string()));
1511        assert_eq!(op.parent_id, Some("parent-456".to_string()));
1512        assert_eq!(op.name, Some("my-step".to_string()));
1513    }
1514
1515    #[test]
1516    fn test_operation_deserialization_legacy_field_names() {
1517        // Test with legacy field names (OperationId, OperationType) for backward compatibility
1518        let json = r#"{
1519            "OperationId": "op-123",
1520            "OperationType": "STEP",
1521            "Status": "SUCCEEDED",
1522            "Result": "{\"value\": 42}",
1523            "ParentId": "parent-456",
1524            "Name": "my-step"
1525        }"#;
1526
1527        let op: Operation = serde_json::from_str(json).unwrap();
1528        assert_eq!(op.operation_id, "op-123");
1529        assert_eq!(op.operation_type, OperationType::Step);
1530        assert_eq!(op.status, OperationStatus::Succeeded);
1531    }
1532
1533    #[test]
1534    fn test_operation_deserialization_with_timestamps() {
1535        // Test with timestamps and execution details (as sent by the API)
1536        let json = r#"{
1537            "Id": "778f03ea-ab5a-3e77-8d6d-9119253f8565",
1538            "Name": "21e26aa2-4866-4c09-958a-15a272f16c87",
1539            "Type": "EXECUTION",
1540            "StartTimestamp": 1767896523358,
1541            "Status": "STARTED",
1542            "ExecutionDetails": {
1543                "InputPayload": "{\"order_id\":\"order-122342134\"}"
1544            }
1545        }"#;
1546
1547        let op: Operation = serde_json::from_str(json).unwrap();
1548        assert_eq!(op.operation_id, "778f03ea-ab5a-3e77-8d6d-9119253f8565");
1549        assert_eq!(op.operation_type, OperationType::Execution);
1550        assert_eq!(op.status, OperationStatus::Started);
1551        assert_eq!(op.start_timestamp, Some(1767896523358));
1552        assert!(op.execution_details.is_some());
1553        let details = op.execution_details.unwrap();
1554        assert!(details.input_payload.is_some());
1555    }
1556
1557    #[test]
1558    fn test_operation_update_serialization() {
1559        let update = OperationUpdate::succeed(
1560            "op-123",
1561            OperationType::Step,
1562            Some(r#"{"value": 42}"#.to_string()),
1563        )
1564        .with_parent_id("parent-456");
1565
1566        let json = serde_json::to_string(&update).unwrap();
1567        assert!(json.contains("\"Id\":\"op-123\""));
1568        assert!(json.contains("\"Action\":\"SUCCEED\""));
1569        assert!(json.contains("\"Type\":\"STEP\""));
1570        assert!(json.contains("\"Payload\":\"{\\\"value\\\": 42}\""));
1571        assert!(json.contains("\"ParentId\":\"parent-456\""));
1572    }
1573
1574    #[test]
1575    fn test_operation_status_pending_serialization() {
1576        // Test PENDING status serialization/deserialization
1577        let json = r#"{
1578            "Id": "op-123",
1579            "Type": "STEP",
1580            "Status": "PENDING"
1581        }"#;
1582
1583        let op: Operation = serde_json::from_str(json).unwrap();
1584        assert_eq!(op.status, OperationStatus::Pending);
1585        assert!(op.status.is_pending());
1586        assert!(!op.status.is_terminal());
1587        assert!(op.status.is_resumable());
1588    }
1589
1590    #[test]
1591    fn test_operation_status_ready_serialization() {
1592        // Test READY status serialization/deserialization
1593        let json = r#"{
1594            "Id": "op-123",
1595            "Type": "STEP",
1596            "Status": "READY"
1597        }"#;
1598
1599        let op: Operation = serde_json::from_str(json).unwrap();
1600        assert_eq!(op.status, OperationStatus::Ready);
1601        assert!(op.status.is_ready());
1602        assert!(!op.status.is_terminal());
1603        assert!(op.status.is_resumable());
1604    }
1605
1606    #[test]
1607    fn test_operation_status_display() {
1608        assert_eq!(OperationStatus::Started.to_string(), "Started");
1609        assert_eq!(OperationStatus::Pending.to_string(), "Pending");
1610        assert_eq!(OperationStatus::Ready.to_string(), "Ready");
1611        assert_eq!(OperationStatus::Succeeded.to_string(), "Succeeded");
1612        assert_eq!(OperationStatus::Failed.to_string(), "Failed");
1613        assert_eq!(OperationStatus::Cancelled.to_string(), "Cancelled");
1614        assert_eq!(OperationStatus::TimedOut.to_string(), "TimedOut");
1615        assert_eq!(OperationStatus::Stopped.to_string(), "Stopped");
1616    }
1617
1618    #[test]
1619    fn test_operation_with_sub_type() {
1620        let op = Operation::new("op-123", OperationType::Context).with_sub_type("map");
1621        assert_eq!(op.sub_type, Some("map".to_string()));
1622    }
1623
1624    #[test]
1625    fn test_operation_update_with_sub_type() {
1626        let update =
1627            OperationUpdate::start("op-123", OperationType::Context).with_sub_type("parallel");
1628        assert_eq!(update.sub_type, Some("parallel".to_string()));
1629    }
1630
1631    #[test]
1632    fn test_operation_sub_type_serialization() {
1633        let op =
1634            Operation::new("op-123", OperationType::Context).with_sub_type("wait_for_condition");
1635
1636        let json = serde_json::to_string(&op).unwrap();
1637        assert!(json.contains("\"SubType\":\"wait_for_condition\""));
1638    }
1639
1640    #[test]
1641    fn test_operation_sub_type_deserialization() {
1642        let json = r#"{
1643            "Id": "op-123",
1644            "Type": "CONTEXT",
1645            "Status": "STARTED",
1646            "SubType": "map"
1647        }"#;
1648
1649        let op: Operation = serde_json::from_str(json).unwrap();
1650        assert_eq!(op.sub_type, Some("map".to_string()));
1651    }
1652
1653    #[test]
1654    fn test_operation_metadata_fields() {
1655        // Test that start_timestamp and end_timestamp are properly deserialized
1656        let json = r#"{
1657            "Id": "op-123",
1658            "Type": "STEP",
1659            "Status": "SUCCEEDED",
1660            "StartTimestamp": 1704067200000,
1661            "EndTimestamp": 1704067260000,
1662            "Name": "my-step",
1663            "SubType": "custom"
1664        }"#;
1665
1666        let op: Operation = serde_json::from_str(json).unwrap();
1667        assert_eq!(op.start_timestamp, Some(1704067200000));
1668        assert_eq!(op.end_timestamp, Some(1704067260000));
1669        assert_eq!(op.name, Some("my-step".to_string()));
1670        assert_eq!(op.sub_type, Some("custom".to_string()));
1671    }
1672
1673    #[test]
1674    fn test_operation_action_retry_display() {
1675        assert_eq!(OperationAction::Retry.to_string(), "Retry");
1676    }
1677
1678    #[test]
1679    fn test_operation_update_retry_with_payload() {
1680        let update = OperationUpdate::retry(
1681            "op-123",
1682            OperationType::Step,
1683            Some(r#"{"state": "waiting"}"#.to_string()),
1684            Some(5),
1685        );
1686        assert_eq!(update.operation_id, "op-123");
1687        assert_eq!(update.action, OperationAction::Retry);
1688        assert_eq!(update.operation_type, OperationType::Step);
1689        assert_eq!(update.result, Some(r#"{"state": "waiting"}"#.to_string()));
1690        assert!(update.error.is_none());
1691        assert!(update.step_options.is_some());
1692        assert_eq!(
1693            update
1694                .step_options
1695                .as_ref()
1696                .unwrap()
1697                .next_attempt_delay_seconds,
1698            Some(5)
1699        );
1700    }
1701
1702    #[test]
1703    fn test_operation_update_retry_with_error() {
1704        let error = ErrorObject::new("RetryableError", "Temporary failure");
1705        let update =
1706            OperationUpdate::retry_with_error("op-123", OperationType::Step, error, Some(10));
1707        assert_eq!(update.operation_id, "op-123");
1708        assert_eq!(update.action, OperationAction::Retry);
1709        assert!(update.result.is_none());
1710        assert!(update.error.is_some());
1711        assert_eq!(update.error.as_ref().unwrap().error_type, "RetryableError");
1712        assert_eq!(
1713            update
1714                .step_options
1715                .as_ref()
1716                .unwrap()
1717                .next_attempt_delay_seconds,
1718            Some(10)
1719        );
1720    }
1721
1722    #[test]
1723    fn test_operation_update_retry_serialization() {
1724        let update = OperationUpdate::retry(
1725            "op-123",
1726            OperationType::Step,
1727            Some(r#"{"counter": 5}"#.to_string()),
1728            Some(3),
1729        );
1730
1731        let json = serde_json::to_string(&update).unwrap();
1732        assert!(json.contains("\"Action\":\"RETRY\""));
1733        assert!(json.contains("\"Payload\":\"{\\\"counter\\\": 5}\""));
1734        assert!(json.contains("\"NextAttemptDelaySeconds\":3"));
1735    }
1736
1737    #[test]
1738    fn test_step_details_with_payload() {
1739        let json = r#"{
1740            "Id": "op-123",
1741            "Type": "STEP",
1742            "Status": "PENDING",
1743            "StepDetails": {
1744                "Attempt": 2,
1745                "Payload": "{\"state\": \"processing\"}"
1746            }
1747        }"#;
1748
1749        let op: Operation = serde_json::from_str(json).unwrap();
1750        assert_eq!(op.status, OperationStatus::Pending);
1751        assert!(op.step_details.is_some());
1752        let details = op.step_details.as_ref().unwrap();
1753        assert_eq!(details.attempt, Some(2));
1754        assert_eq!(
1755            details.payload,
1756            Some(r#"{"state": "processing"}"#.to_string())
1757        );
1758    }
1759
1760    #[test]
1761    fn test_operation_get_retry_payload() {
1762        let mut op = Operation::new("op-123", OperationType::Step);
1763        op.step_details = Some(StepDetails {
1764            result: None,
1765            attempt: Some(1),
1766            next_attempt_timestamp: None,
1767            error: None,
1768            payload: Some(r#"{"counter": 3}"#.to_string()),
1769        });
1770
1771        assert_eq!(op.get_retry_payload(), Some(r#"{"counter": 3}"#));
1772    }
1773
1774    #[test]
1775    fn test_operation_get_attempt() {
1776        let mut op = Operation::new("op-123", OperationType::Step);
1777        op.step_details = Some(StepDetails {
1778            result: None,
1779            attempt: Some(5),
1780            next_attempt_timestamp: None,
1781            error: None,
1782            payload: None,
1783        });
1784
1785        assert_eq!(op.get_attempt(), Some(5));
1786    }
1787
1788    #[test]
1789    fn test_operation_get_attempt_no_details() {
1790        let op = Operation::new("op-123", OperationType::Step);
1791        assert_eq!(op.get_attempt(), None);
1792    }
1793
1794    #[test]
1795    fn test_operation_get_retry_payload_wrong_type() {
1796        let op = Operation::new("op-123", OperationType::Wait);
1797        assert_eq!(op.get_retry_payload(), None);
1798    }
1799
1800    // Size verification tests for enum discriminant optimization
1801    // Requirements: 6.7 - Verify each enum is 1 byte after optimization
1802
1803    #[test]
1804    fn test_operation_status_size_is_one_byte() {
1805        assert_eq!(
1806            std::mem::size_of::<OperationStatus>(),
1807            1,
1808            "OperationStatus should be 1 byte with #[repr(u8)]"
1809        );
1810    }
1811
1812    #[test]
1813    fn test_operation_type_size_is_one_byte() {
1814        assert_eq!(
1815            std::mem::size_of::<OperationType>(),
1816            1,
1817            "OperationType should be 1 byte with #[repr(u8)]"
1818        );
1819    }
1820
1821    #[test]
1822    fn test_operation_action_size_is_one_byte() {
1823        assert_eq!(
1824            std::mem::size_of::<OperationAction>(),
1825            1,
1826            "OperationAction should be 1 byte with #[repr(u8)]"
1827        );
1828    }
1829
1830    // Serde compatibility tests for enum discriminant optimization
1831    // Requirements: 6.6 - Verify JSON serialization uses string representations
1832
1833    #[test]
1834    fn test_operation_status_serde_uses_string_representation() {
1835        // Verify serialization produces string values, not numeric discriminants
1836        let status = OperationStatus::Started;
1837        let json = serde_json::to_string(&status).unwrap();
1838        assert_eq!(json, "\"STARTED\"");
1839
1840        let status = OperationStatus::Pending;
1841        let json = serde_json::to_string(&status).unwrap();
1842        assert_eq!(json, "\"PENDING\"");
1843
1844        let status = OperationStatus::Ready;
1845        let json = serde_json::to_string(&status).unwrap();
1846        assert_eq!(json, "\"READY\"");
1847
1848        let status = OperationStatus::Succeeded;
1849        let json = serde_json::to_string(&status).unwrap();
1850        assert_eq!(json, "\"SUCCEEDED\"");
1851
1852        let status = OperationStatus::Failed;
1853        let json = serde_json::to_string(&status).unwrap();
1854        assert_eq!(json, "\"FAILED\"");
1855
1856        let status = OperationStatus::Cancelled;
1857        let json = serde_json::to_string(&status).unwrap();
1858        assert_eq!(json, "\"CANCELLED\"");
1859
1860        let status = OperationStatus::TimedOut;
1861        let json = serde_json::to_string(&status).unwrap();
1862        assert_eq!(json, "\"TIMED_OUT\"");
1863
1864        let status = OperationStatus::Stopped;
1865        let json = serde_json::to_string(&status).unwrap();
1866        assert_eq!(json, "\"STOPPED\"");
1867    }
1868
1869    #[test]
1870    fn test_operation_status_serde_round_trip() {
1871        let statuses = [
1872            OperationStatus::Started,
1873            OperationStatus::Pending,
1874            OperationStatus::Ready,
1875            OperationStatus::Succeeded,
1876            OperationStatus::Failed,
1877            OperationStatus::Cancelled,
1878            OperationStatus::TimedOut,
1879            OperationStatus::Stopped,
1880        ];
1881
1882        for status in statuses {
1883            let json = serde_json::to_string(&status).unwrap();
1884            let deserialized: OperationStatus = serde_json::from_str(&json).unwrap();
1885            assert_eq!(status, deserialized, "Round-trip failed for {:?}", status);
1886        }
1887    }
1888
1889    #[test]
1890    fn test_operation_type_serde_uses_string_representation() {
1891        // Verify serialization produces string values, not numeric discriminants
1892        let op_type = OperationType::Execution;
1893        let json = serde_json::to_string(&op_type).unwrap();
1894        assert_eq!(json, "\"EXECUTION\"");
1895
1896        let op_type = OperationType::Step;
1897        let json = serde_json::to_string(&op_type).unwrap();
1898        assert_eq!(json, "\"STEP\"");
1899
1900        let op_type = OperationType::Wait;
1901        let json = serde_json::to_string(&op_type).unwrap();
1902        assert_eq!(json, "\"WAIT\"");
1903
1904        let op_type = OperationType::Callback;
1905        let json = serde_json::to_string(&op_type).unwrap();
1906        assert_eq!(json, "\"CALLBACK\"");
1907
1908        let op_type = OperationType::Invoke;
1909        let json = serde_json::to_string(&op_type).unwrap();
1910        assert_eq!(json, "\"INVOKE\"");
1911
1912        let op_type = OperationType::Context;
1913        let json = serde_json::to_string(&op_type).unwrap();
1914        assert_eq!(json, "\"CONTEXT\"");
1915    }
1916
1917    #[test]
1918    fn test_operation_type_serde_round_trip() {
1919        let types = [
1920            OperationType::Execution,
1921            OperationType::Step,
1922            OperationType::Wait,
1923            OperationType::Callback,
1924            OperationType::Invoke,
1925            OperationType::Context,
1926        ];
1927
1928        for op_type in types {
1929            let json = serde_json::to_string(&op_type).unwrap();
1930            let deserialized: OperationType = serde_json::from_str(&json).unwrap();
1931            assert_eq!(op_type, deserialized, "Round-trip failed for {:?}", op_type);
1932        }
1933    }
1934
1935    #[test]
1936    fn test_operation_action_serde_uses_string_representation() {
1937        // Verify serialization produces string values, not numeric discriminants
1938        let action = OperationAction::Start;
1939        let json = serde_json::to_string(&action).unwrap();
1940        assert_eq!(json, "\"START\"");
1941
1942        let action = OperationAction::Succeed;
1943        let json = serde_json::to_string(&action).unwrap();
1944        assert_eq!(json, "\"SUCCEED\"");
1945
1946        let action = OperationAction::Fail;
1947        let json = serde_json::to_string(&action).unwrap();
1948        assert_eq!(json, "\"FAIL\"");
1949
1950        let action = OperationAction::Cancel;
1951        let json = serde_json::to_string(&action).unwrap();
1952        assert_eq!(json, "\"CANCEL\"");
1953
1954        let action = OperationAction::Retry;
1955        let json = serde_json::to_string(&action).unwrap();
1956        assert_eq!(json, "\"RETRY\"");
1957    }
1958
1959    #[test]
1960    fn test_operation_action_serde_round_trip() {
1961        let actions = [
1962            OperationAction::Start,
1963            OperationAction::Succeed,
1964            OperationAction::Fail,
1965            OperationAction::Cancel,
1966            OperationAction::Retry,
1967        ];
1968
1969        for action in actions {
1970            let json = serde_json::to_string(&action).unwrap();
1971            let deserialized: OperationAction = serde_json::from_str(&json).unwrap();
1972            assert_eq!(action, deserialized, "Round-trip failed for {:?}", action);
1973        }
1974    }
1975
1976    // Timestamp parsing tests
1977
1978    #[test]
1979    fn test_parse_iso8601_rfc3339_format() {
1980        // Standard RFC 3339 format
1981        let result = parse_iso8601_to_millis("2026-01-13T04:10:18.841+00:00");
1982        assert!(result.is_ok(), "Failed to parse: {:?}", result);
1983        let millis = result.unwrap();
1984        // 2026-01-13T04:10:18.841Z - verify it's a reasonable timestamp
1985        // January 13, 2026 is in the future, so millis should be > current time
1986        // Let's just verify it parsed to a positive value and is in a reasonable range
1987        assert!(millis > 0, "Timestamp should be positive, got {}", millis);
1988        // Should be after year 2020 (1577836800000) and before year 2100 (4102444800000)
1989        assert!(
1990            millis > 1577836800000 && millis < 4102444800000,
1991            "Timestamp {} is outside reasonable range",
1992            millis
1993        );
1994    }
1995
1996    #[test]
1997    fn test_parse_iso8601_with_space_separator() {
1998        // Format with space instead of T (common in some systems)
1999        let result = parse_iso8601_to_millis("2026-01-13 04:10:18.841055+00:00");
2000        assert!(result.is_ok());
2001    }
2002
2003    #[test]
2004    fn test_parse_iso8601_without_timezone() {
2005        // Naive datetime (assumes UTC)
2006        let result = parse_iso8601_to_millis("2026-01-13T04:10:18.841");
2007        assert!(result.is_ok());
2008    }
2009
2010    #[test]
2011    fn test_parse_iso8601_without_fractional_seconds() {
2012        // No fractional seconds
2013        let result = parse_iso8601_to_millis("2026-01-13T04:10:18+00:00");
2014        assert!(result.is_ok());
2015    }
2016
2017    #[test]
2018    fn test_parse_iso8601_invalid_format() {
2019        // Invalid format should return error
2020        let result = parse_iso8601_to_millis("not-a-timestamp");
2021        assert!(result.is_err());
2022    }
2023
2024    #[test]
2025    fn test_timestamp_deserialization_integer() {
2026        let json = r#"{
2027            "Id": "op-123",
2028            "Type": "STEP",
2029            "Status": "STARTED",
2030            "StartTimestamp": 1768279818841
2031        }"#;
2032
2033        let op: Operation = serde_json::from_str(json).unwrap();
2034        assert_eq!(op.start_timestamp, Some(1768279818841));
2035    }
2036
2037    #[test]
2038    fn test_timestamp_deserialization_float() {
2039        // Floating point timestamp (seconds with fractional milliseconds)
2040        let json = r#"{
2041            "Id": "op-123",
2042            "Type": "STEP",
2043            "Status": "STARTED",
2044            "StartTimestamp": 1768279818.841
2045        }"#;
2046
2047        let op: Operation = serde_json::from_str(json).unwrap();
2048        // Should be converted to milliseconds
2049        assert_eq!(op.start_timestamp, Some(1768279818841));
2050    }
2051
2052    #[test]
2053    fn test_timestamp_deserialization_iso8601_string() {
2054        let json = r#"{
2055            "Id": "op-123",
2056            "Type": "STEP",
2057            "Status": "STARTED",
2058            "StartTimestamp": "2026-01-13T04:10:18.841+00:00"
2059        }"#;
2060
2061        let op: Operation = serde_json::from_str(json).unwrap();
2062        assert!(op.start_timestamp.is_some());
2063        let ts = op.start_timestamp.unwrap();
2064        // Should be a reasonable timestamp (after 2020, before 2100)
2065        assert!(
2066            ts > 1577836800000 && ts < 4102444800000,
2067            "Timestamp {} is outside reasonable range",
2068            ts
2069        );
2070    }
2071
2072    #[test]
2073    fn test_timestamp_deserialization_null() {
2074        let json = r#"{
2075            "Id": "op-123",
2076            "Type": "STEP",
2077            "Status": "STARTED",
2078            "StartTimestamp": null
2079        }"#;
2080
2081        let op: Operation = serde_json::from_str(json).unwrap();
2082        assert!(op.start_timestamp.is_none());
2083    }
2084
2085    #[test]
2086    fn test_timestamp_deserialization_missing() {
2087        let json = r#"{
2088            "Id": "op-123",
2089            "Type": "STEP",
2090            "Status": "STARTED"
2091        }"#;
2092
2093        let op: Operation = serde_json::from_str(json).unwrap();
2094        assert!(op.start_timestamp.is_none());
2095    }
2096}