Skip to main content

durable_execution_sdk/
config.rs

1//! Configuration types for durable execution operations.
2//!
3//! This module provides type-safe configuration structs for all
4//! durable operations including steps, callbacks, invocations,
5//! map, and parallel operations.
6//!
7//! ## Performance Configuration
8//!
9//! The SDK supports different checkpointing modes that trade off between
10//! durability and performance. See [`CheckpointingMode`] for details.
11//!
12//! ## Sealed Traits
13//!
14//! The `RetryStrategy` trait is sealed and cannot be implemented outside of this crate.
15//! This allows the SDK maintainers to evolve the retry interface without breaking
16//! external code.
17
18use std::marker::PhantomData;
19use std::sync::Arc;
20
21use blake2::{Blake2b512, Digest};
22use serde::{Deserialize, Serialize};
23
24use crate::duration::Duration;
25use crate::error::DurableError;
26use crate::sealed::Sealed;
27
28/// Jitter strategy for retry delays.
29///
30/// Jitter adds randomness to retry delays to prevent thundering herd problems
31/// when many executions retry simultaneously.
32///
33/// # Variants
34///
35/// - `None` — Use the exact calculated delay (no jitter).
36/// - `Full` — Random delay in `[0, calculated_delay]`.
37/// - `Half` — Random delay in `[calculated_delay/2, calculated_delay]`.
38///
39/// # Example
40///
41/// ```
42/// use durable_execution_sdk::config::JitterStrategy;
43///
44/// let none = JitterStrategy::None;
45/// assert_eq!(none.apply(10.0, 1), 10.0);
46///
47/// let full = JitterStrategy::Full;
48/// let jittered = full.apply(10.0, 1);
49/// assert!(jittered >= 0.0 && jittered <= 10.0);
50///
51/// let half = JitterStrategy::Half;
52/// let jittered = half.apply(10.0, 1);
53/// assert!(jittered >= 5.0 && jittered <= 10.0);
54/// ```
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum JitterStrategy {
57    /// No jitter — use exact calculated delay.
58    #[default]
59    None,
60    /// Full jitter — random delay in [0, calculated_delay].
61    Full,
62    /// Half jitter — random delay in [calculated_delay/2, calculated_delay].
63    Half,
64}
65
66impl JitterStrategy {
67    /// Applies jitter to a delay value in seconds.
68    ///
69    /// Uses a deterministic seed derived from the attempt number via blake2b
70    /// hashing. This makes jitter replay-safe since the same attempt always
71    /// produces the same jittered value.
72    ///
73    /// # Arguments
74    ///
75    /// * `delay_secs` - The base delay in seconds
76    /// * `attempt` - The retry attempt number (used as seed for deterministic randomness)
77    ///
78    /// # Returns
79    ///
80    /// The jittered delay in seconds:
81    /// - `None`: returns `delay_secs` exactly
82    /// - `Full`: returns a value in `[0, delay_secs]`
83    /// - `Half`: returns a value in `[delay_secs/2, delay_secs]`
84    pub fn apply(&self, delay_secs: f64, attempt: u32) -> f64 {
85        match self {
86            JitterStrategy::None => delay_secs,
87            JitterStrategy::Full => {
88                let factor = deterministic_random_factor(attempt);
89                factor * delay_secs
90            }
91            JitterStrategy::Half => {
92                let factor = deterministic_random_factor(attempt);
93                delay_secs / 2.0 + factor * (delay_secs / 2.0)
94            }
95        }
96    }
97}
98
99/// Generates a deterministic random factor in [0.0, 1.0) from an attempt number.
100///
101/// Uses blake2b hashing to produce a deterministic pseudo-random value
102/// seeded by the attempt number. This ensures replay safety.
103fn deterministic_random_factor(attempt: u32) -> f64 {
104    let mut hasher = Blake2b512::new();
105    hasher.update(b"jitter");
106    hasher.update(attempt.to_le_bytes());
107    let result = hasher.finalize();
108
109    // Take the first 8 bytes and convert to a u64, then normalize to [0.0, 1.0)
110    let mut bytes = [0u8; 8];
111    bytes.copy_from_slice(&result[..8]);
112    let value = u64::from_le_bytes(bytes);
113    (value as f64) / (u64::MAX as f64)
114}
115
116/// Decision returned by a wait strategy.
117///
118/// A wait strategy function returns this enum to indicate whether polling
119/// should continue (with a specified delay) or stop (condition is met).
120///
121/// # Variants
122///
123/// - `Continue { delay }` — Continue polling after the specified delay.
124/// - `Done` — Stop polling; the condition has been met.
125///
126/// # Example
127///
128/// ```
129/// use durable_execution_sdk::config::WaitDecision;
130/// use durable_execution_sdk::Duration;
131///
132/// let cont = WaitDecision::Continue { delay: Duration::from_seconds(5) };
133/// let done = WaitDecision::Done;
134/// ```
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum WaitDecision {
137    /// Continue polling after the specified delay.
138    Continue { delay: Duration },
139    /// Stop polling — condition is met.
140    Done,
141}
142
143/// Configuration for creating a wait strategy.
144///
145/// This struct holds all the parameters needed to build a wait strategy function
146/// via [`create_wait_strategy`]. The resulting function can be used with
147/// [`WaitForConditionConfig`](crate::context::WaitForConditionConfig) to control
148/// polling behavior with backoff, jitter, and a custom predicate.
149///
150/// # Type Parameters
151///
152/// - `T`: The state type returned by the condition check function.
153///
154/// # Example
155///
156/// ```
157/// use durable_execution_sdk::config::{WaitStrategyConfig, JitterStrategy, create_wait_strategy, WaitDecision};
158/// use durable_execution_sdk::Duration;
159///
160/// let config = WaitStrategyConfig {
161///     max_attempts: Some(10),
162///     initial_delay: Duration::from_seconds(5),
163///     max_delay: Duration::from_seconds(300),
164///     backoff_rate: 1.5,
165///     jitter: JitterStrategy::Full,
166///     should_continue_polling: Box::new(|state: &String| state != "COMPLETED"),
167/// };
168///
169/// let strategy = create_wait_strategy(config);
170/// // strategy(&"COMPLETED".to_string(), 1) => WaitDecision::Done
171/// ```
172pub struct WaitStrategyConfig<T> {
173    /// Maximum number of polling attempts. `None` defaults to 60.
174    pub max_attempts: Option<usize>,
175    /// Initial delay between polls.
176    pub initial_delay: Duration,
177    /// Maximum delay cap.
178    pub max_delay: Duration,
179    /// Backoff multiplier applied per attempt.
180    pub backoff_rate: f64,
181    /// Jitter strategy applied to the computed delay.
182    pub jitter: JitterStrategy,
183    /// Predicate that returns `true` if polling should continue, `false` if the condition is met.
184    pub should_continue_polling: Box<dyn Fn(&T) -> bool + Send + Sync>,
185}
186
187/// Creates a wait strategy function from the given configuration.
188///
189/// The returned closure takes a reference to the current state and the number of
190/// attempts made so far (1-indexed), and returns a [`WaitDecision`].
191///
192/// # Behavior
193///
194/// 1. If `should_continue_polling` returns `false`, returns `WaitDecision::Done`.
195/// 2. If `attempts_made >= max_attempts` and `should_continue_polling` is `true`,
196///    panics with a message indicating max attempts exceeded.
197/// 3. Otherwise, computes delay as `min(initial_delay * backoff_rate^(attempts_made - 1), max_delay)`,
198///    applies jitter, floors at 1 second, and returns `WaitDecision::Continue { delay }`.
199#[allow(clippy::type_complexity)]
200pub fn create_wait_strategy<T: Send + Sync + 'static>(
201    config: WaitStrategyConfig<T>,
202) -> Box<dyn Fn(&T, usize) -> WaitDecision + Send + Sync> {
203    let max_attempts = config.max_attempts.unwrap_or(60);
204    let initial_delay_secs = config.initial_delay.to_seconds() as f64;
205    let max_delay_secs = config.max_delay.to_seconds() as f64;
206    let backoff_rate = config.backoff_rate;
207    let jitter = config.jitter;
208    let should_continue = config.should_continue_polling;
209
210    Box::new(move |result: &T, attempts_made: usize| -> WaitDecision {
211        // Check if condition is met
212        if !should_continue(result) {
213            return WaitDecision::Done;
214        }
215
216        // Check max attempts — return Done so the handler can fail gracefully
217        // instead of panicking and crashing the async task.
218        if attempts_made >= max_attempts {
219            return WaitDecision::Done;
220        }
221
222        // Calculate delay with exponential backoff
223        let exponent = if attempts_made > 0 {
224            (attempts_made as i32) - 1
225        } else {
226            0
227        };
228        let base_delay = (initial_delay_secs * backoff_rate.powi(exponent)).min(max_delay_secs);
229
230        // Apply jitter
231        let jittered = jitter.apply(base_delay, attempts_made as u32);
232        let final_delay = jittered.max(1.0).round() as u64;
233
234        WaitDecision::Continue {
235            delay: Duration::from_seconds(final_delay),
236        }
237    })
238}
239
240/// Checkpointing mode that controls the trade-off between durability and performance.
241///
242/// The checkpointing mode determines when and how often the SDK persists operation
243/// state to the durable execution service. Different modes offer different trade-offs:
244///
245/// ## Modes
246///
247/// ### Eager Mode
248/// - Checkpoints after every operation completes
249/// - Maximum durability: minimal work is lost on failure
250/// - More API calls: higher latency and cost
251/// - Best for: Critical workflows where every operation must be durable
252///
253/// ### Batched Mode (Default)
254/// - Groups multiple operations into batches before checkpointing
255/// - Balanced durability: some operations may be replayed on failure
256/// - Fewer API calls: better performance and lower cost
257/// - Best for: Most workflows with reasonable durability requirements
258///
259/// ### Optimistic Mode
260/// - Executes multiple operations before checkpointing
261/// - Minimal durability: more work may be replayed on failure
262/// - Best performance: fewest API calls
263/// - Best for: Workflows where replay is cheap and performance is critical
264///
265/// ## Example
266///
267/// ```rust
268/// use durable_execution_sdk::CheckpointingMode;
269///
270/// // Use eager mode for maximum durability
271/// let eager = CheckpointingMode::Eager;
272///
273/// // Use batched mode for balanced performance (default)
274/// let batched = CheckpointingMode::default();
275///
276/// // Use optimistic mode for best performance
277/// let optimistic = CheckpointingMode::Optimistic;
278/// ```
279///
280/// ## Requirements
281///
282/// - 24.1: THE Performance_Configuration SHALL support eager checkpointing mode
283/// - 24.2: THE Performance_Configuration SHALL support batched checkpointing mode
284/// - 24.3: THE Performance_Configuration SHALL support optimistic execution mode
285/// - 24.4: THE Performance_Configuration SHALL document the default behavior and trade-offs
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
287pub enum CheckpointingMode {
288    /// Checkpoint after every operation for maximum durability.
289    ///
290    /// This mode provides the strongest durability guarantees but has the
291    /// highest overhead due to frequent API calls.
292    ///
293    /// ## Characteristics
294    /// - Every operation is immediately checkpointed
295    /// - Minimal work lost on failure (at most one operation)
296    /// - Higher latency due to synchronous checkpointing
297    /// - More API calls and higher cost
298    ///
299    /// ## Use Cases
300    /// - Financial transactions
301    /// - Critical business workflows
302    /// - Operations with expensive side effects
303    ///
304    /// ## Requirements
305    /// - 24.1: THE Performance_Configuration SHALL support eager checkpointing mode
306    Eager,
307
308    /// Batch multiple operations before checkpointing for balanced performance.
309    ///
310    /// This is the default mode that provides a good balance between durability
311    /// and performance. Operations are grouped into batches based on size, count,
312    /// or time limits before being checkpointed together.
313    ///
314    /// ## Characteristics
315    /// - Operations are batched before checkpointing
316    /// - Some operations may be replayed on failure
317    /// - Better performance than eager mode
318    /// - Configurable batch size and timing
319    ///
320    /// ## Use Cases
321    /// - Most general-purpose workflows
322    /// - Workflows with moderate durability requirements
323    /// - Cost-sensitive applications
324    ///
325    /// ## Requirements
326    /// - 24.2: THE Performance_Configuration SHALL support batched checkpointing mode
327    Batched,
328
329    /// Execute multiple operations before checkpointing for best performance.
330    ///
331    /// This mode prioritizes performance over durability by executing multiple
332    /// operations before creating a checkpoint. On failure, more work may need
333    /// to be replayed.
334    ///
335    /// ## Characteristics
336    /// - Multiple operations execute before checkpointing
337    /// - More work may be replayed on failure
338    /// - Best performance and lowest cost
339    /// - Suitable for idempotent operations
340    ///
341    /// ## Use Cases
342    /// - High-throughput batch processing
343    /// - Workflows with cheap, idempotent operations
344    /// - Performance-critical applications
345    ///
346    /// ## Requirements
347    /// - 24.3: THE Performance_Configuration SHALL support optimistic execution mode
348    Optimistic,
349}
350
351impl Default for CheckpointingMode {
352    /// Returns the default checkpointing mode (Batched).
353    ///
354    /// Batched mode is the default because it provides a good balance between
355    /// durability and performance for most use cases.
356    fn default() -> Self {
357        Self::Batched
358    }
359}
360
361impl CheckpointingMode {
362    /// Returns true if this mode checkpoints after every operation.
363    pub fn is_eager(&self) -> bool {
364        matches!(self, Self::Eager)
365    }
366
367    /// Returns true if this mode batches operations before checkpointing.
368    pub fn is_batched(&self) -> bool {
369        matches!(self, Self::Batched)
370    }
371
372    /// Returns true if this mode executes multiple operations before checkpointing.
373    pub fn is_optimistic(&self) -> bool {
374        matches!(self, Self::Optimistic)
375    }
376
377    /// Returns a human-readable description of this mode.
378    pub fn description(&self) -> &'static str {
379        match self {
380            Self::Eager => "Checkpoint after every operation (maximum durability)",
381            Self::Batched => "Batch operations before checkpointing (balanced)",
382            Self::Optimistic => {
383                "Execute multiple operations before checkpointing (best performance)"
384            }
385        }
386    }
387}
388
389/// Retry strategy trait for configuring step retry behavior.
390///
391/// # Sealed Trait
392///
393/// This trait is sealed and cannot be implemented outside of this crate.
394/// This allows the SDK maintainers to evolve the retry interface without
395/// breaking external code. If you need custom retry behavior, use the
396/// provided factory functions.
397#[allow(private_bounds)]
398pub trait RetryStrategy: Sealed + Send + Sync {
399    /// Returns the delay before the next retry attempt, or None if no more retries.
400    fn next_delay(&self, attempt: u32, error: &str) -> Option<Duration>;
401
402    /// Clone the retry strategy into a boxed trait object.
403    fn clone_box(&self) -> Box<dyn RetryStrategy>;
404}
405
406impl Clone for Box<dyn RetryStrategy> {
407    fn clone(&self) -> Self {
408        self.clone_box()
409    }
410}
411
412// =============================================================================
413// Built-in Retry Strategies
414// =============================================================================
415
416/// Exponential backoff retry strategy.
417///
418/// Delays increase exponentially with each attempt: `base_delay * 2^(attempt-1)`,
419/// capped at `max_delay`. Includes optional jitter to prevent thundering herd.
420///
421/// # Example
422///
423/// ```
424/// use durable_execution_sdk::config::ExponentialBackoff;
425/// use durable_execution_sdk::Duration;
426///
427/// // Retry up to 5 times with exponential backoff starting at 1 second
428/// let strategy = ExponentialBackoff::new(5, Duration::from_seconds(1));
429///
430/// // With custom max delay
431/// let strategy = ExponentialBackoff::builder()
432///     .max_attempts(5)
433///     .base_delay(Duration::from_seconds(1))
434///     .max_delay(Duration::from_minutes(5))
435///     .build();
436/// ```
437#[derive(Debug, Clone)]
438pub struct ExponentialBackoff {
439    /// Maximum number of retry attempts (not including the initial attempt).
440    pub max_attempts: u32,
441    /// Initial delay before the first retry.
442    pub base_delay: Duration,
443    /// Maximum delay between retries.
444    pub max_delay: Duration,
445    /// Multiplier for exponential growth (default: 2.0).
446    pub multiplier: f64,
447    /// Jitter strategy applied to computed delays.
448    pub jitter: JitterStrategy,
449}
450
451impl ExponentialBackoff {
452    /// Creates a new exponential backoff strategy with default settings.
453    ///
454    /// # Arguments
455    ///
456    /// * `max_attempts` - Maximum number of retry attempts
457    /// * `base_delay` - Initial delay before the first retry
458    pub fn new(max_attempts: u32, base_delay: Duration) -> Self {
459        Self {
460            max_attempts,
461            base_delay,
462            max_delay: Duration::from_hours(1),
463            multiplier: 2.0,
464            jitter: JitterStrategy::None,
465        }
466    }
467
468    /// Creates a builder for more detailed configuration.
469    pub fn builder() -> ExponentialBackoffBuilder {
470        ExponentialBackoffBuilder::default()
471    }
472}
473
474impl Sealed for ExponentialBackoff {}
475
476impl RetryStrategy for ExponentialBackoff {
477    fn next_delay(&self, attempt: u32, _error: &str) -> Option<Duration> {
478        if attempt >= self.max_attempts {
479            return None;
480        }
481
482        let base_seconds = self.base_delay.to_seconds() as f64;
483        let delay_seconds = base_seconds * self.multiplier.powi(attempt as i32);
484        let max_seconds = self.max_delay.to_seconds() as f64;
485        let capped_seconds = delay_seconds.min(max_seconds);
486
487        let jittered = self.jitter.apply(capped_seconds, attempt);
488        let final_seconds = jittered.max(1.0);
489
490        Some(Duration::from_seconds(final_seconds as u64))
491    }
492
493    fn clone_box(&self) -> Box<dyn RetryStrategy> {
494        Box::new(self.clone())
495    }
496}
497
498/// Builder for [`ExponentialBackoff`].
499#[derive(Debug, Clone)]
500pub struct ExponentialBackoffBuilder {
501    max_attempts: u32,
502    base_delay: Duration,
503    max_delay: Duration,
504    multiplier: f64,
505    jitter: JitterStrategy,
506}
507
508impl Default for ExponentialBackoffBuilder {
509    fn default() -> Self {
510        Self {
511            max_attempts: 3,
512            base_delay: Duration::from_seconds(1),
513            max_delay: Duration::from_hours(1),
514            multiplier: 2.0,
515            jitter: JitterStrategy::None,
516        }
517    }
518}
519
520impl ExponentialBackoffBuilder {
521    /// Sets the maximum number of retry attempts.
522    pub fn max_attempts(mut self, max_attempts: u32) -> Self {
523        self.max_attempts = max_attempts;
524        self
525    }
526
527    /// Sets the initial delay before the first retry.
528    pub fn base_delay(mut self, base_delay: Duration) -> Self {
529        self.base_delay = base_delay;
530        self
531    }
532
533    /// Sets the maximum delay between retries.
534    pub fn max_delay(mut self, max_delay: Duration) -> Self {
535        self.max_delay = max_delay;
536        self
537    }
538
539    /// Sets the multiplier for exponential growth (default: 2.0).
540    pub fn multiplier(mut self, multiplier: f64) -> Self {
541        self.multiplier = multiplier;
542        self
543    }
544
545    /// Sets the jitter strategy for retry delays.
546    pub fn jitter(mut self, jitter: JitterStrategy) -> Self {
547        self.jitter = jitter;
548        self
549    }
550
551    /// Builds the exponential backoff strategy.
552    pub fn build(self) -> ExponentialBackoff {
553        ExponentialBackoff {
554            max_attempts: self.max_attempts,
555            base_delay: self.base_delay,
556            max_delay: self.max_delay,
557            multiplier: self.multiplier,
558            jitter: self.jitter,
559        }
560    }
561}
562
563/// Fixed delay retry strategy.
564///
565/// Retries with a constant delay between attempts.
566///
567/// # Example
568///
569/// ```
570/// use durable_execution_sdk::config::FixedDelay;
571/// use durable_execution_sdk::Duration;
572///
573/// // Retry up to 3 times with 5 second delay between attempts
574/// let strategy = FixedDelay::new(3, Duration::from_seconds(5));
575/// ```
576#[derive(Debug, Clone)]
577pub struct FixedDelay {
578    /// Maximum number of retry attempts.
579    pub max_attempts: u32,
580    /// Delay between retry attempts.
581    pub delay: Duration,
582    /// Jitter strategy applied to the fixed delay.
583    pub jitter: JitterStrategy,
584}
585
586impl FixedDelay {
587    /// Creates a new fixed delay retry strategy.
588    ///
589    /// # Arguments
590    ///
591    /// * `max_attempts` - Maximum number of retry attempts
592    /// * `delay` - Delay between retry attempts
593    pub fn new(max_attempts: u32, delay: Duration) -> Self {
594        Self {
595            max_attempts,
596            delay,
597            jitter: JitterStrategy::None,
598        }
599    }
600
601    /// Sets the jitter strategy for retry delays.
602    pub fn with_jitter(mut self, jitter: JitterStrategy) -> Self {
603        self.jitter = jitter;
604        self
605    }
606}
607
608impl Sealed for FixedDelay {}
609
610impl RetryStrategy for FixedDelay {
611    fn next_delay(&self, attempt: u32, _error: &str) -> Option<Duration> {
612        if attempt >= self.max_attempts {
613            return None;
614        }
615
616        let delay_secs = self.delay.to_seconds() as f64;
617        let jittered = self.jitter.apply(delay_secs, attempt);
618        let final_seconds = jittered.max(1.0);
619
620        Some(Duration::from_seconds(final_seconds as u64))
621    }
622
623    fn clone_box(&self) -> Box<dyn RetryStrategy> {
624        Box::new(self.clone())
625    }
626}
627
628/// Linear backoff retry strategy.
629///
630/// Delays increase linearly with each attempt: `base_delay * attempt`.
631///
632/// # Example
633///
634/// ```
635/// use durable_execution_sdk::config::LinearBackoff;
636/// use durable_execution_sdk::Duration;
637///
638/// // Retry up to 5 times: 2s, 4s, 6s, 8s, 10s
639/// let strategy = LinearBackoff::new(5, Duration::from_seconds(2));
640/// ```
641#[derive(Debug, Clone)]
642pub struct LinearBackoff {
643    /// Maximum number of retry attempts.
644    pub max_attempts: u32,
645    /// Base delay that is multiplied by the attempt number.
646    pub base_delay: Duration,
647    /// Maximum delay between retries.
648    pub max_delay: Duration,
649    /// Jitter strategy applied to computed delays.
650    pub jitter: JitterStrategy,
651}
652
653impl LinearBackoff {
654    /// Creates a new linear backoff retry strategy.
655    ///
656    /// # Arguments
657    ///
658    /// * `max_attempts` - Maximum number of retry attempts
659    /// * `base_delay` - Base delay multiplied by attempt number
660    pub fn new(max_attempts: u32, base_delay: Duration) -> Self {
661        Self {
662            max_attempts,
663            base_delay,
664            max_delay: Duration::from_hours(1),
665            jitter: JitterStrategy::None,
666        }
667    }
668
669    /// Sets the maximum delay between retries.
670    pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
671        self.max_delay = max_delay;
672        self
673    }
674
675    /// Sets the jitter strategy for retry delays.
676    pub fn with_jitter(mut self, jitter: JitterStrategy) -> Self {
677        self.jitter = jitter;
678        self
679    }
680}
681
682impl Sealed for LinearBackoff {}
683
684impl RetryStrategy for LinearBackoff {
685    fn next_delay(&self, attempt: u32, _error: &str) -> Option<Duration> {
686        if attempt >= self.max_attempts {
687            return None;
688        }
689
690        let base_seconds = self.base_delay.to_seconds();
691        let delay_seconds = base_seconds.saturating_mul((attempt + 1) as u64);
692        let max_seconds = self.max_delay.to_seconds();
693        let capped_seconds = delay_seconds.min(max_seconds) as f64;
694
695        let jittered = self.jitter.apply(capped_seconds, attempt);
696        let final_seconds = jittered.max(1.0);
697
698        Some(Duration::from_seconds(final_seconds as u64))
699    }
700
701    fn clone_box(&self) -> Box<dyn RetryStrategy> {
702        Box::new(self.clone())
703    }
704}
705
706/// No retry strategy - fails immediately on first error.
707///
708/// # Example
709///
710/// ```
711/// use durable_execution_sdk::config::NoRetry;
712///
713/// let strategy = NoRetry;
714/// ```
715#[derive(Debug, Clone, Copy, Default)]
716pub struct NoRetry;
717
718impl Sealed for NoRetry {}
719
720impl RetryStrategy for NoRetry {
721    fn next_delay(&self, _attempt: u32, _error: &str) -> Option<Duration> {
722        None
723    }
724
725    fn clone_box(&self) -> Box<dyn RetryStrategy> {
726        Box::new(*self)
727    }
728}
729
730/// Pattern for matching retryable errors.
731///
732/// Used with [`RetryableErrorFilter`] to declaratively specify which errors
733/// should be retried.
734///
735/// # Example
736///
737/// ```
738/// use durable_execution_sdk::config::ErrorPattern;
739///
740/// let contains = ErrorPattern::Contains("timeout".to_string());
741/// let regex = ErrorPattern::Regex(regex::Regex::new(r"(?i)connection.*refused").unwrap());
742/// ```
743#[derive(Clone)]
744pub enum ErrorPattern {
745    /// Match if error message contains this substring.
746    Contains(String),
747    /// Match if error message matches this regex.
748    Regex(regex::Regex),
749}
750
751impl std::fmt::Debug for ErrorPattern {
752    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
753        match self {
754            ErrorPattern::Contains(s) => f.debug_tuple("Contains").field(s).finish(),
755            ErrorPattern::Regex(r) => f.debug_tuple("Regex").field(&r.as_str()).finish(),
756        }
757    }
758}
759
760/// Declarative filter for retryable errors.
761///
762/// When configured on a [`StepConfig`], only errors matching the filter will be retried.
763/// If no patterns and no error types are configured, all errors are retried (backward-compatible).
764///
765/// Patterns and error types are combined with OR logic: an error is retryable if it matches
766/// ANY pattern OR ANY error type.
767///
768/// # Example
769///
770/// ```
771/// use durable_execution_sdk::config::{RetryableErrorFilter, ErrorPattern};
772///
773/// let filter = RetryableErrorFilter {
774///     patterns: vec![
775///         ErrorPattern::Contains("timeout".to_string()),
776///         ErrorPattern::Regex(regex::Regex::new(r"(?i)connection.*refused").unwrap()),
777///     ],
778///     error_types: vec!["TransientError".to_string()],
779/// };
780///
781/// assert!(filter.is_retryable("request timeout occurred"));
782/// assert!(!filter.is_retryable("invalid input"));
783/// assert!(filter.is_retryable_with_type("invalid input", "TransientError"));
784/// ```
785#[derive(Clone, Debug, Default)]
786pub struct RetryableErrorFilter {
787    /// Error message patterns (string contains or regex).
788    pub patterns: Vec<ErrorPattern>,
789    /// Error type names to match against.
790    pub error_types: Vec<String>,
791}
792
793impl RetryableErrorFilter {
794    /// Returns `true` if the error message is retryable according to this filter.
795    ///
796    /// If no filters are configured (empty patterns and empty error_types),
797    /// returns `true` for all errors (backward-compatible default).
798    ///
799    /// Otherwise, returns `true` if the error message matches any configured pattern.
800    pub fn is_retryable(&self, error_msg: &str) -> bool {
801        if self.patterns.is_empty() && self.error_types.is_empty() {
802            return true;
803        }
804
805        self.patterns.iter().any(|p| match p {
806            ErrorPattern::Contains(s) => error_msg.contains(s.as_str()),
807            ErrorPattern::Regex(r) => r.is_match(error_msg),
808        })
809    }
810
811    /// Returns `true` if the error is retryable by message or type.
812    ///
813    /// Uses OR logic: returns `true` if the error matches any pattern
814    /// OR if the error type matches any configured error type.
815    ///
816    /// If no filters are configured, returns `true` for all errors.
817    pub fn is_retryable_with_type(&self, error_msg: &str, error_type: &str) -> bool {
818        if self.patterns.is_empty() && self.error_types.is_empty() {
819            return true;
820        }
821
822        let matches_type = self.error_types.iter().any(|t| t == error_type);
823        matches_type || self.is_retryable(error_msg)
824    }
825}
826
827/// Custom retry strategy using a user-provided closure.
828///
829/// This allows users to define custom retry logic without implementing
830/// the sealed `RetryStrategy` trait directly.
831///
832/// # Example
833///
834/// ```
835/// use durable_execution_sdk::config::custom_retry;
836/// use durable_execution_sdk::Duration;
837///
838/// // Custom strategy: retry up to 3 times, but only for specific errors
839/// let strategy = custom_retry(|attempt, error| {
840///     if attempt >= 3 {
841///         return None;
842///     }
843///     if error.contains("transient") || error.contains("timeout") {
844///         Some(Duration::from_seconds(5))
845///     } else {
846///         None // Don't retry other errors
847///     }
848/// });
849/// ```
850pub fn custom_retry<F>(f: F) -> CustomRetry<F>
851where
852    F: Fn(u32, &str) -> Option<Duration> + Send + Sync + Clone + 'static,
853{
854    CustomRetry { f }
855}
856
857/// Custom retry strategy wrapper.
858///
859/// Created via the [`custom_retry`] function.
860#[derive(Clone)]
861pub struct CustomRetry<F>
862where
863    F: Fn(u32, &str) -> Option<Duration> + Send + Sync + Clone + 'static,
864{
865    f: F,
866}
867
868impl<F> std::fmt::Debug for CustomRetry<F>
869where
870    F: Fn(u32, &str) -> Option<Duration> + Send + Sync + Clone + 'static,
871{
872    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
873        f.debug_struct("CustomRetry").finish()
874    }
875}
876
877impl<F> Sealed for CustomRetry<F> where
878    F: Fn(u32, &str) -> Option<Duration> + Send + Sync + Clone + 'static
879{
880}
881
882impl<F> RetryStrategy for CustomRetry<F>
883where
884    F: Fn(u32, &str) -> Option<Duration> + Send + Sync + Clone + 'static,
885{
886    fn next_delay(&self, attempt: u32, error: &str) -> Option<Duration> {
887        (self.f)(attempt, error)
888    }
889
890    fn clone_box(&self) -> Box<dyn RetryStrategy> {
891        Box::new(self.clone())
892    }
893}
894
895// =============================================================================
896// Step Semantics and Configuration
897// =============================================================================
898
899/// Execution semantics for step operations.
900#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
901pub enum StepSemantics {
902    /// Checkpoint before execution - guarantees at most once execution per retry.
903    AtMostOncePerRetry,
904    /// Checkpoint after execution - guarantees at least once execution per retry.
905    #[default]
906    AtLeastOncePerRetry,
907}
908
909/// Configuration for step operations.
910///
911/// # Examples
912///
913/// Using default configuration:
914///
915/// ```
916/// use durable_execution_sdk::StepConfig;
917///
918/// let config = StepConfig::default();
919/// // Default uses AtLeastOncePerRetry semantics
920/// ```
921///
922/// Configuring step semantics:
923///
924/// ```
925/// use durable_execution_sdk::{StepConfig, StepSemantics};
926///
927/// // For non-idempotent operations, use AtMostOncePerRetry
928/// let config = StepConfig {
929///     step_semantics: StepSemantics::AtMostOncePerRetry,
930///     ..Default::default()
931/// };
932/// ```
933#[derive(Clone, Default)]
934pub struct StepConfig {
935    /// Optional retry strategy for failed steps.
936    pub retry_strategy: Option<Box<dyn RetryStrategy>>,
937    /// Execution semantics (at-most-once or at-least-once).
938    pub step_semantics: StepSemantics,
939    /// Optional custom serializer/deserializer.
940    pub serdes: Option<Arc<dyn SerDesAny>>,
941    /// Optional filter for retryable errors. When set, only errors matching
942    /// the filter will be retried. When `None`, all errors are retried
943    /// (current behavior preserved).
944    pub retryable_error_filter: Option<RetryableErrorFilter>,
945}
946
947impl std::fmt::Debug for StepConfig {
948    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
949        f.debug_struct("StepConfig")
950            .field("retry_strategy", &self.retry_strategy.is_some())
951            .field("step_semantics", &self.step_semantics)
952            .field("serdes", &self.serdes.is_some())
953            .field(
954                "retryable_error_filter",
955                &self.retryable_error_filter.is_some(),
956            )
957            .finish()
958    }
959}
960
961/// Configuration for callback operations.
962#[derive(Debug, Clone, Default)]
963pub struct CallbackConfig {
964    /// Timeout duration for the callback.
965    pub timeout: Duration,
966    /// Heartbeat timeout duration.
967    pub heartbeat_timeout: Duration,
968    /// Optional custom serializer/deserializer.
969    pub serdes: Option<Arc<dyn SerDesAny>>,
970}
971
972/// Configuration for invoke operations.
973#[derive(Clone)]
974pub struct InvokeConfig<P, R> {
975    /// Timeout duration for the invocation.
976    pub timeout: Duration,
977    /// Optional custom serializer for the payload.
978    pub serdes_payload: Option<Arc<dyn SerDesAny>>,
979    /// Optional custom deserializer for the result.
980    pub serdes_result: Option<Arc<dyn SerDesAny>>,
981    /// Optional tenant ID for multi-tenant scenarios.
982    pub tenant_id: Option<String>,
983    /// Phantom data for type parameters.
984    _marker: PhantomData<(P, R)>,
985}
986
987impl<P, R> Default for InvokeConfig<P, R> {
988    fn default() -> Self {
989        Self {
990            timeout: Duration::default(),
991            serdes_payload: None,
992            serdes_result: None,
993            tenant_id: None,
994            _marker: PhantomData,
995        }
996    }
997}
998
999impl<P, R> std::fmt::Debug for InvokeConfig<P, R> {
1000    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1001        f.debug_struct("InvokeConfig")
1002            .field("timeout", &self.timeout)
1003            .field("serdes_payload", &self.serdes_payload.is_some())
1004            .field("serdes_result", &self.serdes_result.is_some())
1005            .field("tenant_id", &self.tenant_id)
1006            .finish()
1007    }
1008}
1009
1010/// Configuration for map operations.
1011///
1012/// # Examples
1013///
1014/// Basic map configuration with concurrency limit:
1015///
1016/// ```
1017/// use durable_execution_sdk::MapConfig;
1018///
1019/// let config = MapConfig {
1020///     max_concurrency: Some(5),
1021///     ..Default::default()
1022/// };
1023/// ```
1024///
1025/// Map with failure tolerance:
1026///
1027/// ```
1028/// use durable_execution_sdk::{MapConfig, CompletionConfig};
1029///
1030/// let config = MapConfig {
1031///     max_concurrency: Some(10),
1032///     completion_config: CompletionConfig::with_failure_tolerance(2),
1033///     ..Default::default()
1034/// };
1035/// ```
1036#[derive(Debug, Clone, Default)]
1037pub struct MapConfig {
1038    /// Maximum number of concurrent executions.
1039    pub max_concurrency: Option<usize>,
1040    /// Optional item batcher for grouping items.
1041    pub item_batcher: Option<ItemBatcher>,
1042    /// Completion configuration defining success/failure criteria.
1043    pub completion_config: CompletionConfig,
1044    /// Optional custom serializer/deserializer.
1045    pub serdes: Option<Arc<dyn SerDesAny>>,
1046}
1047
1048/// Configuration for parallel operations.
1049#[derive(Debug, Clone, Default)]
1050pub struct ParallelConfig {
1051    /// Maximum number of concurrent executions.
1052    pub max_concurrency: Option<usize>,
1053    /// Completion configuration defining success/failure criteria.
1054    pub completion_config: CompletionConfig,
1055    /// Optional custom serializer/deserializer.
1056    pub serdes: Option<Arc<dyn SerDesAny>>,
1057}
1058
1059/// Configuration for child context operations.
1060///
1061/// This configuration controls how child contexts behave, including
1062/// whether to replay children when loading state for large parallel operations.
1063#[derive(Clone, Default)]
1064#[allow(clippy::type_complexity)]
1065pub struct ChildConfig {
1066    /// Optional custom serializer/deserializer.
1067    pub serdes: Option<Arc<dyn SerDesAny>>,
1068    /// Whether to replay children when loading state.
1069    ///
1070    /// When set to `true`, the child context will request child operations
1071    /// to be included in state loads during replay. This is useful for large
1072    /// parallel operations where the combined output needs to be reconstructed
1073    /// by replaying each branch.
1074    ///
1075    /// Default is `false` for better performance in most cases.
1076    pub replay_children: bool,
1077    /// Optional function to map child context errors before propagation.
1078    ///
1079    /// When set, this function is applied to errors from child context execution
1080    /// before they are checkpointed and propagated. Suspend errors are never mapped.
1081    ///
1082    /// Default is `None`, which preserves current behavior (errors propagate unchanged).
1083    pub error_mapper: Option<Arc<dyn Fn(DurableError) -> DurableError + Send + Sync>>,
1084    /// Optional function to generate a summary when the serialized child result exceeds 256KB.
1085    ///
1086    /// When set, this function is invoked with the serialized result string if its size
1087    /// exceeds 256KB (262144 bytes). The returned summary string is stored instead of the
1088    /// full result, enabling replay-based reconstruction for large payloads.
1089    ///
1090    /// When the serialized result is 256KB or less, the full result is stored even if
1091    /// a summary generator is configured.
1092    ///
1093    /// Default is `None`, which preserves current behavior (full result stored regardless of size).
1094    pub summary_generator: Option<Arc<dyn Fn(&str) -> String + Send + Sync>>,
1095}
1096
1097impl std::fmt::Debug for ChildConfig {
1098    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1099        f.debug_struct("ChildConfig")
1100            .field("serdes", &self.serdes)
1101            .field("replay_children", &self.replay_children)
1102            .field("error_mapper", &self.error_mapper.as_ref().map(|_| "..."))
1103            .field(
1104                "summary_generator",
1105                &self.summary_generator.as_ref().map(|_| "..."),
1106            )
1107            .finish()
1108    }
1109}
1110
1111impl ChildConfig {
1112    /// Creates a new ChildConfig with default values.
1113    pub fn new() -> Self {
1114        Self::default()
1115    }
1116
1117    /// Creates a ChildConfig with replay_children enabled.
1118    ///
1119    /// Use this when you need to reconstruct the combined output of a large
1120    /// parallel operation by replaying each branch.
1121    ///
1122    /// # Example
1123    ///
1124    /// ```
1125    /// use durable_execution_sdk::ChildConfig;
1126    ///
1127    /// let config = ChildConfig::with_replay_children();
1128    /// assert!(config.replay_children);
1129    /// ```
1130    pub fn with_replay_children() -> Self {
1131        Self {
1132            replay_children: true,
1133            ..Default::default()
1134        }
1135    }
1136
1137    /// Sets the replay_children option.
1138    ///
1139    /// # Arguments
1140    ///
1141    /// * `replay_children` - Whether to replay children when loading state
1142    pub fn set_replay_children(mut self, replay_children: bool) -> Self {
1143        self.replay_children = replay_children;
1144        self
1145    }
1146
1147    /// Sets the custom serializer/deserializer.
1148    pub fn set_serdes(mut self, serdes: Arc<dyn SerDesAny>) -> Self {
1149        self.serdes = Some(serdes);
1150        self
1151    }
1152
1153    /// Sets the error mapper function.
1154    ///
1155    /// The error mapper is applied to child context errors before they are
1156    /// checkpointed and propagated. Suspend errors are never mapped.
1157    pub fn set_error_mapper(
1158        mut self,
1159        mapper: Arc<dyn Fn(DurableError) -> DurableError + Send + Sync>,
1160    ) -> Self {
1161        self.error_mapper = Some(mapper);
1162        self
1163    }
1164
1165    /// Sets the summary generator function.
1166    ///
1167    /// The summary generator is invoked when the serialized child result exceeds
1168    /// 256KB (262144 bytes). It receives the serialized result string and should
1169    /// return a compact summary string to store instead.
1170    pub fn set_summary_generator(
1171        mut self,
1172        generator: Arc<dyn Fn(&str) -> String + Send + Sync>,
1173    ) -> Self {
1174        self.summary_generator = Some(generator);
1175        self
1176    }
1177}
1178
1179/// Type alias for ChildConfig for consistency with the design document.
1180///
1181/// The design document refers to this as `ContextConfig`, but internally
1182/// we use `ChildConfig` to be more descriptive of its purpose.
1183pub type ContextConfig = ChildConfig;
1184
1185/// Configuration defining success/failure criteria for concurrent operations.
1186#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1187pub struct CompletionConfig {
1188    /// Minimum number of successful completions required.
1189    pub min_successful: Option<usize>,
1190    /// Maximum number of tolerated failures (absolute count).
1191    pub tolerated_failure_count: Option<usize>,
1192    /// Maximum percentage of tolerated failures (0.0 to 1.0).
1193    pub tolerated_failure_percentage: Option<f64>,
1194}
1195
1196impl CompletionConfig {
1197    /// Creates a completion config that succeeds when the first task succeeds.
1198    ///
1199    /// # Example
1200    ///
1201    /// ```
1202    /// use durable_execution_sdk::CompletionConfig;
1203    ///
1204    /// let config = CompletionConfig::first_successful();
1205    /// assert_eq!(config.min_successful, Some(1));
1206    /// ```
1207    pub fn first_successful() -> Self {
1208        Self {
1209            min_successful: Some(1),
1210            ..Default::default()
1211        }
1212    }
1213
1214    /// Creates a completion config that waits for all tasks to complete.
1215    ///
1216    /// # Example
1217    ///
1218    /// ```
1219    /// use durable_execution_sdk::CompletionConfig;
1220    ///
1221    /// let config = CompletionConfig::all_completed();
1222    /// assert!(config.min_successful.is_none());
1223    /// ```
1224    pub fn all_completed() -> Self {
1225        Self::default()
1226    }
1227
1228    /// Creates a completion config that requires all tasks to succeed.
1229    ///
1230    /// # Example
1231    ///
1232    /// ```
1233    /// use durable_execution_sdk::CompletionConfig;
1234    ///
1235    /// let config = CompletionConfig::all_successful();
1236    /// assert_eq!(config.tolerated_failure_count, Some(0));
1237    /// assert_eq!(config.tolerated_failure_percentage, Some(0.0));
1238    /// ```
1239    pub fn all_successful() -> Self {
1240        Self {
1241            tolerated_failure_count: Some(0),
1242            tolerated_failure_percentage: Some(0.0),
1243            ..Default::default()
1244        }
1245    }
1246
1247    /// Creates a completion config with a specific minimum successful count.
1248    pub fn with_min_successful(count: usize) -> Self {
1249        Self {
1250            min_successful: Some(count),
1251            ..Default::default()
1252        }
1253    }
1254
1255    /// Creates a completion config with a specific failure tolerance.
1256    pub fn with_failure_tolerance(count: usize) -> Self {
1257        Self {
1258            tolerated_failure_count: Some(count),
1259            ..Default::default()
1260        }
1261    }
1262}
1263
1264/// Configuration for batching items in map operations.
1265#[derive(Debug, Clone)]
1266pub struct ItemBatcher {
1267    /// Maximum number of items per batch.
1268    pub max_items_per_batch: usize,
1269    /// Maximum total bytes per batch.
1270    pub max_bytes_per_batch: usize,
1271}
1272
1273impl Default for ItemBatcher {
1274    fn default() -> Self {
1275        Self {
1276            max_items_per_batch: 100,
1277            max_bytes_per_batch: 256 * 1024, // 256KB
1278        }
1279    }
1280}
1281
1282impl ItemBatcher {
1283    /// Creates a new ItemBatcher with the specified limits.
1284    pub fn new(max_items_per_batch: usize, max_bytes_per_batch: usize) -> Self {
1285        Self {
1286            max_items_per_batch,
1287            max_bytes_per_batch,
1288        }
1289    }
1290
1291    /// Batches items according to configuration, respecting both item count and byte limits.
1292    ///
1293    /// This method groups items into batches where each batch:
1294    /// - Contains at most `max_items_per_batch` items
1295    /// - Has an estimated total size of at most `max_bytes_per_batch` bytes
1296    ///
1297    /// Item size is estimated using JSON serialization via `serde_json`.
1298    ///
1299    /// # Arguments
1300    ///
1301    /// * `items` - The slice of items to batch
1302    ///
1303    /// # Returns
1304    ///
1305    /// A vector of `(start_index, batch)` tuples where:
1306    /// - `start_index` is the index of the first item in the batch from the original slice
1307    /// - `batch` is a vector of cloned items in that batch
1308    /// # Example
1309    ///
1310    /// ```
1311    /// use durable_execution_sdk::ItemBatcher;
1312    ///
1313    /// let batcher = ItemBatcher::new(2, 1024);
1314    /// let items = vec!["a", "b", "c", "d", "e"];
1315    /// let batches = batcher.batch(&items);
1316    ///
1317    /// // Items are grouped into batches of at most 2 items each
1318    /// assert_eq!(batches.len(), 3);
1319    /// assert_eq!(batches[0], (0, vec!["a", "b"]));
1320    /// assert_eq!(batches[1], (2, vec!["c", "d"]));
1321    /// assert_eq!(batches[2], (4, vec!["e"]));
1322    /// ```
1323    pub fn batch<T: Serialize + Clone>(&self, items: &[T]) -> Vec<(usize, Vec<T>)> {
1324        if items.is_empty() {
1325            return Vec::new();
1326        }
1327
1328        let mut batches = Vec::new();
1329        let mut current_batch = Vec::new();
1330        let mut current_bytes = 0usize;
1331        let mut batch_start_index = 0;
1332
1333        for (i, item) in items.iter().enumerate() {
1334            // Estimate item size using JSON serialization
1335            let item_bytes = serde_json::to_string(item).map(|s| s.len()).unwrap_or(0);
1336
1337            // Check if adding this item would exceed limits
1338            let would_exceed_items = current_batch.len() >= self.max_items_per_batch;
1339            let would_exceed_bytes =
1340                current_bytes + item_bytes > self.max_bytes_per_batch && !current_batch.is_empty();
1341
1342            if would_exceed_items || would_exceed_bytes {
1343                // Finalize current batch and start a new one
1344                batches.push((batch_start_index, std::mem::take(&mut current_batch)));
1345                current_bytes = 0;
1346                batch_start_index = i;
1347            }
1348
1349            current_batch.push(item.clone());
1350            current_bytes += item_bytes;
1351        }
1352
1353        // Don't forget the last batch
1354        if !current_batch.is_empty() {
1355            batches.push((batch_start_index, current_batch));
1356        }
1357
1358        batches
1359    }
1360}
1361
1362/// Type-erased SerDes trait for storing in config structs.
1363pub trait SerDesAny: Send + Sync {
1364    /// Serialize a value to a string.
1365    fn serialize_any(
1366        &self,
1367        value: &dyn std::any::Any,
1368    ) -> Result<String, crate::error::DurableError>;
1369    /// Deserialize a string to a boxed Any value.
1370    fn deserialize_any(
1371        &self,
1372        data: &str,
1373    ) -> Result<Box<dyn std::any::Any + Send>, crate::error::DurableError>;
1374}
1375
1376impl std::fmt::Debug for dyn SerDesAny {
1377    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1378        f.write_str("SerDesAny")
1379    }
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384    use super::*;
1385    use proptest::prelude::*;
1386
1387    // =========================================================================
1388    // Unit Tests
1389    // =========================================================================
1390
1391    #[test]
1392    fn test_step_semantics_default() {
1393        let semantics = StepSemantics::default();
1394        assert_eq!(semantics, StepSemantics::AtLeastOncePerRetry);
1395    }
1396
1397    #[test]
1398    fn test_step_config_default() {
1399        let config = StepConfig::default();
1400        assert!(config.retry_strategy.is_none());
1401        assert_eq!(config.step_semantics, StepSemantics::AtLeastOncePerRetry);
1402        assert!(config.serdes.is_none());
1403    }
1404
1405    #[test]
1406    fn test_completion_config_first_successful() {
1407        let config = CompletionConfig::first_successful();
1408        assert_eq!(config.min_successful, Some(1));
1409        assert!(config.tolerated_failure_count.is_none());
1410        assert!(config.tolerated_failure_percentage.is_none());
1411    }
1412
1413    #[test]
1414    fn test_completion_config_all_completed() {
1415        let config = CompletionConfig::all_completed();
1416        assert!(config.min_successful.is_none());
1417        assert!(config.tolerated_failure_count.is_none());
1418        assert!(config.tolerated_failure_percentage.is_none());
1419    }
1420
1421    #[test]
1422    fn test_completion_config_all_successful() {
1423        let config = CompletionConfig::all_successful();
1424        assert!(config.min_successful.is_none());
1425        assert_eq!(config.tolerated_failure_count, Some(0));
1426        assert_eq!(config.tolerated_failure_percentage, Some(0.0));
1427    }
1428
1429    #[test]
1430    fn test_item_batcher_default() {
1431        let batcher = ItemBatcher::default();
1432        assert_eq!(batcher.max_items_per_batch, 100);
1433        assert_eq!(batcher.max_bytes_per_batch, 256 * 1024);
1434    }
1435
1436    #[test]
1437    fn test_item_batcher_new() {
1438        let batcher = ItemBatcher::new(50, 128 * 1024);
1439        assert_eq!(batcher.max_items_per_batch, 50);
1440        assert_eq!(batcher.max_bytes_per_batch, 128 * 1024);
1441    }
1442
1443    #[test]
1444    fn test_callback_config_default() {
1445        let config = CallbackConfig::default();
1446        assert_eq!(config.timeout.to_seconds(), 0);
1447        assert_eq!(config.heartbeat_timeout.to_seconds(), 0);
1448    }
1449
1450    #[test]
1451    fn test_invoke_config_default() {
1452        let config: InvokeConfig<String, String> = InvokeConfig::default();
1453        assert_eq!(config.timeout.to_seconds(), 0);
1454        assert!(config.tenant_id.is_none());
1455    }
1456
1457    #[test]
1458    fn test_map_config_default() {
1459        let config = MapConfig::default();
1460        assert!(config.max_concurrency.is_none());
1461        assert!(config.item_batcher.is_none());
1462    }
1463
1464    #[test]
1465    fn test_parallel_config_default() {
1466        let config = ParallelConfig::default();
1467        assert!(config.max_concurrency.is_none());
1468    }
1469
1470    #[test]
1471    fn test_child_config_default() {
1472        let config = ChildConfig::default();
1473        assert!(!config.replay_children);
1474        assert!(config.serdes.is_none());
1475        assert!(config.error_mapper.is_none());
1476        assert!(config.summary_generator.is_none());
1477    }
1478
1479    #[test]
1480    fn test_child_config_with_replay_children() {
1481        let config = ChildConfig::with_replay_children();
1482        assert!(config.replay_children);
1483    }
1484
1485    #[test]
1486    fn test_child_config_set_replay_children() {
1487        let config = ChildConfig::new().set_replay_children(true);
1488        assert!(config.replay_children);
1489    }
1490
1491    #[test]
1492    fn test_context_config_type_alias() {
1493        // ContextConfig is a type alias for ChildConfig
1494        let config: ContextConfig = ContextConfig::with_replay_children();
1495        assert!(config.replay_children);
1496    }
1497
1498    #[test]
1499    fn test_checkpointing_mode_default() {
1500        let mode = CheckpointingMode::default();
1501        assert_eq!(mode, CheckpointingMode::Batched);
1502        assert!(mode.is_batched());
1503    }
1504
1505    #[test]
1506    fn test_checkpointing_mode_eager() {
1507        let mode = CheckpointingMode::Eager;
1508        assert!(mode.is_eager());
1509        assert!(!mode.is_batched());
1510        assert!(!mode.is_optimistic());
1511    }
1512
1513    #[test]
1514    fn test_checkpointing_mode_batched() {
1515        let mode = CheckpointingMode::Batched;
1516        assert!(!mode.is_eager());
1517        assert!(mode.is_batched());
1518        assert!(!mode.is_optimistic());
1519    }
1520
1521    #[test]
1522    fn test_checkpointing_mode_optimistic() {
1523        let mode = CheckpointingMode::Optimistic;
1524        assert!(!mode.is_eager());
1525        assert!(!mode.is_batched());
1526        assert!(mode.is_optimistic());
1527    }
1528
1529    #[test]
1530    fn test_checkpointing_mode_description() {
1531        assert!(CheckpointingMode::Eager
1532            .description()
1533            .contains("maximum durability"));
1534        assert!(CheckpointingMode::Batched
1535            .description()
1536            .contains("balanced"));
1537        assert!(CheckpointingMode::Optimistic
1538            .description()
1539            .contains("best performance"));
1540    }
1541
1542    #[test]
1543    fn test_checkpointing_mode_serialization() {
1544        // Test that CheckpointingMode can be serialized and deserialized
1545        let mode = CheckpointingMode::Eager;
1546        let serialized = serde_json::to_string(&mode).unwrap();
1547        let deserialized: CheckpointingMode = serde_json::from_str(&serialized).unwrap();
1548        assert_eq!(mode, deserialized);
1549
1550        let mode = CheckpointingMode::Batched;
1551        let serialized = serde_json::to_string(&mode).unwrap();
1552        let deserialized: CheckpointingMode = serde_json::from_str(&serialized).unwrap();
1553        assert_eq!(mode, deserialized);
1554
1555        let mode = CheckpointingMode::Optimistic;
1556        let serialized = serde_json::to_string(&mode).unwrap();
1557        let deserialized: CheckpointingMode = serde_json::from_str(&serialized).unwrap();
1558        assert_eq!(mode, deserialized);
1559    }
1560
1561    // =========================================================================
1562    // Retry Strategy Tests
1563    // =========================================================================
1564
1565    #[test]
1566    fn test_exponential_backoff_new() {
1567        let strategy = ExponentialBackoff::new(5, Duration::from_seconds(1));
1568        assert_eq!(strategy.max_attempts, 5);
1569        assert_eq!(strategy.base_delay.to_seconds(), 1);
1570        assert_eq!(strategy.max_delay.to_seconds(), 3600); // 1 hour default
1571        assert!((strategy.multiplier - 2.0).abs() < f64::EPSILON);
1572    }
1573
1574    #[test]
1575    fn test_exponential_backoff_builder() {
1576        let strategy = ExponentialBackoff::builder()
1577            .max_attempts(10)
1578            .base_delay(Duration::from_seconds(2))
1579            .max_delay(Duration::from_minutes(30))
1580            .multiplier(3.0)
1581            .build();
1582
1583        assert_eq!(strategy.max_attempts, 10);
1584        assert_eq!(strategy.base_delay.to_seconds(), 2);
1585        assert_eq!(strategy.max_delay.to_seconds(), 1800); // 30 minutes
1586        assert!((strategy.multiplier - 3.0).abs() < f64::EPSILON);
1587    }
1588
1589    #[test]
1590    fn test_exponential_backoff_delays() {
1591        let strategy = ExponentialBackoff::new(5, Duration::from_seconds(1));
1592
1593        // attempt 0: 1 * 2^0 = 1 second
1594        assert_eq!(
1595            strategy.next_delay(0, "error").map(|d| d.to_seconds()),
1596            Some(1)
1597        );
1598        // attempt 1: 1 * 2^1 = 2 seconds
1599        assert_eq!(
1600            strategy.next_delay(1, "error").map(|d| d.to_seconds()),
1601            Some(2)
1602        );
1603        // attempt 2: 1 * 2^2 = 4 seconds
1604        assert_eq!(
1605            strategy.next_delay(2, "error").map(|d| d.to_seconds()),
1606            Some(4)
1607        );
1608        // attempt 3: 1 * 2^3 = 8 seconds
1609        assert_eq!(
1610            strategy.next_delay(3, "error").map(|d| d.to_seconds()),
1611            Some(8)
1612        );
1613        // attempt 4: 1 * 2^4 = 16 seconds
1614        assert_eq!(
1615            strategy.next_delay(4, "error").map(|d| d.to_seconds()),
1616            Some(16)
1617        );
1618        // attempt 5: exceeds max_attempts
1619        assert_eq!(strategy.next_delay(5, "error"), None);
1620    }
1621
1622    #[test]
1623    fn test_exponential_backoff_max_delay_cap() {
1624        let strategy = ExponentialBackoff::builder()
1625            .max_attempts(10)
1626            .base_delay(Duration::from_seconds(10))
1627            .max_delay(Duration::from_seconds(30))
1628            .build();
1629
1630        // attempt 0: 10 * 2^0 = 10 seconds
1631        assert_eq!(
1632            strategy.next_delay(0, "error").map(|d| d.to_seconds()),
1633            Some(10)
1634        );
1635        // attempt 1: 10 * 2^1 = 20 seconds
1636        assert_eq!(
1637            strategy.next_delay(1, "error").map(|d| d.to_seconds()),
1638            Some(20)
1639        );
1640        // attempt 2: 10 * 2^2 = 40 seconds, capped at 30
1641        assert_eq!(
1642            strategy.next_delay(2, "error").map(|d| d.to_seconds()),
1643            Some(30)
1644        );
1645        // attempt 3: 10 * 2^3 = 80 seconds, capped at 30
1646        assert_eq!(
1647            strategy.next_delay(3, "error").map(|d| d.to_seconds()),
1648            Some(30)
1649        );
1650    }
1651
1652    #[test]
1653    fn test_fixed_delay_new() {
1654        let strategy = FixedDelay::new(3, Duration::from_seconds(5));
1655        assert_eq!(strategy.max_attempts, 3);
1656        assert_eq!(strategy.delay.to_seconds(), 5);
1657    }
1658
1659    #[test]
1660    fn test_fixed_delay_constant() {
1661        let strategy = FixedDelay::new(3, Duration::from_seconds(5));
1662
1663        // All delays should be the same
1664        assert_eq!(
1665            strategy.next_delay(0, "error").map(|d| d.to_seconds()),
1666            Some(5)
1667        );
1668        assert_eq!(
1669            strategy.next_delay(1, "error").map(|d| d.to_seconds()),
1670            Some(5)
1671        );
1672        assert_eq!(
1673            strategy.next_delay(2, "error").map(|d| d.to_seconds()),
1674            Some(5)
1675        );
1676        // Exceeds max_attempts
1677        assert_eq!(strategy.next_delay(3, "error"), None);
1678    }
1679
1680    #[test]
1681    fn test_linear_backoff_new() {
1682        let strategy = LinearBackoff::new(5, Duration::from_seconds(2));
1683        assert_eq!(strategy.max_attempts, 5);
1684        assert_eq!(strategy.base_delay.to_seconds(), 2);
1685        assert_eq!(strategy.max_delay.to_seconds(), 3600); // 1 hour default
1686    }
1687
1688    #[test]
1689    fn test_linear_backoff_with_max_delay() {
1690        let strategy = LinearBackoff::new(5, Duration::from_seconds(2))
1691            .with_max_delay(Duration::from_seconds(10));
1692        assert_eq!(strategy.max_delay.to_seconds(), 10);
1693    }
1694
1695    #[test]
1696    fn test_linear_backoff_delays() {
1697        let strategy = LinearBackoff::new(5, Duration::from_seconds(2));
1698
1699        // attempt 0: 2 * (0+1) = 2 seconds
1700        assert_eq!(
1701            strategy.next_delay(0, "error").map(|d| d.to_seconds()),
1702            Some(2)
1703        );
1704        // attempt 1: 2 * (1+1) = 4 seconds
1705        assert_eq!(
1706            strategy.next_delay(1, "error").map(|d| d.to_seconds()),
1707            Some(4)
1708        );
1709        // attempt 2: 2 * (2+1) = 6 seconds
1710        assert_eq!(
1711            strategy.next_delay(2, "error").map(|d| d.to_seconds()),
1712            Some(6)
1713        );
1714        // attempt 3: 2 * (3+1) = 8 seconds
1715        assert_eq!(
1716            strategy.next_delay(3, "error").map(|d| d.to_seconds()),
1717            Some(8)
1718        );
1719        // attempt 4: 2 * (4+1) = 10 seconds
1720        assert_eq!(
1721            strategy.next_delay(4, "error").map(|d| d.to_seconds()),
1722            Some(10)
1723        );
1724        // attempt 5: exceeds max_attempts
1725        assert_eq!(strategy.next_delay(5, "error"), None);
1726    }
1727
1728    #[test]
1729    fn test_linear_backoff_max_delay_cap() {
1730        let strategy = LinearBackoff::new(10, Duration::from_seconds(5))
1731            .with_max_delay(Duration::from_seconds(15));
1732
1733        // attempt 0: 5 * 1 = 5 seconds
1734        assert_eq!(
1735            strategy.next_delay(0, "error").map(|d| d.to_seconds()),
1736            Some(5)
1737        );
1738        // attempt 1: 5 * 2 = 10 seconds
1739        assert_eq!(
1740            strategy.next_delay(1, "error").map(|d| d.to_seconds()),
1741            Some(10)
1742        );
1743        // attempt 2: 5 * 3 = 15 seconds
1744        assert_eq!(
1745            strategy.next_delay(2, "error").map(|d| d.to_seconds()),
1746            Some(15)
1747        );
1748        // attempt 3: 5 * 4 = 20 seconds, capped at 15
1749        assert_eq!(
1750            strategy.next_delay(3, "error").map(|d| d.to_seconds()),
1751            Some(15)
1752        );
1753    }
1754
1755    #[test]
1756    fn test_no_retry() {
1757        let strategy = NoRetry;
1758
1759        // Should always return None
1760        assert_eq!(strategy.next_delay(0, "error"), None);
1761        assert_eq!(strategy.next_delay(1, "error"), None);
1762        assert_eq!(strategy.next_delay(100, "error"), None);
1763    }
1764
1765    #[test]
1766    fn test_no_retry_default() {
1767        let strategy = NoRetry::default();
1768        assert_eq!(strategy.next_delay(0, "error"), None);
1769    }
1770
1771    #[test]
1772    fn test_custom_retry_basic() {
1773        let strategy = custom_retry(|attempt, _error| {
1774            if attempt >= 3 {
1775                None
1776            } else {
1777                Some(Duration::from_seconds(10))
1778            }
1779        });
1780
1781        assert_eq!(
1782            strategy.next_delay(0, "error").map(|d| d.to_seconds()),
1783            Some(10)
1784        );
1785        assert_eq!(
1786            strategy.next_delay(1, "error").map(|d| d.to_seconds()),
1787            Some(10)
1788        );
1789        assert_eq!(
1790            strategy.next_delay(2, "error").map(|d| d.to_seconds()),
1791            Some(10)
1792        );
1793        assert_eq!(strategy.next_delay(3, "error"), None);
1794    }
1795
1796    #[test]
1797    fn test_custom_retry_error_based() {
1798        let strategy = custom_retry(|attempt, error| {
1799            if attempt >= 5 {
1800                return None;
1801            }
1802            if error.contains("transient") {
1803                Some(Duration::from_seconds(1))
1804            } else if error.contains("rate_limit") {
1805                Some(Duration::from_seconds(30))
1806            } else {
1807                None // Don't retry other errors
1808            }
1809        });
1810
1811        // Transient errors get short delay
1812        assert_eq!(
1813            strategy
1814                .next_delay(0, "transient error")
1815                .map(|d| d.to_seconds()),
1816            Some(1)
1817        );
1818        // Rate limit errors get longer delay
1819        assert_eq!(
1820            strategy
1821                .next_delay(0, "rate_limit exceeded")
1822                .map(|d| d.to_seconds()),
1823            Some(30)
1824        );
1825        // Other errors don't retry
1826        assert_eq!(strategy.next_delay(0, "permanent failure"), None);
1827    }
1828
1829    #[test]
1830    fn test_retry_strategy_clone_box() {
1831        // Test that clone_box works for all strategies
1832        let exp: Box<dyn RetryStrategy> =
1833            Box::new(ExponentialBackoff::new(3, Duration::from_seconds(1)));
1834        let exp_clone = exp.clone_box();
1835        assert_eq!(
1836            exp.next_delay(0, "e").map(|d| d.to_seconds()),
1837            exp_clone.next_delay(0, "e").map(|d| d.to_seconds())
1838        );
1839
1840        let fixed: Box<dyn RetryStrategy> = Box::new(FixedDelay::new(3, Duration::from_seconds(5)));
1841        let fixed_clone = fixed.clone_box();
1842        assert_eq!(
1843            fixed.next_delay(0, "e").map(|d| d.to_seconds()),
1844            fixed_clone.next_delay(0, "e").map(|d| d.to_seconds())
1845        );
1846
1847        let linear: Box<dyn RetryStrategy> =
1848            Box::new(LinearBackoff::new(3, Duration::from_seconds(2)));
1849        let linear_clone = linear.clone_box();
1850        assert_eq!(
1851            linear.next_delay(0, "e").map(|d| d.to_seconds()),
1852            linear_clone.next_delay(0, "e").map(|d| d.to_seconds())
1853        );
1854
1855        let no_retry: Box<dyn RetryStrategy> = Box::new(NoRetry);
1856        let no_retry_clone = no_retry.clone_box();
1857        assert_eq!(
1858            no_retry.next_delay(0, "e"),
1859            no_retry_clone.next_delay(0, "e")
1860        );
1861    }
1862
1863    #[test]
1864    fn test_boxed_retry_strategy_clone() {
1865        // Test the Clone impl for Box<dyn RetryStrategy>
1866        let strategy: Box<dyn RetryStrategy> =
1867            Box::new(ExponentialBackoff::new(3, Duration::from_seconds(1)));
1868        let cloned = strategy.clone();
1869
1870        assert_eq!(
1871            strategy.next_delay(0, "error").map(|d| d.to_seconds()),
1872            cloned.next_delay(0, "error").map(|d| d.to_seconds())
1873        );
1874    }
1875
1876    #[test]
1877    fn test_step_config_with_retry_strategy() {
1878        let config = StepConfig {
1879            retry_strategy: Some(Box::new(ExponentialBackoff::new(
1880                3,
1881                Duration::from_seconds(1),
1882            ))),
1883            step_semantics: StepSemantics::AtLeastOncePerRetry,
1884            serdes: None,
1885            retryable_error_filter: None,
1886        };
1887
1888        assert!(config.retry_strategy.is_some());
1889        let strategy = config.retry_strategy.as_ref().unwrap();
1890        assert_eq!(
1891            strategy.next_delay(0, "error").map(|d| d.to_seconds()),
1892            Some(1)
1893        );
1894    }
1895
1896    #[test]
1897    fn test_retry_strategy_debug() {
1898        // Test Debug implementations
1899        let exp = ExponentialBackoff::new(3, Duration::from_seconds(1));
1900        let debug_str = format!("{:?}", exp);
1901        assert!(debug_str.contains("ExponentialBackoff"));
1902
1903        let fixed = FixedDelay::new(3, Duration::from_seconds(5));
1904        let debug_str = format!("{:?}", fixed);
1905        assert!(debug_str.contains("FixedDelay"));
1906
1907        let linear = LinearBackoff::new(3, Duration::from_seconds(2));
1908        let debug_str = format!("{:?}", linear);
1909        assert!(debug_str.contains("LinearBackoff"));
1910
1911        let no_retry = NoRetry;
1912        let debug_str = format!("{:?}", no_retry);
1913        assert!(debug_str.contains("NoRetry"));
1914
1915        let custom = custom_retry(|_, _| None);
1916        let debug_str = format!("{:?}", custom);
1917        assert!(debug_str.contains("CustomRetry"));
1918    }
1919
1920    // =========================================================================
1921    // Property-Based Tests
1922    // =========================================================================
1923
1924    /// Strategy for generating valid StepSemantics values
1925    fn step_semantics_strategy() -> impl Strategy<Value = StepSemantics> {
1926        prop_oneof![
1927            Just(StepSemantics::AtMostOncePerRetry),
1928            Just(StepSemantics::AtLeastOncePerRetry),
1929        ]
1930    }
1931
1932    /// Strategy for generating valid CheckpointingMode values
1933    fn checkpointing_mode_strategy() -> impl Strategy<Value = CheckpointingMode> {
1934        prop_oneof![
1935            Just(CheckpointingMode::Eager),
1936            Just(CheckpointingMode::Batched),
1937            Just(CheckpointingMode::Optimistic),
1938        ]
1939    }
1940
1941    proptest! {
1942        // **Feature: rust-sdk-test-suite, Property: StepConfig validity**
1943        // **Validates: Requirements 5.1**
1944        /// Property: For any valid StepConfig instance, the configuration SHALL be usable without panics.
1945        /// StepConfig with any StepSemantics value should be valid and usable.
1946        #[test]
1947        fn prop_step_config_validity(semantics in step_semantics_strategy()) {
1948            let config = StepConfig {
1949                retry_strategy: None,
1950                step_semantics: semantics,
1951                serdes: None,
1952                retryable_error_filter: None,
1953            };
1954
1955            // Verify the config is usable - accessing fields should not panic
1956            let _ = config.retry_strategy.is_none();
1957            let _ = config.step_semantics;
1958            let _ = config.serdes.is_none();
1959
1960            // Verify Debug trait works
1961            let debug_str = format!("{:?}", config);
1962            prop_assert!(!debug_str.is_empty());
1963        }
1964
1965        // **Feature: rust-sdk-test-suite, Property: CallbackConfig with positive timeout values**
1966        // **Validates: Requirements 5.2**
1967        /// Property: For any valid CallbackConfig with positive timeout values, the configuration SHALL be valid.
1968        #[test]
1969        fn prop_callback_config_positive_timeout(
1970            timeout_secs in 1u64..=86400u64,
1971            heartbeat_secs in 1u64..=86400u64
1972        ) {
1973            let config = CallbackConfig {
1974                timeout: Duration::from_seconds(timeout_secs),
1975                heartbeat_timeout: Duration::from_seconds(heartbeat_secs),
1976                serdes: None,
1977            };
1978
1979            // Verify the config has the expected timeout values
1980            prop_assert_eq!(config.timeout.to_seconds(), timeout_secs);
1981            prop_assert_eq!(config.heartbeat_timeout.to_seconds(), heartbeat_secs);
1982
1983            // Verify Debug trait works
1984            let debug_str = format!("{:?}", config);
1985            prop_assert!(!debug_str.is_empty());
1986        }
1987
1988        // **Feature: rust-sdk-test-suite, Property 12: Duration conversion round-trip**
1989        // **Validates: Requirements 5.3**
1990        /// Property: For any Duration value, converting to seconds and back SHALL preserve the value.
1991        #[test]
1992        fn prop_duration_conversion_roundtrip(seconds in 0u64..=u64::MAX / 2) {
1993            let original = Duration::from_seconds(seconds);
1994            let extracted = original.to_seconds();
1995            let reconstructed = Duration::from_seconds(extracted);
1996
1997            prop_assert_eq!(original, reconstructed);
1998            prop_assert_eq!(original.to_seconds(), reconstructed.to_seconds());
1999        }
2000
2001        // **Feature: rust-sdk-test-suite, Property: RetryStrategy consistency**
2002        // **Validates: Requirements 5.4**
2003        /// Property: For any CompletionConfig, the configuration SHALL produce consistent behavior.
2004        /// Since RetryStrategy is a sealed trait, we test CompletionConfig which is the main
2005        /// configurable retry-related type.
2006        #[test]
2007        fn prop_completion_config_consistency(
2008            min_successful in proptest::option::of(0usize..100),
2009            tolerated_count in proptest::option::of(0usize..100),
2010            tolerated_pct in proptest::option::of(0.0f64..=1.0f64)
2011        ) {
2012            let config = CompletionConfig {
2013                min_successful,
2014                tolerated_failure_count: tolerated_count,
2015                tolerated_failure_percentage: tolerated_pct,
2016            };
2017
2018            // Verify the config has the expected values
2019            prop_assert_eq!(config.min_successful, min_successful);
2020            prop_assert_eq!(config.tolerated_failure_count, tolerated_count);
2021            prop_assert_eq!(config.tolerated_failure_percentage, tolerated_pct);
2022
2023            // Verify serialization round-trip
2024            let serialized = serde_json::to_string(&config).unwrap();
2025            let deserialized: CompletionConfig = serde_json::from_str(&serialized).unwrap();
2026
2027            prop_assert_eq!(config.min_successful, deserialized.min_successful);
2028            prop_assert_eq!(config.tolerated_failure_count, deserialized.tolerated_failure_count);
2029            // For f64, we need to handle NaN specially
2030            match (config.tolerated_failure_percentage, deserialized.tolerated_failure_percentage) {
2031                (Some(a), Some(b)) => prop_assert!((a - b).abs() < f64::EPSILON),
2032                (None, None) => {},
2033                _ => prop_assert!(false, "tolerated_failure_percentage mismatch"),
2034            }
2035        }
2036
2037        // **Feature: rust-sdk-test-suite, Property: CheckpointingMode serialization round-trip**
2038        // **Validates: Requirements 5.1**
2039        /// Property: For any CheckpointingMode value, serializing then deserializing SHALL produce the same value.
2040        #[test]
2041        fn prop_checkpointing_mode_roundtrip(mode in checkpointing_mode_strategy()) {
2042            let serialized = serde_json::to_string(&mode).unwrap();
2043            let deserialized: CheckpointingMode = serde_json::from_str(&serialized).unwrap();
2044            prop_assert_eq!(mode, deserialized);
2045        }
2046
2047        // **Feature: rust-sdk-test-suite, Property: CheckpointingMode classification consistency**
2048        // **Validates: Requirements 5.1**
2049        /// Property: For any CheckpointingMode, exactly one of is_eager/is_batched/is_optimistic SHALL be true.
2050        #[test]
2051        fn prop_checkpointing_mode_classification(mode in checkpointing_mode_strategy()) {
2052            let eager = mode.is_eager();
2053            let batched = mode.is_batched();
2054            let optimistic = mode.is_optimistic();
2055
2056            // Exactly one should be true
2057            let count = [eager, batched, optimistic].iter().filter(|&&x| x).count();
2058            prop_assert_eq!(count, 1, "Exactly one classification should be true");
2059
2060            // Verify consistency with the enum variant
2061            match mode {
2062                CheckpointingMode::Eager => prop_assert!(eager),
2063                CheckpointingMode::Batched => prop_assert!(batched),
2064                CheckpointingMode::Optimistic => prop_assert!(optimistic),
2065            }
2066        }
2067
2068        // **Feature: rust-sdk-test-suite, Property: StepSemantics serialization round-trip**
2069        // **Validates: Requirements 5.1**
2070        /// Property: For any StepSemantics value, serializing then deserializing SHALL produce the same value.
2071        #[test]
2072        fn prop_step_semantics_roundtrip(semantics in step_semantics_strategy()) {
2073            let serialized = serde_json::to_string(&semantics).unwrap();
2074            let deserialized: StepSemantics = serde_json::from_str(&serialized).unwrap();
2075            prop_assert_eq!(semantics, deserialized);
2076        }
2077
2078        // **Feature: rust-sdk-test-suite, Property: ItemBatcher validity**
2079        // **Validates: Requirements 5.1**
2080        /// Property: For any ItemBatcher with positive values, the configuration SHALL be valid.
2081        #[test]
2082        fn prop_item_batcher_validity(
2083            max_items in 1usize..=10000,
2084            max_bytes in 1usize..=10_000_000
2085        ) {
2086            let batcher = ItemBatcher::new(max_items, max_bytes);
2087
2088            prop_assert_eq!(batcher.max_items_per_batch, max_items);
2089            prop_assert_eq!(batcher.max_bytes_per_batch, max_bytes);
2090
2091            // Verify Debug trait works
2092            let debug_str = format!("{:?}", batcher);
2093            prop_assert!(!debug_str.is_empty());
2094        }
2095
2096        // **Feature: rust-sdk-test-suite, Property: ChildConfig builder pattern consistency**
2097        // **Validates: Requirements 5.1**
2098        /// Property: For any ChildConfig, the builder pattern SHALL produce consistent results.
2099        #[test]
2100        fn prop_child_config_builder_consistency(replay_children in proptest::bool::ANY) {
2101            let config = ChildConfig::new().set_replay_children(replay_children);
2102
2103            prop_assert_eq!(config.replay_children, replay_children);
2104
2105            // Verify Debug trait works
2106            let debug_str = format!("{:?}", config);
2107            prop_assert!(!debug_str.is_empty());
2108        }
2109
2110        // **Feature: rust-sdk-test-suite, Property: MapConfig validity**
2111        // **Validates: Requirements 5.1**
2112        /// Property: For any MapConfig with valid values, the configuration SHALL be usable.
2113        #[test]
2114        fn prop_map_config_validity(
2115            max_concurrency in proptest::option::of(1usize..=1000)
2116        ) {
2117            let config = MapConfig {
2118                max_concurrency,
2119                item_batcher: None,
2120                completion_config: CompletionConfig::default(),
2121                serdes: None,
2122            };
2123
2124            prop_assert_eq!(config.max_concurrency, max_concurrency);
2125
2126            // Verify Debug trait works
2127            let debug_str = format!("{:?}", config);
2128            prop_assert!(!debug_str.is_empty());
2129        }
2130
2131        // **Feature: rust-sdk-test-suite, Property: ParallelConfig validity**
2132        // **Validates: Requirements 5.1**
2133        /// Property: For any ParallelConfig with valid values, the configuration SHALL be usable.
2134        #[test]
2135        fn prop_parallel_config_validity(
2136            max_concurrency in proptest::option::of(1usize..=1000)
2137        ) {
2138            let config = ParallelConfig {
2139                max_concurrency,
2140                completion_config: CompletionConfig::default(),
2141                serdes: None,
2142            };
2143
2144            prop_assert_eq!(config.max_concurrency, max_concurrency);
2145
2146            // Verify Debug trait works
2147            let debug_str = format!("{:?}", config);
2148            prop_assert!(!debug_str.is_empty());
2149        }
2150
2151        // **Feature: sdk-ergonomics-improvements, Property 5: ItemBatcher Configuration Respected**
2152        // **Validates: Requirements 2.1, 2.2**
2153        /// Property: For any ItemBatcher configuration with max_items_per_batch and max_bytes_per_batch,
2154        /// the batch method SHALL produce batches where each batch has at most max_items_per_batch items
2155        /// AND at most max_bytes_per_batch bytes (estimated).
2156        #[test]
2157        fn prop_item_batcher_configuration_respected(
2158            max_items in 1usize..=50,
2159            max_bytes in 100usize..=10000,
2160            item_count in 0usize..=200
2161        ) {
2162            let batcher = ItemBatcher::new(max_items, max_bytes);
2163
2164            // Generate items of varying sizes (strings of different lengths)
2165            let items: Vec<String> = (0..item_count)
2166                .map(|i| format!("item_{:04}", i))
2167                .collect();
2168
2169            let batches = batcher.batch(&items);
2170
2171            // Verify each batch respects the item count limit
2172            for (_, batch) in &batches {
2173                prop_assert!(
2174                    batch.len() <= max_items,
2175                    "Batch has {} items but max is {}",
2176                    batch.len(),
2177                    max_items
2178                );
2179            }
2180
2181            // Verify each batch respects the byte limit (with tolerance for single large items)
2182            for (_, batch) in &batches {
2183                let batch_bytes: usize = batch.iter()
2184                    .map(|item| serde_json::to_string(item).map(|s| s.len()).unwrap_or(0))
2185                    .sum();
2186
2187                // A batch may exceed max_bytes only if it contains a single item
2188                // (we can't split a single item)
2189                if batch.len() > 1 {
2190                    prop_assert!(
2191                        batch_bytes <= max_bytes,
2192                        "Batch has {} bytes but max is {} (batch has {} items)",
2193                        batch_bytes,
2194                        max_bytes,
2195                        batch.len()
2196                    );
2197                }
2198            }
2199        }
2200
2201        // **Feature: sdk-ergonomics-improvements, Property 6: ItemBatcher Ordering Preservation**
2202        // **Validates: Requirements 2.3, 2.4, 2.6, 2.7**
2203        /// Property: For any list of items, after batching with ItemBatcher, concatenating all batches
2204        /// in order SHALL produce a list equal to the original input list.
2205        #[test]
2206        fn prop_item_batcher_ordering_preservation(
2207            max_items in 1usize..=50,
2208            max_bytes in 100usize..=10000,
2209            item_count in 0usize..=200
2210        ) {
2211            let batcher = ItemBatcher::new(max_items, max_bytes);
2212
2213            // Generate items with unique identifiers to verify ordering
2214            let items: Vec<String> = (0..item_count)
2215                .map(|i| format!("item_{:04}", i))
2216                .collect();
2217
2218            let batches = batcher.batch(&items);
2219
2220            // Concatenate all batches in order
2221            let reconstructed: Vec<String> = batches
2222                .into_iter()
2223                .flat_map(|(_, batch)| batch)
2224                .collect();
2225
2226            // Verify the reconstructed list equals the original
2227            prop_assert_eq!(
2228                items.len(),
2229                reconstructed.len(),
2230                "Reconstructed list has different length: expected {}, got {}",
2231                items.len(),
2232                reconstructed.len()
2233            );
2234
2235            for (i, (original, reconstructed_item)) in items.iter().zip(reconstructed.iter()).enumerate() {
2236                prop_assert_eq!(
2237                    original,
2238                    reconstructed_item,
2239                    "Item at index {} differs: expected '{}', got '{}'",
2240                    i,
2241                    original,
2242                    reconstructed_item
2243                );
2244            }
2245        }
2246    }
2247
2248    // =========================================================================
2249    // JitterStrategy Unit Tests
2250    // =========================================================================
2251
2252    #[test]
2253    fn test_jitter_strategy_none_returns_exact_delay() {
2254        let jitter = JitterStrategy::None;
2255        assert_eq!(jitter.apply(10.0, 0), 10.0);
2256        assert_eq!(jitter.apply(5.5, 3), 5.5);
2257        assert_eq!(jitter.apply(0.0, 0), 0.0);
2258        assert_eq!(jitter.apply(100.0, 99), 100.0);
2259    }
2260
2261    #[test]
2262    fn test_jitter_strategy_full_bounds() {
2263        let jitter = JitterStrategy::Full;
2264        for attempt in 0..20 {
2265            let result = jitter.apply(10.0, attempt);
2266            assert!(
2267                result >= 0.0 && result <= 10.0,
2268                "Full jitter for attempt {} produced {}, expected [0, 10]",
2269                attempt,
2270                result
2271            );
2272        }
2273    }
2274
2275    #[test]
2276    fn test_jitter_strategy_half_bounds() {
2277        let jitter = JitterStrategy::Half;
2278        for attempt in 0..20 {
2279            let result = jitter.apply(10.0, attempt);
2280            assert!(
2281                result >= 5.0 && result <= 10.0,
2282                "Half jitter for attempt {} produced {}, expected [5, 10]",
2283                attempt,
2284                result
2285            );
2286        }
2287    }
2288
2289    #[test]
2290    fn test_jitter_strategy_deterministic() {
2291        // Same inputs should always produce the same output
2292        let full = JitterStrategy::Full;
2293        let r1 = full.apply(10.0, 5);
2294        let r2 = full.apply(10.0, 5);
2295        assert_eq!(r1, r2);
2296
2297        let half = JitterStrategy::Half;
2298        let r1 = half.apply(10.0, 5);
2299        let r2 = half.apply(10.0, 5);
2300        assert_eq!(r1, r2);
2301    }
2302
2303    #[test]
2304    fn test_jitter_strategy_zero_delay() {
2305        // Jitter with zero delay should return 0
2306        assert_eq!(JitterStrategy::Full.apply(0.0, 0), 0.0);
2307        assert_eq!(JitterStrategy::Half.apply(0.0, 0), 0.0);
2308        assert_eq!(JitterStrategy::None.apply(0.0, 0), 0.0);
2309    }
2310
2311    #[test]
2312    fn test_jitter_strategy_default_is_none() {
2313        assert_eq!(JitterStrategy::default(), JitterStrategy::None);
2314    }
2315
2316    // =========================================================================
2317    // Retry Strategy with Jitter Integration Tests
2318    // =========================================================================
2319
2320    #[test]
2321    fn test_exponential_backoff_with_full_jitter() {
2322        let strategy = ExponentialBackoff::builder()
2323            .max_attempts(5)
2324            .base_delay(Duration::from_seconds(5))
2325            .max_delay(Duration::from_seconds(60))
2326            .jitter(JitterStrategy::Full)
2327            .build();
2328
2329        for attempt in 0..5 {
2330            let delay = strategy.next_delay(attempt, "error");
2331            assert!(delay.is_some());
2332            let secs = delay.unwrap().to_seconds();
2333            // With full jitter, delay should be >= 1 (minimum floor)
2334            assert!(secs >= 1, "Attempt {} delay {} < 1", attempt, secs);
2335        }
2336        assert!(strategy.next_delay(5, "error").is_none());
2337    }
2338
2339    #[test]
2340    fn test_exponential_backoff_with_half_jitter() {
2341        let strategy = ExponentialBackoff::builder()
2342            .max_attempts(5)
2343            .base_delay(Duration::from_seconds(10))
2344            .max_delay(Duration::from_seconds(60))
2345            .jitter(JitterStrategy::Half)
2346            .build();
2347
2348        for attempt in 0..5 {
2349            let delay = strategy.next_delay(attempt, "error");
2350            assert!(delay.is_some());
2351            let secs = delay.unwrap().to_seconds();
2352            assert!(secs >= 1, "Attempt {} delay {} < 1", attempt, secs);
2353        }
2354    }
2355
2356    #[test]
2357    fn test_exponential_backoff_no_jitter_unchanged() {
2358        // Verify backward compatibility: no jitter produces same results as before
2359        let strategy = ExponentialBackoff::new(5, Duration::from_seconds(1));
2360        assert_eq!(strategy.jitter, JitterStrategy::None);
2361        assert_eq!(strategy.next_delay(0, "e").map(|d| d.to_seconds()), Some(1));
2362        assert_eq!(strategy.next_delay(1, "e").map(|d| d.to_seconds()), Some(2));
2363        assert_eq!(strategy.next_delay(2, "e").map(|d| d.to_seconds()), Some(4));
2364    }
2365
2366    #[test]
2367    fn test_fixed_delay_with_jitter() {
2368        let strategy =
2369            FixedDelay::new(3, Duration::from_seconds(10)).with_jitter(JitterStrategy::Full);
2370
2371        for attempt in 0..3 {
2372            let delay = strategy.next_delay(attempt, "error");
2373            assert!(delay.is_some());
2374            let secs = delay.unwrap().to_seconds();
2375            assert!(secs >= 1, "Attempt {} delay {} < 1", attempt, secs);
2376        }
2377        assert!(strategy.next_delay(3, "error").is_none());
2378    }
2379
2380    #[test]
2381    fn test_fixed_delay_no_jitter_unchanged() {
2382        let strategy = FixedDelay::new(3, Duration::from_seconds(5));
2383        assert_eq!(strategy.jitter, JitterStrategy::None);
2384        assert_eq!(strategy.next_delay(0, "e").map(|d| d.to_seconds()), Some(5));
2385        assert_eq!(strategy.next_delay(1, "e").map(|d| d.to_seconds()), Some(5));
2386    }
2387
2388    #[test]
2389    fn test_linear_backoff_with_jitter() {
2390        let strategy =
2391            LinearBackoff::new(5, Duration::from_seconds(5)).with_jitter(JitterStrategy::Half);
2392
2393        for attempt in 0..5 {
2394            let delay = strategy.next_delay(attempt, "error");
2395            assert!(delay.is_some());
2396            let secs = delay.unwrap().to_seconds();
2397            assert!(secs >= 1, "Attempt {} delay {} < 1", attempt, secs);
2398        }
2399        assert!(strategy.next_delay(5, "error").is_none());
2400    }
2401
2402    #[test]
2403    fn test_linear_backoff_no_jitter_unchanged() {
2404        let strategy = LinearBackoff::new(5, Duration::from_seconds(2));
2405        assert_eq!(strategy.jitter, JitterStrategy::None);
2406        assert_eq!(strategy.next_delay(0, "e").map(|d| d.to_seconds()), Some(2));
2407        assert_eq!(strategy.next_delay(1, "e").map(|d| d.to_seconds()), Some(4));
2408    }
2409
2410    #[test]
2411    fn test_jitter_minimum_floor_all_strategies() {
2412        // Even with full jitter on small delays, minimum should be 1 second
2413        let exp = ExponentialBackoff::builder()
2414            .max_attempts(3)
2415            .base_delay(Duration::from_seconds(1))
2416            .jitter(JitterStrategy::Full)
2417            .build();
2418        for attempt in 0..3 {
2419            let secs = exp.next_delay(attempt, "e").unwrap().to_seconds();
2420            assert!(
2421                secs >= 1,
2422                "ExponentialBackoff attempt {} delay {} < 1",
2423                attempt,
2424                secs
2425            );
2426        }
2427
2428        let fixed = FixedDelay::new(3, Duration::from_seconds(1)).with_jitter(JitterStrategy::Full);
2429        for attempt in 0..3 {
2430            let secs = fixed.next_delay(attempt, "e").unwrap().to_seconds();
2431            assert!(
2432                secs >= 1,
2433                "FixedDelay attempt {} delay {} < 1",
2434                attempt,
2435                secs
2436            );
2437        }
2438
2439        let linear =
2440            LinearBackoff::new(3, Duration::from_seconds(1)).with_jitter(JitterStrategy::Full);
2441        for attempt in 0..3 {
2442            let secs = linear.next_delay(attempt, "e").unwrap().to_seconds();
2443            assert!(
2444                secs >= 1,
2445                "LinearBackoff attempt {} delay {} < 1",
2446                attempt,
2447                secs
2448            );
2449        }
2450    }
2451
2452    // =========================================================================
2453    // JitterStrategy Property-Based Tests
2454    // =========================================================================
2455
2456    /// Strategy for generating valid JitterStrategy values
2457    fn jitter_strategy_strategy() -> impl Strategy<Value = JitterStrategy> {
2458        prop_oneof![
2459            Just(JitterStrategy::None),
2460            Just(JitterStrategy::Full),
2461            Just(JitterStrategy::Half),
2462        ]
2463    }
2464
2465    proptest! {
2466        // **Feature: rust-sdk-parity-gaps, Property: JitterStrategy::None identity**
2467        // **Validates: Requirements 1.2**
2468        /// Property: JitterStrategy::None SHALL return the exact delay for any delay and attempt.
2469        #[test]
2470        fn prop_jitter_none_identity(delay in 0.0f64..1000.0, attempt in 0u32..100) {
2471            let result = JitterStrategy::None.apply(delay, attempt);
2472            prop_assert!((result - delay).abs() < f64::EPSILON,
2473                "None jitter changed delay from {} to {}", delay, result);
2474        }
2475
2476        // **Feature: rust-sdk-parity-gaps, Property: JitterStrategy::Full bounds**
2477        // **Validates: Requirements 1.3**
2478        /// Property: JitterStrategy::Full SHALL return a delay in [0, d] for any non-negative delay.
2479        #[test]
2480        fn prop_jitter_full_bounds(delay in 0.0f64..1000.0, attempt in 0u32..100) {
2481            let result = JitterStrategy::Full.apply(delay, attempt);
2482            prop_assert!(result >= 0.0, "Full jitter result {} < 0", result);
2483            prop_assert!(result <= delay + f64::EPSILON,
2484                "Full jitter result {} > delay {}", result, delay);
2485        }
2486
2487        // **Feature: rust-sdk-parity-gaps, Property: JitterStrategy::Half bounds**
2488        // **Validates: Requirements 1.4**
2489        /// Property: JitterStrategy::Half SHALL return a delay in [d/2, d] for any non-negative delay.
2490        #[test]
2491        fn prop_jitter_half_bounds(delay in 0.0f64..1000.0, attempt in 0u32..100) {
2492            let result = JitterStrategy::Half.apply(delay, attempt);
2493            prop_assert!(result >= delay / 2.0 - f64::EPSILON,
2494                "Half jitter result {} < delay/2 {}", result, delay / 2.0);
2495            prop_assert!(result <= delay + f64::EPSILON,
2496                "Half jitter result {} > delay {}", result, delay);
2497        }
2498
2499        // **Feature: rust-sdk-parity-gaps, Property: JitterStrategy determinism**
2500        // **Validates: Requirements 1.2, 1.3, 1.4**
2501        /// Property: JitterStrategy::apply SHALL be deterministic for the same inputs.
2502        #[test]
2503        fn prop_jitter_deterministic(
2504            jitter in jitter_strategy_strategy(),
2505            delay in 0.0f64..1000.0,
2506            attempt in 0u32..100
2507        ) {
2508            let r1 = jitter.apply(delay, attempt);
2509            let r2 = jitter.apply(delay, attempt);
2510            prop_assert!((r1 - r2).abs() < f64::EPSILON,
2511                "Jitter not deterministic: {} vs {}", r1, r2);
2512        }
2513
2514        // **Feature: rust-sdk-parity-gaps, Property: Jittered delay minimum floor**
2515        // **Validates: Requirements 1.10**
2516        /// Property: All retry strategies with jitter SHALL produce delays >= 1 second.
2517        #[test]
2518        fn prop_jitter_minimum_floor(
2519            jitter in jitter_strategy_strategy(),
2520            attempt in 0u32..10,
2521            base_delay_secs in 1u64..100
2522        ) {
2523            // ExponentialBackoff
2524            let exp = ExponentialBackoff::builder()
2525                .max_attempts(10)
2526                .base_delay(Duration::from_seconds(base_delay_secs))
2527                .jitter(jitter)
2528                .build();
2529            if let Some(d) = exp.next_delay(attempt, "e") {
2530                prop_assert!(d.to_seconds() >= 1,
2531                    "ExponentialBackoff delay {} < 1 for attempt {}", d.to_seconds(), attempt);
2532            }
2533
2534            // FixedDelay
2535            let fixed = FixedDelay::new(10, Duration::from_seconds(base_delay_secs))
2536                .with_jitter(jitter);
2537            if let Some(d) = fixed.next_delay(attempt, "e") {
2538                prop_assert!(d.to_seconds() >= 1,
2539                    "FixedDelay delay {} < 1 for attempt {}", d.to_seconds(), attempt);
2540            }
2541
2542            // LinearBackoff
2543            let linear = LinearBackoff::new(10, Duration::from_seconds(base_delay_secs))
2544                .with_jitter(jitter);
2545            if let Some(d) = linear.next_delay(attempt, "e") {
2546                prop_assert!(d.to_seconds() >= 1,
2547                    "LinearBackoff delay {} < 1 for attempt {}", d.to_seconds(), attempt);
2548            }
2549        }
2550    }
2551}
2552
2553#[cfg(test)]
2554mod retryable_error_filter_tests {
2555    use super::*;
2556
2557    #[test]
2558    fn test_empty_filter_retries_all() {
2559        let filter = RetryableErrorFilter::default();
2560        assert!(filter.is_retryable("any error message"));
2561        assert!(filter.is_retryable(""));
2562        assert!(filter.is_retryable("timeout"));
2563        assert!(filter.is_retryable_with_type("any error", "AnyType"));
2564    }
2565
2566    #[test]
2567    fn test_contains_pattern_matches_substring() {
2568        let filter = RetryableErrorFilter {
2569            patterns: vec![ErrorPattern::Contains("timeout".to_string())],
2570            error_types: vec![],
2571        };
2572        assert!(filter.is_retryable("request timeout occurred"));
2573        assert!(filter.is_retryable("timeout"));
2574        assert!(filter.is_retryable("a timeout happened"));
2575    }
2576
2577    #[test]
2578    fn test_contains_pattern_no_match() {
2579        let filter = RetryableErrorFilter {
2580            patterns: vec![ErrorPattern::Contains("timeout".to_string())],
2581            error_types: vec![],
2582        };
2583        assert!(!filter.is_retryable("connection refused"));
2584        assert!(!filter.is_retryable("invalid input"));
2585        assert!(!filter.is_retryable(""));
2586    }
2587
2588    #[test]
2589    fn test_regex_pattern_matches() {
2590        let filter = RetryableErrorFilter {
2591            patterns: vec![ErrorPattern::Regex(
2592                regex::Regex::new(r"(?i)connection.*refused").unwrap(),
2593            )],
2594            error_types: vec![],
2595        };
2596        assert!(filter.is_retryable("Connection was refused"));
2597        assert!(filter.is_retryable("connection refused"));
2598        assert!(filter.is_retryable("CONNECTION actively REFUSED"));
2599    }
2600
2601    #[test]
2602    fn test_regex_pattern_no_match() {
2603        let filter = RetryableErrorFilter {
2604            patterns: vec![ErrorPattern::Regex(
2605                regex::Regex::new(r"(?i)connection.*refused").unwrap(),
2606            )],
2607            error_types: vec![],
2608        };
2609        assert!(!filter.is_retryable("timeout error"));
2610        assert!(!filter.is_retryable("refused connection")); // wrong order
2611    }
2612
2613    #[test]
2614    fn test_or_logic_multiple_patterns() {
2615        let filter = RetryableErrorFilter {
2616            patterns: vec![
2617                ErrorPattern::Contains("timeout".to_string()),
2618                ErrorPattern::Regex(regex::Regex::new(r"(?i)connection.*refused").unwrap()),
2619            ],
2620            error_types: vec![],
2621        };
2622        // Matches first pattern
2623        assert!(filter.is_retryable("request timeout"));
2624        // Matches second pattern
2625        assert!(filter.is_retryable("Connection refused"));
2626        // Matches neither
2627        assert!(!filter.is_retryable("invalid input"));
2628    }
2629
2630    #[test]
2631    fn test_error_type_matching() {
2632        let filter = RetryableErrorFilter {
2633            patterns: vec![],
2634            error_types: vec!["TransientError".to_string()],
2635        };
2636        // is_retryable only checks patterns, not types
2637        assert!(!filter.is_retryable("some error"));
2638        // is_retryable_with_type checks both
2639        assert!(filter.is_retryable_with_type("some error", "TransientError"));
2640        assert!(!filter.is_retryable_with_type("some error", "PermanentError"));
2641    }
2642
2643    #[test]
2644    fn test_or_logic_patterns_and_types() {
2645        let filter = RetryableErrorFilter {
2646            patterns: vec![ErrorPattern::Contains("timeout".to_string())],
2647            error_types: vec!["TransientError".to_string()],
2648        };
2649        // Matches pattern only
2650        assert!(filter.is_retryable_with_type("request timeout", "PermanentError"));
2651        // Matches type only
2652        assert!(filter.is_retryable_with_type("invalid input", "TransientError"));
2653        // Matches both
2654        assert!(filter.is_retryable_with_type("request timeout", "TransientError"));
2655        // Matches neither
2656        assert!(!filter.is_retryable_with_type("invalid input", "PermanentError"));
2657    }
2658
2659    #[test]
2660    fn test_error_pattern_debug() {
2661        let contains = ErrorPattern::Contains("test".to_string());
2662        let debug_str = format!("{:?}", contains);
2663        assert!(debug_str.contains("Contains"));
2664        assert!(debug_str.contains("test"));
2665
2666        let regex = ErrorPattern::Regex(regex::Regex::new(r"\d+").unwrap());
2667        let debug_str = format!("{:?}", regex);
2668        assert!(debug_str.contains("Regex"));
2669    }
2670
2671    #[test]
2672    fn test_retryable_error_filter_clone() {
2673        let filter = RetryableErrorFilter {
2674            patterns: vec![
2675                ErrorPattern::Contains("timeout".to_string()),
2676                ErrorPattern::Regex(regex::Regex::new(r"err\d+").unwrap()),
2677            ],
2678            error_types: vec!["TransientError".to_string()],
2679        };
2680        let cloned = filter.clone();
2681        assert!(cloned.is_retryable("timeout error"));
2682        assert!(cloned.is_retryable("err42"));
2683        assert!(cloned.is_retryable_with_type("x", "TransientError"));
2684    }
2685
2686    // ==========================================================================
2687    // Tests for WaitDecision, WaitStrategyConfig, and create_wait_strategy
2688    // Requirements: 4.1–4.6
2689    // ==========================================================================
2690
2691    #[test]
2692    fn test_wait_decision_done_when_predicate_false() {
2693        // **Validates: Requirements 4.1, 4.2**
2694        let strategy = create_wait_strategy(WaitStrategyConfig {
2695            max_attempts: Some(10),
2696            initial_delay: Duration::from_seconds(5),
2697            max_delay: Duration::from_seconds(300),
2698            backoff_rate: 1.5,
2699            jitter: JitterStrategy::None,
2700            should_continue_polling: Box::new(|state: &String| state != "COMPLETED"),
2701        });
2702
2703        // When predicate returns false (state == "COMPLETED"), should return Done
2704        let decision = strategy(&"COMPLETED".to_string(), 1);
2705        assert_eq!(decision, WaitDecision::Done);
2706    }
2707
2708    #[test]
2709    fn test_wait_decision_continue_with_backoff() {
2710        // **Validates: Requirements 4.3, 4.5**
2711        let strategy = create_wait_strategy(WaitStrategyConfig {
2712            max_attempts: Some(10),
2713            initial_delay: Duration::from_seconds(5),
2714            max_delay: Duration::from_seconds(300),
2715            backoff_rate: 2.0,
2716            jitter: JitterStrategy::None,
2717            should_continue_polling: Box::new(|state: &String| state != "DONE"),
2718        });
2719
2720        // Attempt 1: delay = min(5 * 2^0, 300) = 5s
2721        let decision = strategy(&"PENDING".to_string(), 1);
2722        assert_eq!(
2723            decision,
2724            WaitDecision::Continue {
2725                delay: Duration::from_seconds(5)
2726            }
2727        );
2728
2729        // Attempt 2: delay = min(5 * 2^1, 300) = 10s
2730        let decision = strategy(&"PENDING".to_string(), 2);
2731        assert_eq!(
2732            decision,
2733            WaitDecision::Continue {
2734                delay: Duration::from_seconds(10)
2735            }
2736        );
2737
2738        // Attempt 3: delay = min(5 * 2^2, 300) = 20s
2739        let decision = strategy(&"PENDING".to_string(), 3);
2740        assert_eq!(
2741            decision,
2742            WaitDecision::Continue {
2743                delay: Duration::from_seconds(20)
2744            }
2745        );
2746    }
2747
2748    #[test]
2749    fn test_wait_strategy_delay_capped_at_max() {
2750        // **Validates: Requirement 4.5**
2751        let strategy = create_wait_strategy(WaitStrategyConfig {
2752            max_attempts: Some(20),
2753            initial_delay: Duration::from_seconds(10),
2754            max_delay: Duration::from_seconds(30),
2755            backoff_rate: 2.0,
2756            jitter: JitterStrategy::None,
2757            should_continue_polling: Box::new(|_: &i32| true),
2758        });
2759
2760        // Attempt 3: delay = min(10 * 2^2, 30) = min(40, 30) = 30s
2761        let decision = strategy(&0, 3);
2762        assert_eq!(
2763            decision,
2764            WaitDecision::Continue {
2765                delay: Duration::from_seconds(30)
2766            }
2767        );
2768
2769        // Attempt 5: delay = min(10 * 2^4, 30) = min(160, 30) = 30s
2770        let decision = strategy(&0, 5);
2771        assert_eq!(
2772            decision,
2773            WaitDecision::Continue {
2774                delay: Duration::from_seconds(30)
2775            }
2776        );
2777    }
2778
2779    #[test]
2780    fn test_wait_strategy_max_attempts_returns_done() {
2781        // **Validates: Requirement 4.4**
2782        let strategy = create_wait_strategy(WaitStrategyConfig {
2783            max_attempts: Some(3),
2784            initial_delay: Duration::from_seconds(5),
2785            max_delay: Duration::from_seconds(300),
2786            backoff_rate: 1.5,
2787            jitter: JitterStrategy::None,
2788            should_continue_polling: Box::new(|_: &i32| true),
2789        });
2790
2791        // Attempt 3 should return Done (attempts_made >= max_attempts)
2792        let decision = strategy(&0, 3);
2793        assert_eq!(decision, WaitDecision::Done);
2794    }
2795
2796    #[test]
2797    fn test_wait_strategy_jitter_application() {
2798        // **Validates: Requirement 4.6**
2799        let strategy = create_wait_strategy(WaitStrategyConfig {
2800            max_attempts: Some(10),
2801            initial_delay: Duration::from_seconds(10),
2802            max_delay: Duration::from_seconds(300),
2803            backoff_rate: 1.0,
2804            jitter: JitterStrategy::Full,
2805            should_continue_polling: Box::new(|_: &i32| true),
2806        });
2807
2808        // With Full jitter on a 10s base delay, the result should be in [1, 10]
2809        // (floored at 1s minimum)
2810        let decision = strategy(&0, 1);
2811        match decision {
2812            WaitDecision::Continue { delay } => {
2813                assert!(
2814                    delay.to_seconds() >= 1 && delay.to_seconds() <= 10,
2815                    "Jittered delay {} should be in [1, 10]",
2816                    delay.to_seconds()
2817                );
2818            }
2819            WaitDecision::Done => panic!("Expected Continue, got Done"),
2820        }
2821    }
2822
2823    #[test]
2824    fn test_wait_strategy_delay_minimum_floor() {
2825        // **Validates: Requirement 4.3**
2826        let strategy = create_wait_strategy(WaitStrategyConfig {
2827            max_attempts: Some(10),
2828            initial_delay: Duration::from_seconds(1),
2829            max_delay: Duration::from_seconds(300),
2830            backoff_rate: 1.0,
2831            jitter: JitterStrategy::Full,
2832            should_continue_polling: Box::new(|_: &i32| true),
2833        });
2834
2835        // Even with Full jitter that could produce 0, the floor should be 1s
2836        let decision = strategy(&0, 1);
2837        match decision {
2838            WaitDecision::Continue { delay } => {
2839                assert!(
2840                    delay.to_seconds() >= 1,
2841                    "Delay {} should be at least 1 second",
2842                    delay.to_seconds()
2843                );
2844            }
2845            WaitDecision::Done => panic!("Expected Continue, got Done"),
2846        }
2847    }
2848
2849    #[test]
2850    fn test_wait_strategy_default_max_attempts() {
2851        // **Validates: Requirement 4.4** — default max_attempts is 60
2852        let strategy = create_wait_strategy(WaitStrategyConfig {
2853            max_attempts: None, // defaults to 60
2854            initial_delay: Duration::from_seconds(1),
2855            max_delay: Duration::from_seconds(10),
2856            backoff_rate: 1.0,
2857            jitter: JitterStrategy::None,
2858            should_continue_polling: Box::new(|_: &i32| true),
2859        });
2860
2861        // Attempt 59 should succeed (< 60)
2862        let decision = strategy(&0, 59);
2863        assert!(matches!(decision, WaitDecision::Continue { .. }));
2864    }
2865
2866    #[test]
2867    fn test_wait_strategy_default_max_attempts_returns_done() {
2868        // **Validates: Requirement 4.4** — default max_attempts is 60
2869        let strategy = create_wait_strategy(WaitStrategyConfig {
2870            max_attempts: None, // defaults to 60
2871            initial_delay: Duration::from_seconds(1),
2872            max_delay: Duration::from_seconds(10),
2873            backoff_rate: 1.0,
2874            jitter: JitterStrategy::None,
2875            should_continue_polling: Box::new(|_: &i32| true),
2876        });
2877
2878        // Attempt 60 should return Done (>= 60)
2879        let decision = strategy(&0, 60);
2880        assert_eq!(decision, WaitDecision::Done);
2881    }
2882
2883    #[test]
2884    fn test_wait_decision_enum_variants() {
2885        // **Validates: Requirement 4.1**
2886        let cont = WaitDecision::Continue {
2887            delay: Duration::from_seconds(5),
2888        };
2889        let done = WaitDecision::Done;
2890
2891        // Verify Debug
2892        assert!(format!("{:?}", cont).contains("Continue"));
2893        assert!(format!("{:?}", done).contains("Done"));
2894
2895        // Verify PartialEq
2896        assert_eq!(
2897            WaitDecision::Continue {
2898                delay: Duration::from_seconds(5)
2899            },
2900            WaitDecision::Continue {
2901                delay: Duration::from_seconds(5)
2902            }
2903        );
2904        assert_ne!(
2905            WaitDecision::Continue {
2906                delay: Duration::from_seconds(5)
2907            },
2908            WaitDecision::Done
2909        );
2910    }
2911}