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    pub fn get_retry_payload(&self) -> Option<&str> {
444        if self.operation_type == OperationType::Step {
445            if let Some(ref details) = self.step_details {
446                return details.payload.as_deref();
447            }
448        }
449        None
450    }
451
452    /// Gets the current attempt number from StepDetails for STEP operations.
453    ///
454    /// # Returns
455    ///
456    /// The attempt number (0-indexed) if this is a STEP operation with attempt tracking, None otherwise.
457    pub fn get_attempt(&self) -> Option<u32> {
458        if self.operation_type == OperationType::Step {
459            if let Some(ref details) = self.step_details {
460                return details.attempt;
461            }
462        }
463        None
464    }
465}
466
467/// The type of operation in a durable execution.
468///
469/// This enum uses `#[repr(u8)]` for compact memory representation (1 byte).
470/// Explicit discriminant values ensure stability across versions.
471///
472/// # Examples
473///
474/// ```
475/// use durable_execution_sdk::operation::OperationType;
476///
477/// let step = OperationType::Step;
478/// let wait = OperationType::Wait;
479///
480/// // Serialization uses uppercase names
481/// let json = serde_json::to_string(&step).unwrap();
482/// assert_eq!(json, "\"STEP\"");
483///
484/// // Display uses title case
485/// assert_eq!(format!("{}", step), "Step");
486/// assert_eq!(format!("{}", wait), "Wait");
487/// ```
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
489#[repr(u8)]
490pub enum OperationType {
491    /// The root execution operation
492    #[serde(rename = "EXECUTION")]
493    Execution = 0,
494    /// A step operation (unit of work)
495    #[serde(rename = "STEP")]
496    Step = 1,
497    /// A wait/sleep operation
498    #[serde(rename = "WAIT")]
499    Wait = 2,
500    /// A callback operation waiting for external signal
501    #[serde(rename = "CALLBACK")]
502    Callback = 3,
503    /// An invoke operation calling another Lambda function
504    #[serde(rename = "INVOKE")]
505    Invoke = 4,
506    /// A context operation for nested child contexts
507    #[serde(rename = "CONTEXT")]
508    Context = 5,
509}
510
511impl std::fmt::Display for OperationType {
512    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513        match self {
514            Self::Execution => write!(f, "Execution"),
515            Self::Step => write!(f, "Step"),
516            Self::Wait => write!(f, "Wait"),
517            Self::Callback => write!(f, "Callback"),
518            Self::Invoke => write!(f, "Invoke"),
519            Self::Context => write!(f, "Context"),
520        }
521    }
522}
523
524/// The status of an operation in a durable execution.
525///
526/// This enum uses `#[repr(u8)]` for compact memory representation (1 byte).
527/// Explicit discriminant values ensure stability across versions.
528///
529/// # Examples
530///
531/// ```
532/// use durable_execution_sdk::operation::OperationStatus;
533///
534/// let succeeded = OperationStatus::Succeeded;
535/// let pending = OperationStatus::Pending;
536///
537/// // Check terminal status
538/// assert!(succeeded.is_terminal());
539/// assert!(!pending.is_terminal());
540///
541/// // Check success/failure
542/// assert!(succeeded.is_success());
543/// assert!(!succeeded.is_failure());
544///
545/// // Serialization uses uppercase names
546/// let json = serde_json::to_string(&succeeded).unwrap();
547/// assert_eq!(json, "\"SUCCEEDED\"");
548/// ```
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
550#[repr(u8)]
551pub enum OperationStatus {
552    /// Operation has started but not completed
553    #[serde(rename = "STARTED")]
554    Started = 0,
555    /// Operation is pending (e.g., step waiting for retry)
556    /// Requirements: 3.7, 4.7
557    #[serde(rename = "PENDING")]
558    Pending = 1,
559    /// Operation is ready to resume execution (e.g., after retry delay)
560    /// Requirements: 3.7, 4.7
561    #[serde(rename = "READY")]
562    Ready = 2,
563    /// Operation completed successfully
564    #[serde(rename = "SUCCEEDED")]
565    Succeeded = 3,
566    /// Operation failed with an error
567    #[serde(rename = "FAILED")]
568    Failed = 4,
569    /// Operation was cancelled
570    #[serde(rename = "CANCELLED")]
571    Cancelled = 5,
572    /// Operation timed out
573    #[serde(rename = "TIMED_OUT")]
574    TimedOut = 6,
575    /// Operation was stopped externally
576    #[serde(rename = "STOPPED")]
577    Stopped = 7,
578}
579
580impl OperationStatus {
581    /// Returns true if this status represents a terminal state.
582    pub fn is_terminal(&self) -> bool {
583        !matches!(self, Self::Started | Self::Pending | Self::Ready)
584    }
585
586    /// Returns true if this status represents a successful completion.
587    pub fn is_success(&self) -> bool {
588        matches!(self, Self::Succeeded)
589    }
590
591    /// Returns true if this status represents a failure.
592    pub fn is_failure(&self) -> bool {
593        matches!(
594            self,
595            Self::Failed | Self::Cancelled | Self::TimedOut | Self::Stopped
596        )
597    }
598
599    /// Returns true if this status indicates the operation is pending (waiting for retry).
600    /// Requirements: 3.7, 4.7
601    pub fn is_pending(&self) -> bool {
602        matches!(self, Self::Pending)
603    }
604
605    /// Returns true if this status indicates the operation is ready to resume.
606    /// Requirements: 3.7, 4.7
607    pub fn is_ready(&self) -> bool {
608        matches!(self, Self::Ready)
609    }
610
611    /// Returns true if this status indicates the operation can be resumed.
612    /// This includes both PENDING and READY statuses.
613    /// Requirements: 3.7
614    pub fn is_resumable(&self) -> bool {
615        matches!(self, Self::Started | Self::Pending | Self::Ready)
616    }
617}
618
619impl std::fmt::Display for OperationStatus {
620    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621        match self {
622            Self::Started => write!(f, "Started"),
623            Self::Pending => write!(f, "Pending"),
624            Self::Ready => write!(f, "Ready"),
625            Self::Succeeded => write!(f, "Succeeded"),
626            Self::Failed => write!(f, "Failed"),
627            Self::Cancelled => write!(f, "Cancelled"),
628            Self::TimedOut => write!(f, "TimedOut"),
629            Self::Stopped => write!(f, "Stopped"),
630        }
631    }
632}
633
634/// Action to perform on an operation during checkpoint.
635///
636/// This enum uses `#[repr(u8)]` for compact memory representation (1 byte).
637/// Explicit discriminant values ensure stability across versions.
638#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
639#[repr(u8)]
640pub enum OperationAction {
641    /// Start a new operation
642    #[serde(rename = "START")]
643    Start = 0,
644    /// Mark operation as succeeded
645    #[serde(rename = "SUCCEED")]
646    Succeed = 1,
647    /// Mark operation as failed
648    #[serde(rename = "FAIL")]
649    Fail = 2,
650    /// Cancel an operation (e.g., cancel a wait)
651    /// Requirements: 5.5
652    #[serde(rename = "CANCEL")]
653    Cancel = 3,
654    /// Retry an operation with optional payload (state) for wait-for-condition pattern
655    /// Requirements: 4.7, 4.8, 4.9
656    #[serde(rename = "RETRY")]
657    Retry = 4,
658}
659
660impl std::fmt::Display for OperationAction {
661    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
662        match self {
663            Self::Start => write!(f, "Start"),
664            Self::Succeed => write!(f, "Succeed"),
665            Self::Fail => write!(f, "Fail"),
666            Self::Cancel => write!(f, "Cancel"),
667            Self::Retry => write!(f, "Retry"),
668        }
669    }
670}
671
672/// Options for WAIT operations
673#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct WaitOptions {
675    /// Number of seconds to wait
676    #[serde(rename = "WaitSeconds")]
677    pub wait_seconds: u64,
678}
679
680/// Options for STEP operations
681#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct StepOptions {
683    /// Delay in seconds before the next retry attempt
684    #[serde(
685        rename = "NextAttemptDelaySeconds",
686        skip_serializing_if = "Option::is_none"
687    )]
688    pub next_attempt_delay_seconds: Option<u64>,
689}
690
691/// Options for CALLBACK operations
692#[derive(Debug, Clone, Serialize, Deserialize)]
693pub struct CallbackOptions {
694    /// Timeout in seconds for the callback
695    #[serde(rename = "TimeoutSeconds", skip_serializing_if = "Option::is_none")]
696    pub timeout_seconds: Option<u64>,
697    /// Heartbeat timeout in seconds
698    #[serde(
699        rename = "HeartbeatTimeoutSeconds",
700        skip_serializing_if = "Option::is_none"
701    )]
702    pub heartbeat_timeout_seconds: Option<u64>,
703}
704
705/// Options for CHAINED_INVOKE operations
706#[derive(Debug, Clone, Serialize, Deserialize)]
707pub struct ChainedInvokeOptions {
708    /// The function name or ARN to invoke
709    #[serde(rename = "FunctionName")]
710    pub function_name: String,
711    /// Optional tenant ID for multi-tenant scenarios
712    #[serde(rename = "TenantId", skip_serializing_if = "Option::is_none")]
713    pub tenant_id: Option<String>,
714}
715
716/// Options for CONTEXT operations
717#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct ContextOptions {
719    /// Whether to replay children when the context is loaded
720    #[serde(rename = "ReplayChildren", skip_serializing_if = "Option::is_none")]
721    pub replay_children: Option<bool>,
722}
723
724/// Represents an update to be checkpointed for an operation.
725///
726/// This struct is used to send checkpoint requests to the Lambda service.
727/// Field names match the CheckpointDurableExecution API format.
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct OperationUpdate {
730    /// Unique identifier for this operation
731    #[serde(rename = "Id")]
732    pub operation_id: String,
733
734    /// The action to perform (Start, Succeed, Fail)
735    #[serde(rename = "Action")]
736    pub action: OperationAction,
737
738    /// The type of operation
739    #[serde(rename = "Type")]
740    pub operation_type: OperationType,
741
742    /// Serialized result if succeeding (called "Payload" in the API)
743    #[serde(rename = "Payload", skip_serializing_if = "Option::is_none")]
744    pub result: Option<String>,
745
746    /// Error details if failing
747    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
748    pub error: Option<ErrorObject>,
749
750    /// Parent operation ID for nested operations
751    #[serde(rename = "ParentId", skip_serializing_if = "Option::is_none")]
752    pub parent_id: Option<String>,
753
754    /// Optional human-readable name for the operation
755    #[serde(rename = "Name", skip_serializing_if = "Option::is_none")]
756    pub name: Option<String>,
757
758    /// SDK-level categorization of the operation (e.g., "map", "parallel", "wait_for_condition")
759    /// Requirements: 23.3, 23.4
760    #[serde(rename = "SubType", skip_serializing_if = "Option::is_none")]
761    pub sub_type: Option<String>,
762
763    /// Options for WAIT operations
764    #[serde(rename = "WaitOptions", skip_serializing_if = "Option::is_none")]
765    pub wait_options: Option<WaitOptions>,
766
767    /// Options for STEP operations
768    #[serde(rename = "StepOptions", skip_serializing_if = "Option::is_none")]
769    pub step_options: Option<StepOptions>,
770
771    /// Options for CALLBACK operations
772    #[serde(rename = "CallbackOptions", skip_serializing_if = "Option::is_none")]
773    pub callback_options: Option<CallbackOptions>,
774
775    /// Options for CHAINED_INVOKE operations
776    #[serde(
777        rename = "ChainedInvokeOptions",
778        skip_serializing_if = "Option::is_none"
779    )]
780    pub chained_invoke_options: Option<ChainedInvokeOptions>,
781
782    /// Options for CONTEXT operations
783    #[serde(rename = "ContextOptions", skip_serializing_if = "Option::is_none")]
784    pub context_options: Option<ContextOptions>,
785}
786
787impl OperationUpdate {
788    /// Creates a new OperationUpdate to start an operation.
789    pub fn start(operation_id: impl Into<String>, operation_type: OperationType) -> Self {
790        Self {
791            operation_id: operation_id.into(),
792            action: OperationAction::Start,
793            operation_type,
794            result: None,
795            error: None,
796            parent_id: None,
797            name: None,
798            sub_type: None,
799            wait_options: None,
800            step_options: None,
801            callback_options: None,
802            chained_invoke_options: None,
803            context_options: None,
804        }
805    }
806
807    /// Creates a new OperationUpdate to start a WAIT operation with the required WaitOptions.
808    pub fn start_wait(operation_id: impl Into<String>, wait_seconds: u64) -> Self {
809        Self {
810            operation_id: operation_id.into(),
811            action: OperationAction::Start,
812            operation_type: OperationType::Wait,
813            result: None,
814            error: None,
815            parent_id: None,
816            name: None,
817            sub_type: None,
818            wait_options: Some(WaitOptions { wait_seconds }),
819            step_options: None,
820            callback_options: None,
821            chained_invoke_options: None,
822            context_options: None,
823        }
824    }
825
826    /// Creates a new OperationUpdate to mark an operation as succeeded.
827    pub fn succeed(
828        operation_id: impl Into<String>,
829        operation_type: OperationType,
830        result: Option<String>,
831    ) -> Self {
832        Self {
833            operation_id: operation_id.into(),
834            action: OperationAction::Succeed,
835            operation_type,
836            result,
837            error: None,
838            parent_id: None,
839            name: None,
840            sub_type: None,
841            wait_options: None,
842            step_options: None,
843            callback_options: None,
844            chained_invoke_options: None,
845            context_options: None,
846        }
847    }
848
849    /// Creates a new OperationUpdate to mark an operation as failed.
850    pub fn fail(
851        operation_id: impl Into<String>,
852        operation_type: OperationType,
853        error: ErrorObject,
854    ) -> Self {
855        Self {
856            operation_id: operation_id.into(),
857            action: OperationAction::Fail,
858            operation_type,
859            result: None,
860            error: Some(error),
861            parent_id: None,
862            name: None,
863            sub_type: None,
864            wait_options: None,
865            step_options: None,
866            callback_options: None,
867            chained_invoke_options: None,
868            context_options: None,
869        }
870    }
871
872    /// Creates a new OperationUpdate to cancel an operation.
873    ///
874    /// This is primarily used for cancelling WAIT operations.
875    ///
876    /// # Arguments
877    ///
878    /// * `operation_id` - The ID of the operation to cancel
879    /// * `operation_type` - The type of operation being cancelled
880    pub fn cancel(operation_id: impl Into<String>, operation_type: OperationType) -> Self {
881        Self {
882            operation_id: operation_id.into(),
883            action: OperationAction::Cancel,
884            operation_type,
885            result: None,
886            error: None,
887            parent_id: None,
888            name: None,
889            sub_type: None,
890            wait_options: None,
891            step_options: None,
892            callback_options: None,
893            chained_invoke_options: None,
894            context_options: None,
895        }
896    }
897
898    /// Creates a new OperationUpdate to retry an operation with optional payload.
899    ///
900    /// This is used for the wait-for-condition pattern where state needs to be
901    /// passed between retry attempts. The payload contains the state to preserve
902    /// across retries, not an error.
903    ///
904    /// # Arguments
905    ///
906    /// * `operation_id` - The ID of the operation to retry
907    /// * `operation_type` - The type of operation being retried
908    /// * `payload` - Optional state payload to preserve across retries
909    /// * `next_attempt_delay_seconds` - Optional delay before the next retry attempt
910    pub fn retry(
911        operation_id: impl Into<String>,
912        operation_type: OperationType,
913        payload: Option<String>,
914        next_attempt_delay_seconds: Option<u64>,
915    ) -> Self {
916        Self {
917            operation_id: operation_id.into(),
918            action: OperationAction::Retry,
919            operation_type,
920            result: payload,
921            error: None,
922            parent_id: None,
923            name: None,
924            sub_type: None,
925            wait_options: None,
926            step_options: Some(StepOptions {
927                next_attempt_delay_seconds,
928            }),
929            callback_options: None,
930            chained_invoke_options: None,
931            context_options: None,
932        }
933    }
934
935    /// Creates a new OperationUpdate to retry an operation with an error.
936    ///
937    /// This is used for traditional retry scenarios where the operation failed
938    /// and needs to be retried after a delay.
939    ///
940    /// # Arguments
941    ///
942    /// * `operation_id` - The ID of the operation to retry
943    /// * `operation_type` - The type of operation being retried
944    /// * `error` - The error that caused the retry
945    /// * `next_attempt_delay_seconds` - Optional delay before the next retry attempt
946    pub fn retry_with_error(
947        operation_id: impl Into<String>,
948        operation_type: OperationType,
949        error: ErrorObject,
950        next_attempt_delay_seconds: Option<u64>,
951    ) -> Self {
952        Self {
953            operation_id: operation_id.into(),
954            action: OperationAction::Retry,
955            operation_type,
956            result: None,
957            error: Some(error),
958            parent_id: None,
959            name: None,
960            sub_type: None,
961            wait_options: None,
962            step_options: Some(StepOptions {
963                next_attempt_delay_seconds,
964            }),
965            callback_options: None,
966            chained_invoke_options: None,
967            context_options: None,
968        }
969    }
970
971    /// Sets the parent ID for this operation update.
972    pub fn with_parent_id(mut self, parent_id: impl Into<String>) -> Self {
973        self.parent_id = Some(parent_id.into());
974        self
975    }
976
977    /// Sets the name for this operation update.
978    pub fn with_name(mut self, name: impl Into<String>) -> Self {
979        self.name = Some(name.into());
980        self
981    }
982
983    /// Sets the sub-type for this operation update.
984    /// Requirements: 23.3, 23.4
985    pub fn with_sub_type(mut self, sub_type: impl Into<String>) -> Self {
986        self.sub_type = Some(sub_type.into());
987        self
988    }
989
990    /// Sets the wait options for this operation update.
991    pub fn with_wait_options(mut self, wait_seconds: u64) -> Self {
992        self.wait_options = Some(WaitOptions { wait_seconds });
993        self
994    }
995
996    /// Sets the step options for this operation update.
997    pub fn with_step_options(mut self, next_attempt_delay_seconds: Option<u64>) -> Self {
998        self.step_options = Some(StepOptions {
999            next_attempt_delay_seconds,
1000        });
1001        self
1002    }
1003
1004    /// Sets the callback options for this operation update.
1005    pub fn with_callback_options(
1006        mut self,
1007        timeout_seconds: Option<u64>,
1008        heartbeat_timeout_seconds: Option<u64>,
1009    ) -> Self {
1010        self.callback_options = Some(CallbackOptions {
1011            timeout_seconds,
1012            heartbeat_timeout_seconds,
1013        });
1014        self
1015    }
1016
1017    /// Sets the chained invoke options for this operation update.
1018    pub fn with_chained_invoke_options(
1019        mut self,
1020        function_name: impl Into<String>,
1021        tenant_id: Option<String>,
1022    ) -> Self {
1023        self.chained_invoke_options = Some(ChainedInvokeOptions {
1024            function_name: function_name.into(),
1025            tenant_id,
1026        });
1027        self
1028    }
1029
1030    /// Sets the context options for this operation update.
1031    pub fn with_context_options(mut self, replay_children: Option<bool>) -> Self {
1032        self.context_options = Some(ContextOptions { replay_children });
1033        self
1034    }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039    use super::*;
1040    use proptest::prelude::*;
1041
1042    // ============================================================================
1043    // Proptest Strategies
1044    // ============================================================================
1045
1046    /// Strategy for generating valid OperationType values
1047    /// Feature: rust-sdk-test-suite, Property 1: OperationType Serialization Round-Trip
1048    fn operation_type_strategy() -> impl Strategy<Value = OperationType> {
1049        prop_oneof![
1050            Just(OperationType::Execution),
1051            Just(OperationType::Step),
1052            Just(OperationType::Wait),
1053            Just(OperationType::Callback),
1054            Just(OperationType::Invoke),
1055            Just(OperationType::Context),
1056        ]
1057    }
1058
1059    /// Strategy for generating valid OperationStatus values
1060    /// Feature: rust-sdk-test-suite, Property 2: OperationStatus Serialization Round-Trip
1061    fn operation_status_strategy() -> impl Strategy<Value = OperationStatus> {
1062        prop_oneof![
1063            Just(OperationStatus::Started),
1064            Just(OperationStatus::Pending),
1065            Just(OperationStatus::Ready),
1066            Just(OperationStatus::Succeeded),
1067            Just(OperationStatus::Failed),
1068            Just(OperationStatus::Cancelled),
1069            Just(OperationStatus::TimedOut),
1070            Just(OperationStatus::Stopped),
1071        ]
1072    }
1073
1074    /// Strategy for generating valid OperationAction values
1075    /// Feature: rust-sdk-test-suite, Property 3: OperationAction Serialization Round-Trip
1076    fn operation_action_strategy() -> impl Strategy<Value = OperationAction> {
1077        prop_oneof![
1078            Just(OperationAction::Start),
1079            Just(OperationAction::Succeed),
1080            Just(OperationAction::Fail),
1081            Just(OperationAction::Cancel),
1082            Just(OperationAction::Retry),
1083        ]
1084    }
1085
1086    /// Strategy for generating non-empty strings (for IDs and names)
1087    fn non_empty_string_strategy() -> impl Strategy<Value = String> {
1088        "[a-zA-Z0-9_-]{1,64}".prop_map(|s| s)
1089    }
1090
1091    /// Strategy for generating optional strings
1092    fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
1093        prop_oneof![Just(None), non_empty_string_strategy().prop_map(Some),]
1094    }
1095
1096    /// Strategy for generating optional JSON result strings
1097    fn optional_result_strategy() -> impl Strategy<Value = Option<String>> {
1098        prop_oneof![
1099            Just(None),
1100            Just(Some(r#"{"value": 42}"#.to_string())),
1101            Just(Some(r#""simple string""#.to_string())),
1102            Just(Some("123".to_string())),
1103            Just(Some("true".to_string())),
1104            Just(Some("null".to_string())),
1105        ]
1106    }
1107
1108    /// Strategy for generating optional ErrorObject
1109    fn optional_error_strategy() -> impl Strategy<Value = Option<ErrorObject>> {
1110        prop_oneof![
1111            Just(None),
1112            (non_empty_string_strategy(), non_empty_string_strategy())
1113                .prop_map(|(error_type, message)| Some(ErrorObject::new(error_type, message))),
1114        ]
1115    }
1116
1117    /// Strategy for generating optional timestamps
1118    fn optional_timestamp_strategy() -> impl Strategy<Value = Option<i64>> {
1119        prop_oneof![
1120            Just(None),
1121            // Generate timestamps in a reasonable range (2020-2030)
1122            (1577836800000i64..1893456000000i64).prop_map(Some),
1123        ]
1124    }
1125
1126    /// Strategy for generating valid Operation instances
1127    /// Feature: rust-sdk-test-suite, Property 5: Operation Serialization Round-Trip
1128    fn operation_strategy() -> impl Strategy<Value = Operation> {
1129        (
1130            non_empty_string_strategy(),   // operation_id
1131            operation_type_strategy(),     // operation_type
1132            operation_status_strategy(),   // status
1133            optional_result_strategy(),    // result
1134            optional_error_strategy(),     // error
1135            optional_string_strategy(),    // parent_id
1136            optional_string_strategy(),    // name
1137            optional_string_strategy(),    // sub_type
1138            optional_timestamp_strategy(), // start_timestamp
1139            optional_timestamp_strategy(), // end_timestamp
1140        )
1141            .prop_map(
1142                |(
1143                    operation_id,
1144                    operation_type,
1145                    status,
1146                    result,
1147                    error,
1148                    parent_id,
1149                    name,
1150                    sub_type,
1151                    start_timestamp,
1152                    end_timestamp,
1153                )| {
1154                    Operation {
1155                        operation_id,
1156                        operation_type,
1157                        status,
1158                        result,
1159                        error,
1160                        parent_id,
1161                        name,
1162                        sub_type,
1163                        start_timestamp,
1164                        end_timestamp,
1165                        execution_details: None,
1166                        step_details: None,
1167                        wait_details: None,
1168                        callback_details: None,
1169                        chained_invoke_details: None,
1170                        context_details: None,
1171                    }
1172                },
1173            )
1174    }
1175
1176    // ============================================================================
1177    // Property-Based Tests
1178    // ============================================================================
1179
1180    proptest! {
1181        /// Feature: rust-sdk-test-suite, Property 1: OperationType Serialization Round-Trip
1182        /// For any OperationType value, serializing to JSON then deserializing SHALL produce the same value.
1183        /// **Validates: Requirements 2.1**
1184        #[test]
1185        fn prop_operation_type_serialization_round_trip(op_type in operation_type_strategy()) {
1186            let json = serde_json::to_string(&op_type).expect("serialization should succeed");
1187            let deserialized: OperationType = serde_json::from_str(&json).expect("deserialization should succeed");
1188            prop_assert_eq!(op_type, deserialized, "Round-trip failed for {:?}", op_type);
1189        }
1190
1191        /// Feature: rust-sdk-test-suite, Property 2: OperationStatus Serialization Round-Trip
1192        /// For any OperationStatus value, serializing to JSON then deserializing SHALL produce the same value.
1193        /// **Validates: Requirements 2.2**
1194        #[test]
1195        fn prop_operation_status_serialization_round_trip(status in operation_status_strategy()) {
1196            let json = serde_json::to_string(&status).expect("serialization should succeed");
1197            let deserialized: OperationStatus = serde_json::from_str(&json).expect("deserialization should succeed");
1198            prop_assert_eq!(status, deserialized, "Round-trip failed for {:?}", status);
1199        }
1200
1201        /// Feature: rust-sdk-test-suite, Property 3: OperationAction Serialization Round-Trip
1202        /// For any OperationAction value, serializing to JSON then deserializing SHALL produce the same value.
1203        /// **Validates: Requirements 2.3**
1204        #[test]
1205        fn prop_operation_action_serialization_round_trip(action in operation_action_strategy()) {
1206            let json = serde_json::to_string(&action).expect("serialization should succeed");
1207            let deserialized: OperationAction = serde_json::from_str(&json).expect("deserialization should succeed");
1208            prop_assert_eq!(action, deserialized, "Round-trip failed for {:?}", action);
1209        }
1210
1211        /// Feature: rust-sdk-test-suite, Property 4: Terminal Status Classification
1212        /// For any OperationStatus that is terminal (Succeeded, Failed, Cancelled, TimedOut, Stopped),
1213        /// is_terminal() SHALL return true, and for non-terminal statuses (Started, Pending, Ready),
1214        /// is_terminal() SHALL return false.
1215        /// **Validates: Requirements 2.4, 2.5**
1216        #[test]
1217        fn prop_terminal_status_classification(status in operation_status_strategy()) {
1218            let is_terminal = status.is_terminal();
1219            let expected_terminal = matches!(
1220                status,
1221                OperationStatus::Succeeded
1222                    | OperationStatus::Failed
1223                    | OperationStatus::Cancelled
1224                    | OperationStatus::TimedOut
1225                    | OperationStatus::Stopped
1226            );
1227            prop_assert_eq!(
1228                is_terminal, expected_terminal,
1229                "Terminal classification mismatch for {:?}: got {}, expected {}",
1230                status, is_terminal, expected_terminal
1231            );
1232        }
1233
1234        /// Feature: rust-sdk-test-suite, Property 5: Operation Serialization Round-Trip
1235        /// For any Operation instance with valid fields, serializing to JSON then deserializing
1236        /// SHALL produce an equivalent Operation.
1237        /// **Validates: Requirements 2.6, 9.2**
1238        #[test]
1239        fn prop_operation_serialization_round_trip(op in operation_strategy()) {
1240            let json = serde_json::to_string(&op).expect("serialization should succeed");
1241            let deserialized: Operation = serde_json::from_str(&json).expect("deserialization should succeed");
1242
1243            // Compare all fields
1244            prop_assert_eq!(op.operation_id, deserialized.operation_id, "operation_id mismatch");
1245            prop_assert_eq!(op.operation_type, deserialized.operation_type, "operation_type mismatch");
1246            prop_assert_eq!(op.status, deserialized.status, "status mismatch");
1247            prop_assert_eq!(op.result, deserialized.result, "result mismatch");
1248            prop_assert_eq!(op.parent_id, deserialized.parent_id, "parent_id mismatch");
1249            prop_assert_eq!(op.name, deserialized.name, "name mismatch");
1250            prop_assert_eq!(op.sub_type, deserialized.sub_type, "sub_type mismatch");
1251            prop_assert_eq!(op.start_timestamp, deserialized.start_timestamp, "start_timestamp mismatch");
1252            prop_assert_eq!(op.end_timestamp, deserialized.end_timestamp, "end_timestamp mismatch");
1253
1254            // Compare error if present
1255            match (&op.error, &deserialized.error) {
1256                (Some(e1), Some(e2)) => {
1257                    prop_assert_eq!(&e1.error_type, &e2.error_type, "error_type mismatch");
1258                    prop_assert_eq!(&e1.error_message, &e2.error_message, "error_message mismatch");
1259                }
1260                (None, None) => {}
1261                _ => prop_assert!(false, "error presence mismatch"),
1262            }
1263        }
1264    }
1265
1266    // ============================================================================
1267    // Unit Tests
1268    // ============================================================================
1269
1270    #[test]
1271    fn test_operation_new() {
1272        let op = Operation::new("op-123", OperationType::Step);
1273        assert_eq!(op.operation_id, "op-123");
1274        assert_eq!(op.operation_type, OperationType::Step);
1275        assert_eq!(op.status, OperationStatus::Started);
1276        assert!(op.result.is_none());
1277        assert!(op.error.is_none());
1278        assert!(op.parent_id.is_none());
1279        assert!(op.name.is_none());
1280    }
1281
1282    #[test]
1283    fn test_operation_with_parent_and_name() {
1284        let op = Operation::new("op-123", OperationType::Step)
1285            .with_parent_id("parent-456")
1286            .with_name("my-step");
1287        assert_eq!(op.parent_id, Some("parent-456".to_string()));
1288        assert_eq!(op.name, Some("my-step".to_string()));
1289    }
1290
1291    #[test]
1292    fn test_operation_is_completed() {
1293        let mut op = Operation::new("op-123", OperationType::Step);
1294        assert!(!op.is_completed());
1295
1296        op.status = OperationStatus::Succeeded;
1297        assert!(op.is_completed());
1298
1299        op.status = OperationStatus::Failed;
1300        assert!(op.is_completed());
1301
1302        op.status = OperationStatus::Cancelled;
1303        assert!(op.is_completed());
1304
1305        op.status = OperationStatus::TimedOut;
1306        assert!(op.is_completed());
1307
1308        op.status = OperationStatus::Stopped;
1309        assert!(op.is_completed());
1310    }
1311
1312    #[test]
1313    fn test_operation_is_succeeded() {
1314        let mut op = Operation::new("op-123", OperationType::Step);
1315        assert!(!op.is_succeeded());
1316
1317        op.status = OperationStatus::Succeeded;
1318        assert!(op.is_succeeded());
1319
1320        op.status = OperationStatus::Failed;
1321        assert!(!op.is_succeeded());
1322    }
1323
1324    #[test]
1325    fn test_operation_is_failed() {
1326        let mut op = Operation::new("op-123", OperationType::Step);
1327        assert!(!op.is_failed());
1328
1329        op.status = OperationStatus::Failed;
1330        assert!(op.is_failed());
1331
1332        op.status = OperationStatus::Cancelled;
1333        assert!(op.is_failed());
1334
1335        op.status = OperationStatus::TimedOut;
1336        assert!(op.is_failed());
1337
1338        op.status = OperationStatus::Succeeded;
1339        assert!(!op.is_failed());
1340    }
1341
1342    #[test]
1343    fn test_operation_type_display() {
1344        assert_eq!(OperationType::Execution.to_string(), "Execution");
1345        assert_eq!(OperationType::Step.to_string(), "Step");
1346        assert_eq!(OperationType::Wait.to_string(), "Wait");
1347        assert_eq!(OperationType::Callback.to_string(), "Callback");
1348        assert_eq!(OperationType::Invoke.to_string(), "Invoke");
1349        assert_eq!(OperationType::Context.to_string(), "Context");
1350    }
1351
1352    #[test]
1353    fn test_operation_status_is_terminal() {
1354        assert!(!OperationStatus::Started.is_terminal());
1355        assert!(!OperationStatus::Pending.is_terminal());
1356        assert!(!OperationStatus::Ready.is_terminal());
1357        assert!(OperationStatus::Succeeded.is_terminal());
1358        assert!(OperationStatus::Failed.is_terminal());
1359        assert!(OperationStatus::Cancelled.is_terminal());
1360        assert!(OperationStatus::TimedOut.is_terminal());
1361        assert!(OperationStatus::Stopped.is_terminal());
1362    }
1363
1364    #[test]
1365    fn test_operation_status_is_success() {
1366        assert!(!OperationStatus::Started.is_success());
1367        assert!(!OperationStatus::Pending.is_success());
1368        assert!(!OperationStatus::Ready.is_success());
1369        assert!(OperationStatus::Succeeded.is_success());
1370        assert!(!OperationStatus::Failed.is_success());
1371    }
1372
1373    #[test]
1374    fn test_operation_status_is_failure() {
1375        assert!(!OperationStatus::Started.is_failure());
1376        assert!(!OperationStatus::Pending.is_failure());
1377        assert!(!OperationStatus::Ready.is_failure());
1378        assert!(!OperationStatus::Succeeded.is_failure());
1379        assert!(OperationStatus::Failed.is_failure());
1380        assert!(OperationStatus::Cancelled.is_failure());
1381        assert!(OperationStatus::TimedOut.is_failure());
1382        assert!(OperationStatus::Stopped.is_failure());
1383    }
1384
1385    #[test]
1386    fn test_operation_status_is_pending() {
1387        assert!(!OperationStatus::Started.is_pending());
1388        assert!(OperationStatus::Pending.is_pending());
1389        assert!(!OperationStatus::Ready.is_pending());
1390        assert!(!OperationStatus::Succeeded.is_pending());
1391        assert!(!OperationStatus::Failed.is_pending());
1392    }
1393
1394    #[test]
1395    fn test_operation_status_is_ready() {
1396        assert!(!OperationStatus::Started.is_ready());
1397        assert!(!OperationStatus::Pending.is_ready());
1398        assert!(OperationStatus::Ready.is_ready());
1399        assert!(!OperationStatus::Succeeded.is_ready());
1400        assert!(!OperationStatus::Failed.is_ready());
1401    }
1402
1403    #[test]
1404    fn test_operation_status_is_resumable() {
1405        assert!(OperationStatus::Started.is_resumable());
1406        assert!(OperationStatus::Pending.is_resumable());
1407        assert!(OperationStatus::Ready.is_resumable());
1408        assert!(!OperationStatus::Succeeded.is_resumable());
1409        assert!(!OperationStatus::Failed.is_resumable());
1410        assert!(!OperationStatus::Cancelled.is_resumable());
1411        assert!(!OperationStatus::TimedOut.is_resumable());
1412        assert!(!OperationStatus::Stopped.is_resumable());
1413    }
1414
1415    #[test]
1416    fn test_operation_update_start() {
1417        let update = OperationUpdate::start("op-123", OperationType::Step);
1418        assert_eq!(update.operation_id, "op-123");
1419        assert_eq!(update.action, OperationAction::Start);
1420        assert_eq!(update.operation_type, OperationType::Step);
1421        assert!(update.result.is_none());
1422        assert!(update.error.is_none());
1423    }
1424
1425    #[test]
1426    fn test_operation_update_succeed() {
1427        let update = OperationUpdate::succeed(
1428            "op-123",
1429            OperationType::Step,
1430            Some(r#"{"value": 42}"#.to_string()),
1431        );
1432        assert_eq!(update.operation_id, "op-123");
1433        assert_eq!(update.action, OperationAction::Succeed);
1434        assert_eq!(update.result, Some(r#"{"value": 42}"#.to_string()));
1435        assert!(update.error.is_none());
1436    }
1437
1438    #[test]
1439    fn test_operation_update_fail() {
1440        let error = ErrorObject::new("TestError", "Something went wrong");
1441        let update = OperationUpdate::fail("op-123", OperationType::Step, error);
1442        assert_eq!(update.operation_id, "op-123");
1443        assert_eq!(update.action, OperationAction::Fail);
1444        assert!(update.result.is_none());
1445        assert!(update.error.is_some());
1446        assert_eq!(update.error.as_ref().unwrap().error_type, "TestError");
1447    }
1448
1449    #[test]
1450    fn test_operation_update_with_parent_and_name() {
1451        let update = OperationUpdate::start("op-123", OperationType::Step)
1452            .with_parent_id("parent-456")
1453            .with_name("my-step");
1454        assert_eq!(update.parent_id, Some("parent-456".to_string()));
1455        assert_eq!(update.name, Some("my-step".to_string()));
1456    }
1457
1458    #[test]
1459    fn test_operation_serialization() {
1460        let op = Operation::new("op-123", OperationType::Step)
1461            .with_parent_id("parent-456")
1462            .with_name("my-step");
1463
1464        let json = serde_json::to_string(&op).unwrap();
1465        assert!(json.contains("\"Id\":\"op-123\""));
1466        assert!(json.contains("\"Type\":\"STEP\""));
1467        assert!(json.contains("\"Status\":\"STARTED\""));
1468        assert!(json.contains("\"ParentId\":\"parent-456\""));
1469        assert!(json.contains("\"Name\":\"my-step\""));
1470    }
1471
1472    #[test]
1473    fn test_operation_deserialization() {
1474        // Test with new API field names (Id, Type)
1475        let json = r#"{
1476            "Id": "op-123",
1477            "Type": "STEP",
1478            "Status": "SUCCEEDED",
1479            "Result": "{\"value\": 42}",
1480            "ParentId": "parent-456",
1481            "Name": "my-step"
1482        }"#;
1483
1484        let op: Operation = serde_json::from_str(json).unwrap();
1485        assert_eq!(op.operation_id, "op-123");
1486        assert_eq!(op.operation_type, OperationType::Step);
1487        assert_eq!(op.status, OperationStatus::Succeeded);
1488        assert_eq!(op.result, Some(r#"{"value": 42}"#.to_string()));
1489        assert_eq!(op.parent_id, Some("parent-456".to_string()));
1490        assert_eq!(op.name, Some("my-step".to_string()));
1491    }
1492
1493    #[test]
1494    fn test_operation_deserialization_legacy_field_names() {
1495        // Test with legacy field names (OperationId, OperationType) for backward compatibility
1496        let json = r#"{
1497            "OperationId": "op-123",
1498            "OperationType": "STEP",
1499            "Status": "SUCCEEDED",
1500            "Result": "{\"value\": 42}",
1501            "ParentId": "parent-456",
1502            "Name": "my-step"
1503        }"#;
1504
1505        let op: Operation = serde_json::from_str(json).unwrap();
1506        assert_eq!(op.operation_id, "op-123");
1507        assert_eq!(op.operation_type, OperationType::Step);
1508        assert_eq!(op.status, OperationStatus::Succeeded);
1509    }
1510
1511    #[test]
1512    fn test_operation_deserialization_with_timestamps() {
1513        // Test with timestamps and execution details (as sent by the API)
1514        let json = r#"{
1515            "Id": "778f03ea-ab5a-3e77-8d6d-9119253f8565",
1516            "Name": "21e26aa2-4866-4c09-958a-15a272f16c87",
1517            "Type": "EXECUTION",
1518            "StartTimestamp": 1767896523358,
1519            "Status": "STARTED",
1520            "ExecutionDetails": {
1521                "InputPayload": "{\"order_id\":\"order-122342134\"}"
1522            }
1523        }"#;
1524
1525        let op: Operation = serde_json::from_str(json).unwrap();
1526        assert_eq!(op.operation_id, "778f03ea-ab5a-3e77-8d6d-9119253f8565");
1527        assert_eq!(op.operation_type, OperationType::Execution);
1528        assert_eq!(op.status, OperationStatus::Started);
1529        assert_eq!(op.start_timestamp, Some(1767896523358));
1530        assert!(op.execution_details.is_some());
1531        let details = op.execution_details.unwrap();
1532        assert!(details.input_payload.is_some());
1533    }
1534
1535    #[test]
1536    fn test_operation_update_serialization() {
1537        let update = OperationUpdate::succeed(
1538            "op-123",
1539            OperationType::Step,
1540            Some(r#"{"value": 42}"#.to_string()),
1541        )
1542        .with_parent_id("parent-456");
1543
1544        let json = serde_json::to_string(&update).unwrap();
1545        assert!(json.contains("\"Id\":\"op-123\""));
1546        assert!(json.contains("\"Action\":\"SUCCEED\""));
1547        assert!(json.contains("\"Type\":\"STEP\""));
1548        assert!(json.contains("\"Payload\":\"{\\\"value\\\": 42}\""));
1549        assert!(json.contains("\"ParentId\":\"parent-456\""));
1550    }
1551
1552    #[test]
1553    fn test_operation_status_pending_serialization() {
1554        // Test PENDING status serialization/deserialization
1555        let json = r#"{
1556            "Id": "op-123",
1557            "Type": "STEP",
1558            "Status": "PENDING"
1559        }"#;
1560
1561        let op: Operation = serde_json::from_str(json).unwrap();
1562        assert_eq!(op.status, OperationStatus::Pending);
1563        assert!(op.status.is_pending());
1564        assert!(!op.status.is_terminal());
1565        assert!(op.status.is_resumable());
1566    }
1567
1568    #[test]
1569    fn test_operation_status_ready_serialization() {
1570        // Test READY status serialization/deserialization
1571        let json = r#"{
1572            "Id": "op-123",
1573            "Type": "STEP",
1574            "Status": "READY"
1575        }"#;
1576
1577        let op: Operation = serde_json::from_str(json).unwrap();
1578        assert_eq!(op.status, OperationStatus::Ready);
1579        assert!(op.status.is_ready());
1580        assert!(!op.status.is_terminal());
1581        assert!(op.status.is_resumable());
1582    }
1583
1584    #[test]
1585    fn test_operation_status_display() {
1586        assert_eq!(OperationStatus::Started.to_string(), "Started");
1587        assert_eq!(OperationStatus::Pending.to_string(), "Pending");
1588        assert_eq!(OperationStatus::Ready.to_string(), "Ready");
1589        assert_eq!(OperationStatus::Succeeded.to_string(), "Succeeded");
1590        assert_eq!(OperationStatus::Failed.to_string(), "Failed");
1591        assert_eq!(OperationStatus::Cancelled.to_string(), "Cancelled");
1592        assert_eq!(OperationStatus::TimedOut.to_string(), "TimedOut");
1593        assert_eq!(OperationStatus::Stopped.to_string(), "Stopped");
1594    }
1595
1596    #[test]
1597    fn test_operation_with_sub_type() {
1598        let op = Operation::new("op-123", OperationType::Context).with_sub_type("map");
1599        assert_eq!(op.sub_type, Some("map".to_string()));
1600    }
1601
1602    #[test]
1603    fn test_operation_update_with_sub_type() {
1604        let update =
1605            OperationUpdate::start("op-123", OperationType::Context).with_sub_type("parallel");
1606        assert_eq!(update.sub_type, Some("parallel".to_string()));
1607    }
1608
1609    #[test]
1610    fn test_operation_sub_type_serialization() {
1611        let op =
1612            Operation::new("op-123", OperationType::Context).with_sub_type("wait_for_condition");
1613
1614        let json = serde_json::to_string(&op).unwrap();
1615        assert!(json.contains("\"SubType\":\"wait_for_condition\""));
1616    }
1617
1618    #[test]
1619    fn test_operation_sub_type_deserialization() {
1620        let json = r#"{
1621            "Id": "op-123",
1622            "Type": "CONTEXT",
1623            "Status": "STARTED",
1624            "SubType": "map"
1625        }"#;
1626
1627        let op: Operation = serde_json::from_str(json).unwrap();
1628        assert_eq!(op.sub_type, Some("map".to_string()));
1629    }
1630
1631    #[test]
1632    fn test_operation_metadata_fields() {
1633        // Test that start_timestamp and end_timestamp are properly deserialized
1634        let json = r#"{
1635            "Id": "op-123",
1636            "Type": "STEP",
1637            "Status": "SUCCEEDED",
1638            "StartTimestamp": 1704067200000,
1639            "EndTimestamp": 1704067260000,
1640            "Name": "my-step",
1641            "SubType": "custom"
1642        }"#;
1643
1644        let op: Operation = serde_json::from_str(json).unwrap();
1645        assert_eq!(op.start_timestamp, Some(1704067200000));
1646        assert_eq!(op.end_timestamp, Some(1704067260000));
1647        assert_eq!(op.name, Some("my-step".to_string()));
1648        assert_eq!(op.sub_type, Some("custom".to_string()));
1649    }
1650
1651    #[test]
1652    fn test_operation_action_retry_display() {
1653        assert_eq!(OperationAction::Retry.to_string(), "Retry");
1654    }
1655
1656    #[test]
1657    fn test_operation_update_retry_with_payload() {
1658        let update = OperationUpdate::retry(
1659            "op-123",
1660            OperationType::Step,
1661            Some(r#"{"state": "waiting"}"#.to_string()),
1662            Some(5),
1663        );
1664        assert_eq!(update.operation_id, "op-123");
1665        assert_eq!(update.action, OperationAction::Retry);
1666        assert_eq!(update.operation_type, OperationType::Step);
1667        assert_eq!(update.result, Some(r#"{"state": "waiting"}"#.to_string()));
1668        assert!(update.error.is_none());
1669        assert!(update.step_options.is_some());
1670        assert_eq!(
1671            update
1672                .step_options
1673                .as_ref()
1674                .unwrap()
1675                .next_attempt_delay_seconds,
1676            Some(5)
1677        );
1678    }
1679
1680    #[test]
1681    fn test_operation_update_retry_with_error() {
1682        let error = ErrorObject::new("RetryableError", "Temporary failure");
1683        let update =
1684            OperationUpdate::retry_with_error("op-123", OperationType::Step, error, Some(10));
1685        assert_eq!(update.operation_id, "op-123");
1686        assert_eq!(update.action, OperationAction::Retry);
1687        assert!(update.result.is_none());
1688        assert!(update.error.is_some());
1689        assert_eq!(update.error.as_ref().unwrap().error_type, "RetryableError");
1690        assert_eq!(
1691            update
1692                .step_options
1693                .as_ref()
1694                .unwrap()
1695                .next_attempt_delay_seconds,
1696            Some(10)
1697        );
1698    }
1699
1700    #[test]
1701    fn test_operation_update_retry_serialization() {
1702        let update = OperationUpdate::retry(
1703            "op-123",
1704            OperationType::Step,
1705            Some(r#"{"counter": 5}"#.to_string()),
1706            Some(3),
1707        );
1708
1709        let json = serde_json::to_string(&update).unwrap();
1710        assert!(json.contains("\"Action\":\"RETRY\""));
1711        assert!(json.contains("\"Payload\":\"{\\\"counter\\\": 5}\""));
1712        assert!(json.contains("\"NextAttemptDelaySeconds\":3"));
1713    }
1714
1715    #[test]
1716    fn test_step_details_with_payload() {
1717        let json = r#"{
1718            "Id": "op-123",
1719            "Type": "STEP",
1720            "Status": "PENDING",
1721            "StepDetails": {
1722                "Attempt": 2,
1723                "Payload": "{\"state\": \"processing\"}"
1724            }
1725        }"#;
1726
1727        let op: Operation = serde_json::from_str(json).unwrap();
1728        assert_eq!(op.status, OperationStatus::Pending);
1729        assert!(op.step_details.is_some());
1730        let details = op.step_details.as_ref().unwrap();
1731        assert_eq!(details.attempt, Some(2));
1732        assert_eq!(
1733            details.payload,
1734            Some(r#"{"state": "processing"}"#.to_string())
1735        );
1736    }
1737
1738    #[test]
1739    fn test_operation_get_retry_payload() {
1740        let mut op = Operation::new("op-123", OperationType::Step);
1741        op.step_details = Some(StepDetails {
1742            result: None,
1743            attempt: Some(1),
1744            next_attempt_timestamp: None,
1745            error: None,
1746            payload: Some(r#"{"counter": 3}"#.to_string()),
1747        });
1748
1749        assert_eq!(op.get_retry_payload(), Some(r#"{"counter": 3}"#));
1750    }
1751
1752    #[test]
1753    fn test_operation_get_attempt() {
1754        let mut op = Operation::new("op-123", OperationType::Step);
1755        op.step_details = Some(StepDetails {
1756            result: None,
1757            attempt: Some(5),
1758            next_attempt_timestamp: None,
1759            error: None,
1760            payload: None,
1761        });
1762
1763        assert_eq!(op.get_attempt(), Some(5));
1764    }
1765
1766    #[test]
1767    fn test_operation_get_attempt_no_details() {
1768        let op = Operation::new("op-123", OperationType::Step);
1769        assert_eq!(op.get_attempt(), None);
1770    }
1771
1772    #[test]
1773    fn test_operation_get_retry_payload_wrong_type() {
1774        let op = Operation::new("op-123", OperationType::Wait);
1775        assert_eq!(op.get_retry_payload(), None);
1776    }
1777
1778    // Size verification tests for enum discriminant optimization
1779    // Requirements: 6.7 - Verify each enum is 1 byte after optimization
1780
1781    #[test]
1782    fn test_operation_status_size_is_one_byte() {
1783        assert_eq!(
1784            std::mem::size_of::<OperationStatus>(),
1785            1,
1786            "OperationStatus should be 1 byte with #[repr(u8)]"
1787        );
1788    }
1789
1790    #[test]
1791    fn test_operation_type_size_is_one_byte() {
1792        assert_eq!(
1793            std::mem::size_of::<OperationType>(),
1794            1,
1795            "OperationType should be 1 byte with #[repr(u8)]"
1796        );
1797    }
1798
1799    #[test]
1800    fn test_operation_action_size_is_one_byte() {
1801        assert_eq!(
1802            std::mem::size_of::<OperationAction>(),
1803            1,
1804            "OperationAction should be 1 byte with #[repr(u8)]"
1805        );
1806    }
1807
1808    // Serde compatibility tests for enum discriminant optimization
1809    // Requirements: 6.6 - Verify JSON serialization uses string representations
1810
1811    #[test]
1812    fn test_operation_status_serde_uses_string_representation() {
1813        // Verify serialization produces string values, not numeric discriminants
1814        let status = OperationStatus::Started;
1815        let json = serde_json::to_string(&status).unwrap();
1816        assert_eq!(json, "\"STARTED\"");
1817
1818        let status = OperationStatus::Pending;
1819        let json = serde_json::to_string(&status).unwrap();
1820        assert_eq!(json, "\"PENDING\"");
1821
1822        let status = OperationStatus::Ready;
1823        let json = serde_json::to_string(&status).unwrap();
1824        assert_eq!(json, "\"READY\"");
1825
1826        let status = OperationStatus::Succeeded;
1827        let json = serde_json::to_string(&status).unwrap();
1828        assert_eq!(json, "\"SUCCEEDED\"");
1829
1830        let status = OperationStatus::Failed;
1831        let json = serde_json::to_string(&status).unwrap();
1832        assert_eq!(json, "\"FAILED\"");
1833
1834        let status = OperationStatus::Cancelled;
1835        let json = serde_json::to_string(&status).unwrap();
1836        assert_eq!(json, "\"CANCELLED\"");
1837
1838        let status = OperationStatus::TimedOut;
1839        let json = serde_json::to_string(&status).unwrap();
1840        assert_eq!(json, "\"TIMED_OUT\"");
1841
1842        let status = OperationStatus::Stopped;
1843        let json = serde_json::to_string(&status).unwrap();
1844        assert_eq!(json, "\"STOPPED\"");
1845    }
1846
1847    #[test]
1848    fn test_operation_status_serde_round_trip() {
1849        let statuses = [
1850            OperationStatus::Started,
1851            OperationStatus::Pending,
1852            OperationStatus::Ready,
1853            OperationStatus::Succeeded,
1854            OperationStatus::Failed,
1855            OperationStatus::Cancelled,
1856            OperationStatus::TimedOut,
1857            OperationStatus::Stopped,
1858        ];
1859
1860        for status in statuses {
1861            let json = serde_json::to_string(&status).unwrap();
1862            let deserialized: OperationStatus = serde_json::from_str(&json).unwrap();
1863            assert_eq!(status, deserialized, "Round-trip failed for {:?}", status);
1864        }
1865    }
1866
1867    #[test]
1868    fn test_operation_type_serde_uses_string_representation() {
1869        // Verify serialization produces string values, not numeric discriminants
1870        let op_type = OperationType::Execution;
1871        let json = serde_json::to_string(&op_type).unwrap();
1872        assert_eq!(json, "\"EXECUTION\"");
1873
1874        let op_type = OperationType::Step;
1875        let json = serde_json::to_string(&op_type).unwrap();
1876        assert_eq!(json, "\"STEP\"");
1877
1878        let op_type = OperationType::Wait;
1879        let json = serde_json::to_string(&op_type).unwrap();
1880        assert_eq!(json, "\"WAIT\"");
1881
1882        let op_type = OperationType::Callback;
1883        let json = serde_json::to_string(&op_type).unwrap();
1884        assert_eq!(json, "\"CALLBACK\"");
1885
1886        let op_type = OperationType::Invoke;
1887        let json = serde_json::to_string(&op_type).unwrap();
1888        assert_eq!(json, "\"INVOKE\"");
1889
1890        let op_type = OperationType::Context;
1891        let json = serde_json::to_string(&op_type).unwrap();
1892        assert_eq!(json, "\"CONTEXT\"");
1893    }
1894
1895    #[test]
1896    fn test_operation_type_serde_round_trip() {
1897        let types = [
1898            OperationType::Execution,
1899            OperationType::Step,
1900            OperationType::Wait,
1901            OperationType::Callback,
1902            OperationType::Invoke,
1903            OperationType::Context,
1904        ];
1905
1906        for op_type in types {
1907            let json = serde_json::to_string(&op_type).unwrap();
1908            let deserialized: OperationType = serde_json::from_str(&json).unwrap();
1909            assert_eq!(op_type, deserialized, "Round-trip failed for {:?}", op_type);
1910        }
1911    }
1912
1913    #[test]
1914    fn test_operation_action_serde_uses_string_representation() {
1915        // Verify serialization produces string values, not numeric discriminants
1916        let action = OperationAction::Start;
1917        let json = serde_json::to_string(&action).unwrap();
1918        assert_eq!(json, "\"START\"");
1919
1920        let action = OperationAction::Succeed;
1921        let json = serde_json::to_string(&action).unwrap();
1922        assert_eq!(json, "\"SUCCEED\"");
1923
1924        let action = OperationAction::Fail;
1925        let json = serde_json::to_string(&action).unwrap();
1926        assert_eq!(json, "\"FAIL\"");
1927
1928        let action = OperationAction::Cancel;
1929        let json = serde_json::to_string(&action).unwrap();
1930        assert_eq!(json, "\"CANCEL\"");
1931
1932        let action = OperationAction::Retry;
1933        let json = serde_json::to_string(&action).unwrap();
1934        assert_eq!(json, "\"RETRY\"");
1935    }
1936
1937    #[test]
1938    fn test_operation_action_serde_round_trip() {
1939        let actions = [
1940            OperationAction::Start,
1941            OperationAction::Succeed,
1942            OperationAction::Fail,
1943            OperationAction::Cancel,
1944            OperationAction::Retry,
1945        ];
1946
1947        for action in actions {
1948            let json = serde_json::to_string(&action).unwrap();
1949            let deserialized: OperationAction = serde_json::from_str(&json).unwrap();
1950            assert_eq!(action, deserialized, "Round-trip failed for {:?}", action);
1951        }
1952    }
1953
1954    // Timestamp parsing tests
1955
1956    #[test]
1957    fn test_parse_iso8601_rfc3339_format() {
1958        // Standard RFC 3339 format
1959        let result = parse_iso8601_to_millis("2026-01-13T04:10:18.841+00:00");
1960        assert!(result.is_ok(), "Failed to parse: {:?}", result);
1961        let millis = result.unwrap();
1962        // 2026-01-13T04:10:18.841Z - verify it's a reasonable timestamp
1963        // January 13, 2026 is in the future, so millis should be > current time
1964        // Let's just verify it parsed to a positive value and is in a reasonable range
1965        assert!(millis > 0, "Timestamp should be positive, got {}", millis);
1966        // Should be after year 2020 (1577836800000) and before year 2100 (4102444800000)
1967        assert!(
1968            millis > 1577836800000 && millis < 4102444800000,
1969            "Timestamp {} is outside reasonable range",
1970            millis
1971        );
1972    }
1973
1974    #[test]
1975    fn test_parse_iso8601_with_space_separator() {
1976        // Format with space instead of T (common in some systems)
1977        let result = parse_iso8601_to_millis("2026-01-13 04:10:18.841055+00:00");
1978        assert!(result.is_ok());
1979    }
1980
1981    #[test]
1982    fn test_parse_iso8601_without_timezone() {
1983        // Naive datetime (assumes UTC)
1984        let result = parse_iso8601_to_millis("2026-01-13T04:10:18.841");
1985        assert!(result.is_ok());
1986    }
1987
1988    #[test]
1989    fn test_parse_iso8601_without_fractional_seconds() {
1990        // No fractional seconds
1991        let result = parse_iso8601_to_millis("2026-01-13T04:10:18+00:00");
1992        assert!(result.is_ok());
1993    }
1994
1995    #[test]
1996    fn test_parse_iso8601_invalid_format() {
1997        // Invalid format should return error
1998        let result = parse_iso8601_to_millis("not-a-timestamp");
1999        assert!(result.is_err());
2000    }
2001
2002    #[test]
2003    fn test_timestamp_deserialization_integer() {
2004        let json = r#"{
2005            "Id": "op-123",
2006            "Type": "STEP",
2007            "Status": "STARTED",
2008            "StartTimestamp": 1768279818841
2009        }"#;
2010
2011        let op: Operation = serde_json::from_str(json).unwrap();
2012        assert_eq!(op.start_timestamp, Some(1768279818841));
2013    }
2014
2015    #[test]
2016    fn test_timestamp_deserialization_float() {
2017        // Floating point timestamp (seconds with fractional milliseconds)
2018        let json = r#"{
2019            "Id": "op-123",
2020            "Type": "STEP",
2021            "Status": "STARTED",
2022            "StartTimestamp": 1768279818.841
2023        }"#;
2024
2025        let op: Operation = serde_json::from_str(json).unwrap();
2026        // Should be converted to milliseconds
2027        assert_eq!(op.start_timestamp, Some(1768279818841));
2028    }
2029
2030    #[test]
2031    fn test_timestamp_deserialization_iso8601_string() {
2032        let json = r#"{
2033            "Id": "op-123",
2034            "Type": "STEP",
2035            "Status": "STARTED",
2036            "StartTimestamp": "2026-01-13T04:10:18.841+00:00"
2037        }"#;
2038
2039        let op: Operation = serde_json::from_str(json).unwrap();
2040        assert!(op.start_timestamp.is_some());
2041        let ts = op.start_timestamp.unwrap();
2042        // Should be a reasonable timestamp (after 2020, before 2100)
2043        assert!(
2044            ts > 1577836800000 && ts < 4102444800000,
2045            "Timestamp {} is outside reasonable range",
2046            ts
2047        );
2048    }
2049
2050    #[test]
2051    fn test_timestamp_deserialization_null() {
2052        let json = r#"{
2053            "Id": "op-123",
2054            "Type": "STEP",
2055            "Status": "STARTED",
2056            "StartTimestamp": null
2057        }"#;
2058
2059        let op: Operation = serde_json::from_str(json).unwrap();
2060        assert!(op.start_timestamp.is_none());
2061    }
2062
2063    #[test]
2064    fn test_timestamp_deserialization_missing() {
2065        let json = r#"{
2066            "Id": "op-123",
2067            "Type": "STEP",
2068            "Status": "STARTED"
2069        }"#;
2070
2071        let op: Operation = serde_json::from_str(json).unwrap();
2072        assert!(op.start_timestamp.is_none());
2073    }
2074}