Skip to main content

durable_execution_sdk_testing/
test_result.rs

1//! Test result types for durable execution testing.
2//!
3//! This module provides the `TestResult` struct which contains execution results
4//! and provides inspection methods for verifying workflow behavior.
5
6use serde::{de::DeserializeOwned, Deserialize, Serialize};
7
8use crate::checkpoint_server::NodeJsHistoryEvent;
9use crate::error::TestError;
10use crate::types::{ExecutionStatus, Invocation, TestResultError};
11use durable_execution_sdk::{Operation, OperationStatus};
12
13/// Result of a durable execution test.
14///
15/// Contains the execution outcome, captured operations, and provides methods
16/// for inspecting and verifying workflow behavior.
17///
18/// # Type Parameters
19///
20/// * `T` - The type of the successful result value
21///
22/// # Examples
23///
24/// ```ignore
25/// use durable_execution_sdk_testing::{TestResult, ExecutionStatus};
26///
27/// // After running a test
28/// let result: TestResult<String> = runner.run("input").await?;
29///
30/// // Check status
31/// assert_eq!(result.get_status(), ExecutionStatus::Succeeded);
32///
33/// // Get the result value
34/// let value = result.get_result()?;
35/// assert_eq!(value, "expected output");
36///
37/// // Inspect operations
38/// let ops = result.get_operations();
39/// assert_eq!(ops.len(), 3);
40/// ```
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct TestResult<T> {
43    /// The execution status
44    status: ExecutionStatus,
45    /// The result value if execution succeeded
46    result: Option<T>,
47    /// Error information if execution failed
48    error: Option<TestResultError>,
49    /// All operations captured during execution
50    operations: Vec<Operation>,
51    /// Handler invocation details
52    invocations: Vec<Invocation>,
53    /// History events from the execution
54    history_events: Vec<HistoryEvent>,
55    /// Node.js SDK compatible history events
56    ///
57    /// These events match the Node.js SDK's event history format exactly,
58    /// enabling cross-SDK history comparison.
59    nodejs_history_events: Vec<NodeJsHistoryEvent>,
60}
61
62/// A history event from the execution.
63///
64/// Represents significant events that occurred during the durable execution.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct HistoryEvent {
67    /// Event type identifier
68    pub event_type: String,
69    /// Timestamp of the event (milliseconds since epoch)
70    pub timestamp: Option<i64>,
71    /// Associated operation ID if applicable
72    pub operation_id: Option<String>,
73    /// Additional event data
74    pub data: Option<String>,
75}
76
77impl HistoryEvent {
78    /// Creates a new history event.
79    pub fn new(event_type: impl Into<String>) -> Self {
80        Self {
81            event_type: event_type.into(),
82            timestamp: None,
83            operation_id: None,
84            data: None,
85        }
86    }
87
88    /// Sets the timestamp.
89    pub fn with_timestamp(mut self, timestamp: i64) -> Self {
90        self.timestamp = Some(timestamp);
91        self
92    }
93
94    /// Sets the operation ID.
95    pub fn with_operation_id(mut self, operation_id: impl Into<String>) -> Self {
96        self.operation_id = Some(operation_id.into());
97        self
98    }
99
100    /// Sets the event data.
101    pub fn with_data(mut self, data: impl Into<String>) -> Self {
102        self.data = Some(data.into());
103        self
104    }
105}
106
107impl<T> TestResult<T> {
108    /// Creates a new successful TestResult.
109    pub fn success(result: T, operations: Vec<Operation>) -> Self {
110        Self {
111            status: ExecutionStatus::Succeeded,
112            result: Some(result),
113            error: None,
114            operations,
115            invocations: Vec::new(),
116            history_events: Vec::new(),
117            nodejs_history_events: Vec::new(),
118        }
119    }
120
121    /// Creates a new failed TestResult.
122    pub fn failure(error: TestResultError, operations: Vec<Operation>) -> Self {
123        Self {
124            status: ExecutionStatus::Failed,
125            result: None,
126            error: Some(error),
127            operations,
128            invocations: Vec::new(),
129            history_events: Vec::new(),
130            nodejs_history_events: Vec::new(),
131        }
132    }
133
134    /// Creates a new TestResult with a specific status.
135    pub fn with_status(status: ExecutionStatus, operations: Vec<Operation>) -> Self {
136        Self {
137            status,
138            result: None,
139            error: None,
140            operations,
141            invocations: Vec::new(),
142            history_events: Vec::new(),
143            nodejs_history_events: Vec::new(),
144        }
145    }
146
147    /// Sets the result value.
148    pub fn set_result(&mut self, result: T) {
149        self.result = Some(result);
150    }
151
152    /// Sets the error.
153    pub fn set_error(&mut self, error: TestResultError) {
154        self.error = Some(error);
155    }
156
157    /// Sets the invocations.
158    pub fn set_invocations(&mut self, invocations: Vec<Invocation>) {
159        self.invocations = invocations;
160    }
161
162    /// Adds an invocation.
163    pub fn add_invocation(&mut self, invocation: Invocation) {
164        self.invocations.push(invocation);
165    }
166
167    /// Sets the history events.
168    pub fn set_history_events(&mut self, events: Vec<HistoryEvent>) {
169        self.history_events = events;
170    }
171
172    /// Adds a history event.
173    pub fn add_history_event(&mut self, event: HistoryEvent) {
174        self.history_events.push(event);
175    }
176
177    /// Sets the Node.js SDK compatible history events.
178    ///
179    /// These events match the Node.js SDK's event history format exactly,
180    /// enabling cross-SDK history comparison.
181    pub fn set_nodejs_history_events(&mut self, events: Vec<NodeJsHistoryEvent>) {
182        self.nodejs_history_events = events;
183    }
184
185    /// Adds a Node.js SDK compatible history event.
186    pub fn add_nodejs_history_event(&mut self, event: NodeJsHistoryEvent) {
187        self.nodejs_history_events.push(event);
188    }
189
190    /// Gets the execution status.
191    ///
192    /// # Returns
193    ///
194    /// The current execution status (Succeeded, Failed, Running, etc.)
195    ///
196    /// # Requirements
197    ///
198    /// - 3.1: WHEN a developer calls get_status() on Test_Result, THE Test_Result SHALL return the execution status
199    pub fn get_status(&self) -> ExecutionStatus {
200        self.status
201    }
202
203    /// Gets the result value if execution succeeded.
204    ///
205    /// # Returns
206    ///
207    /// - `Ok(&T)` - Reference to the result value if execution succeeded
208    /// - `Err(TestError)` - Error if execution failed or result is not available
209    ///
210    /// # Requirements
211    ///
212    /// - 3.2: WHEN a developer calls get_result() on a successful execution, THE Test_Result SHALL return the deserialized result value
213    /// - 3.3: WHEN a developer calls get_result() on a failed execution, THE Test_Result SHALL return an error
214    pub fn get_result(&self) -> Result<&T, TestError> {
215        match self.status {
216            ExecutionStatus::Succeeded => self.result.as_ref().ok_or_else(|| {
217                TestError::result_not_available("Execution succeeded but result is not set")
218            }),
219            ExecutionStatus::Failed => Err(TestError::result_not_available(
220                "Cannot get result from failed execution",
221            )),
222            ExecutionStatus::Running => Err(TestError::result_not_available(
223                "Execution is still running",
224            )),
225            ExecutionStatus::Cancelled => {
226                Err(TestError::result_not_available("Execution was cancelled"))
227            }
228            ExecutionStatus::TimedOut => {
229                Err(TestError::result_not_available("Execution timed out"))
230            }
231        }
232    }
233
234    /// Gets the error if execution failed.
235    ///
236    /// # Returns
237    ///
238    /// - `Ok(&TestResultError)` - Reference to the error if execution failed
239    /// - `Err(&str)` - Error message if execution succeeded or error is not available
240    ///
241    /// # Requirements
242    ///
243    /// - 3.4: WHEN a developer calls get_error() on a failed execution, THE Test_Result SHALL return the error details
244    pub fn get_error(&self) -> Result<&TestResultError, &str> {
245        match self.status {
246            ExecutionStatus::Failed | ExecutionStatus::Cancelled | ExecutionStatus::TimedOut => {
247                self.error
248                    .as_ref()
249                    .ok_or("Execution failed but error details are not available")
250            }
251            ExecutionStatus::Succeeded => Err("Cannot get error from successful execution"),
252            ExecutionStatus::Running => Err("Execution is still running"),
253        }
254    }
255
256    /// Gets all operations from the execution.
257    ///
258    /// # Returns
259    ///
260    /// A slice of all operations captured during execution, in execution order.
261    ///
262    /// # Requirements
263    ///
264    /// - 3.5: WHEN a developer calls get_operations() on Test_Result, THE Test_Result SHALL return all operations from the execution
265    pub fn get_operations(&self) -> &[Operation] {
266        &self.operations
267    }
268
269    /// Gets operations filtered by status.
270    ///
271    /// # Arguments
272    ///
273    /// * `status` - The operation status to filter by
274    ///
275    /// # Returns
276    ///
277    /// A vector of references to operations matching the given status.
278    ///
279    /// # Requirements
280    ///
281    /// - 3.6: WHEN a developer calls get_operations() with a status filter, THE Test_Result SHALL return only operations matching that status
282    pub fn get_operations_by_status(&self, status: OperationStatus) -> Vec<&Operation> {
283        self.operations
284            .iter()
285            .filter(|op| op.status == status)
286            .collect()
287    }
288
289    /// Gets handler invocation details.
290    ///
291    /// # Returns
292    ///
293    /// A slice of invocation details for each time the handler was invoked.
294    ///
295    /// # Requirements
296    ///
297    /// - 3.7: WHEN a developer calls get_invocations() on Test_Result, THE Test_Result SHALL return details about each handler invocation
298    pub fn get_invocations(&self) -> &[Invocation] {
299        &self.invocations
300    }
301
302    /// Gets history events from the execution.
303    ///
304    /// # Returns
305    ///
306    /// A slice of history events that occurred during execution.
307    pub fn get_history_events(&self) -> &[HistoryEvent] {
308        &self.history_events
309    }
310
311    /// Gets Node.js SDK compatible history events from the execution.
312    ///
313    /// Returns events in the Node.js SDK compatible format, suitable for
314    /// cross-SDK history comparison. These events use PascalCase field names
315    /// and include detailed event-specific information.
316    ///
317    /// # Returns
318    ///
319    /// A slice of Node.js-compatible history events that occurred during execution.
320    ///
321    /// # Requirements
322    ///
323    /// - 5.3: THE Event_History output SHALL contain History_Event objects in chronological order by EventId
324    pub fn get_nodejs_history_events(&self) -> &[NodeJsHistoryEvent] {
325        &self.nodejs_history_events
326    }
327
328    /// Checks if the execution succeeded.
329    pub fn is_success(&self) -> bool {
330        self.status.is_success()
331    }
332
333    /// Checks if the execution failed.
334    pub fn is_failure(&self) -> bool {
335        self.status.is_failure()
336    }
337
338    /// Checks if the execution is still running.
339    pub fn is_running(&self) -> bool {
340        matches!(self.status, ExecutionStatus::Running)
341    }
342
343    /// Gets the number of operations.
344    pub fn operation_count(&self) -> usize {
345        self.operations.len()
346    }
347
348    /// Gets the number of invocations.
349    pub fn invocation_count(&self) -> usize {
350        self.invocations.len()
351    }
352}
353
354/// Configuration for print output.
355///
356/// Controls which columns are displayed when printing the operations table.
357#[derive(Debug, Clone)]
358pub struct PrintConfig {
359    /// Show the operation ID column
360    pub show_id: bool,
361    /// Show the parent ID column
362    pub show_parent_id: bool,
363    /// Show the operation name column
364    pub show_name: bool,
365    /// Show the operation type column
366    pub show_type: bool,
367    /// Show the operation status column
368    pub show_status: bool,
369    /// Show the start timestamp column
370    pub show_start_time: bool,
371    /// Show the end timestamp column
372    pub show_end_time: bool,
373    /// Show the duration column
374    pub show_duration: bool,
375    /// Show the result/error column
376    pub show_result: bool,
377}
378
379impl Default for PrintConfig {
380    fn default() -> Self {
381        Self {
382            show_id: false,
383            show_parent_id: false,
384            show_name: true,
385            show_type: true,
386            show_status: true,
387            show_start_time: true,
388            show_end_time: true,
389            show_duration: true,
390            show_result: false,
391        }
392    }
393}
394
395impl PrintConfig {
396    /// Creates a new PrintConfig with all columns enabled.
397    pub fn all() -> Self {
398        Self {
399            show_id: true,
400            show_parent_id: true,
401            show_name: true,
402            show_type: true,
403            show_status: true,
404            show_start_time: true,
405            show_end_time: true,
406            show_duration: true,
407            show_result: true,
408        }
409    }
410
411    /// Creates a minimal PrintConfig with only essential columns.
412    pub fn minimal() -> Self {
413        Self {
414            show_id: false,
415            show_parent_id: false,
416            show_name: true,
417            show_type: true,
418            show_status: true,
419            show_start_time: false,
420            show_end_time: false,
421            show_duration: false,
422            show_result: false,
423        }
424    }
425
426    /// Builder method to set show_id.
427    pub fn with_id(mut self, show: bool) -> Self {
428        self.show_id = show;
429        self
430    }
431
432    /// Builder method to set show_parent_id.
433    pub fn with_parent_id(mut self, show: bool) -> Self {
434        self.show_parent_id = show;
435        self
436    }
437
438    /// Builder method to set show_name.
439    pub fn with_name(mut self, show: bool) -> Self {
440        self.show_name = show;
441        self
442    }
443
444    /// Builder method to set show_type.
445    pub fn with_type(mut self, show: bool) -> Self {
446        self.show_type = show;
447        self
448    }
449
450    /// Builder method to set show_status.
451    pub fn with_status(mut self, show: bool) -> Self {
452        self.show_status = show;
453        self
454    }
455
456    /// Builder method to set show_start_time.
457    pub fn with_start_time(mut self, show: bool) -> Self {
458        self.show_start_time = show;
459        self
460    }
461
462    /// Builder method to set show_end_time.
463    pub fn with_end_time(mut self, show: bool) -> Self {
464        self.show_end_time = show;
465        self
466    }
467
468    /// Builder method to set show_duration.
469    pub fn with_duration(mut self, show: bool) -> Self {
470        self.show_duration = show;
471        self
472    }
473
474    /// Builder method to set show_result.
475    pub fn with_result(mut self, show: bool) -> Self {
476        self.show_result = show;
477        self
478    }
479}
480
481impl<T> TestResult<T> {
482    /// Prints a formatted table of all operations to stdout.
483    ///
484    /// Uses default column configuration showing name, type, status, and timing information.
485    ///
486    /// # Requirements
487    ///
488    /// - 11.1: WHEN a developer calls print() on Test_Result, THE Test_Result SHALL print a formatted table of all operations to stdout
489    /// - 11.3: THE printed table SHALL include operation name, type, status, and timing information by default
490    pub fn print(&self) {
491        self.print_with_config(PrintConfig::default());
492    }
493
494    /// Prints a formatted table of operations with custom column configuration.
495    ///
496    /// # Arguments
497    ///
498    /// * `config` - Configuration specifying which columns to include
499    ///
500    /// # Requirements
501    ///
502    /// - 11.2: WHEN a developer calls print() with column configuration, THE Test_Result SHALL include only the specified columns
503    pub fn print_with_config(&self, config: PrintConfig) {
504        // Build header row
505        let mut headers: Vec<&str> = Vec::new();
506        if config.show_id {
507            headers.push("ID");
508        }
509        if config.show_parent_id {
510            headers.push("Parent ID");
511        }
512        if config.show_name {
513            headers.push("Name");
514        }
515        if config.show_type {
516            headers.push("Type");
517        }
518        if config.show_status {
519            headers.push("Status");
520        }
521        if config.show_start_time {
522            headers.push("Start Time");
523        }
524        if config.show_end_time {
525            headers.push("End Time");
526        }
527        if config.show_duration {
528            headers.push("Duration");
529        }
530        if config.show_result {
531            headers.push("Result/Error");
532        }
533
534        // Calculate column widths
535        let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
536
537        // Build rows and update widths
538        let rows: Vec<Vec<String>> = self
539            .operations
540            .iter()
541            .map(|op| {
542                let mut row: Vec<String> = Vec::new();
543                let mut col_idx = 0;
544
545                if config.show_id {
546                    let val = op.operation_id.clone();
547                    widths[col_idx] = widths[col_idx].max(val.len());
548                    row.push(val);
549                    col_idx += 1;
550                }
551                if config.show_parent_id {
552                    let val = op.parent_id.clone().unwrap_or_else(|| "-".to_string());
553                    widths[col_idx] = widths[col_idx].max(val.len());
554                    row.push(val);
555                    col_idx += 1;
556                }
557                if config.show_name {
558                    let val = op.name.clone().unwrap_or_else(|| "-".to_string());
559                    widths[col_idx] = widths[col_idx].max(val.len());
560                    row.push(val);
561                    col_idx += 1;
562                }
563                if config.show_type {
564                    let val = format!("{}", op.operation_type);
565                    widths[col_idx] = widths[col_idx].max(val.len());
566                    row.push(val);
567                    col_idx += 1;
568                }
569                if config.show_status {
570                    let val = format!("{}", op.status);
571                    widths[col_idx] = widths[col_idx].max(val.len());
572                    row.push(val);
573                    col_idx += 1;
574                }
575                if config.show_start_time {
576                    let val = op
577                        .start_timestamp
578                        .map(format_timestamp)
579                        .unwrap_or_else(|| "-".to_string());
580                    widths[col_idx] = widths[col_idx].max(val.len());
581                    row.push(val);
582                    col_idx += 1;
583                }
584                if config.show_end_time {
585                    let val = op
586                        .end_timestamp
587                        .map(format_timestamp)
588                        .unwrap_or_else(|| "-".to_string());
589                    widths[col_idx] = widths[col_idx].max(val.len());
590                    row.push(val);
591                    col_idx += 1;
592                }
593                if config.show_duration {
594                    let val = match (op.start_timestamp, op.end_timestamp) {
595                        (Some(start), Some(end)) => format_duration(end - start),
596                        _ => "-".to_string(),
597                    };
598                    widths[col_idx] = widths[col_idx].max(val.len());
599                    row.push(val);
600                    col_idx += 1;
601                }
602                if config.show_result {
603                    let val = if let Some(ref err) = op.error {
604                        format!("Error: {}", err.error_message)
605                    } else if let Some(result) = op.get_result() {
606                        truncate_string(result, 50)
607                    } else {
608                        "-".to_string()
609                    };
610                    widths[col_idx] = widths[col_idx].max(val.len().min(50));
611                    row.push(val);
612                }
613
614                row
615            })
616            .collect();
617
618        // Print execution status header
619        println!("\n=== Execution Result ===");
620        println!("Status: {}", self.status);
621        if let Some(ref err) = self.error {
622            println!("Error: {}", err);
623        }
624        println!("Operations: {}", self.operations.len());
625        println!("Invocations: {}", self.invocations.len());
626        println!();
627
628        // Print table header
629        print_row(
630            &headers.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
631            &widths,
632        );
633        print_separator(&widths);
634
635        // Print rows
636        for row in rows {
637            print_row(&row, &widths);
638        }
639
640        println!();
641    }
642}
643
644/// Formats a timestamp (milliseconds since epoch) as a human-readable string.
645fn format_timestamp(millis: i64) -> String {
646    use chrono::{TimeZone, Utc};
647    match Utc.timestamp_millis_opt(millis) {
648        chrono::LocalResult::Single(dt) => dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string(),
649        _ => format!("{}ms", millis),
650    }
651}
652
653/// Formats a duration in milliseconds as a human-readable string.
654fn format_duration(millis: i64) -> String {
655    if millis < 1000 {
656        format!("{}ms", millis)
657    } else if millis < 60_000 {
658        format!("{:.2}s", millis as f64 / 1000.0)
659    } else if millis < 3_600_000 {
660        let mins = millis / 60_000;
661        let secs = (millis % 60_000) / 1000;
662        format!("{}m {}s", mins, secs)
663    } else {
664        let hours = millis / 3_600_000;
665        let mins = (millis % 3_600_000) / 60_000;
666        format!("{}h {}m", hours, mins)
667    }
668}
669
670/// Truncates a string to the specified length, adding "..." if truncated.
671fn truncate_string(s: &str, max_len: usize) -> String {
672    if s.len() <= max_len {
673        s.to_string()
674    } else {
675        format!("{}...", &s[..max_len - 3])
676    }
677}
678
679/// Prints a row with proper column alignment.
680fn print_row(row: &[String], widths: &[usize]) {
681    let formatted: Vec<String> = row
682        .iter()
683        .zip(widths.iter())
684        .map(|(val, width)| format!("{:<width$}", val, width = width))
685        .collect();
686    println!("| {} |", formatted.join(" | "));
687}
688
689/// Prints a separator line.
690fn print_separator(widths: &[usize]) {
691    let separators: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
692    println!("+-{}-+", separators.join("-+-"));
693}
694
695// Additional impl block for TestResult with DeserializeOwned bound
696impl<T: DeserializeOwned> TestResult<T> {
697    /// Attempts to deserialize the result from a JSON string.
698    ///
699    /// This is useful when the result was stored as a serialized string
700    /// and needs to be deserialized into the expected type.
701    pub fn deserialize_result_from_json(json: &str) -> Result<T, TestError> {
702        serde_json::from_str(json).map_err(TestError::from)
703    }
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709    use durable_execution_sdk::{Operation, OperationStatus, OperationType};
710
711    fn create_test_operation(
712        name: &str,
713        op_type: OperationType,
714        status: OperationStatus,
715    ) -> Operation {
716        let mut op = Operation::new(format!("{}-001", name), op_type);
717        op.name = Some(name.to_string());
718        op.status = status;
719        op
720    }
721
722    #[test]
723    fn test_success_result() {
724        let ops = vec![create_test_operation(
725            "step1",
726            OperationType::Step,
727            OperationStatus::Succeeded,
728        )];
729        let result: TestResult<String> = TestResult::success("hello".to_string(), ops);
730
731        assert_eq!(result.get_status(), ExecutionStatus::Succeeded);
732        assert!(result.is_success());
733        assert!(!result.is_failure());
734        assert_eq!(result.get_result().unwrap(), "hello");
735        assert!(result.get_error().is_err());
736    }
737
738    #[test]
739    fn test_failure_result() {
740        let ops = vec![create_test_operation(
741            "step1",
742            OperationType::Step,
743            OperationStatus::Failed,
744        )];
745        let error = TestResultError::new("TestError", "Something went wrong");
746        let result: TestResult<String> = TestResult::failure(error, ops);
747
748        assert_eq!(result.get_status(), ExecutionStatus::Failed);
749        assert!(!result.is_success());
750        assert!(result.is_failure());
751        assert!(result.get_result().is_err());
752        assert!(result.get_error().is_ok());
753        assert_eq!(
754            result.get_error().unwrap().error_message,
755            Some("Something went wrong".to_string())
756        );
757    }
758
759    #[test]
760    fn test_get_operations() {
761        let ops = vec![
762            create_test_operation("step1", OperationType::Step, OperationStatus::Succeeded),
763            create_test_operation("wait1", OperationType::Wait, OperationStatus::Succeeded),
764            create_test_operation("step2", OperationType::Step, OperationStatus::Failed),
765        ];
766        let result: TestResult<String> = TestResult::success("done".to_string(), ops);
767
768        assert_eq!(result.get_operations().len(), 3);
769        assert_eq!(result.operation_count(), 3);
770    }
771
772    #[test]
773    fn test_get_operations_by_status() {
774        let ops = vec![
775            create_test_operation("step1", OperationType::Step, OperationStatus::Succeeded),
776            create_test_operation("wait1", OperationType::Wait, OperationStatus::Succeeded),
777            create_test_operation("step2", OperationType::Step, OperationStatus::Failed),
778            create_test_operation("step3", OperationType::Step, OperationStatus::Started),
779        ];
780        let result: TestResult<String> = TestResult::success("done".to_string(), ops);
781
782        let succeeded = result.get_operations_by_status(OperationStatus::Succeeded);
783        assert_eq!(succeeded.len(), 2);
784
785        let failed = result.get_operations_by_status(OperationStatus::Failed);
786        assert_eq!(failed.len(), 1);
787
788        let started = result.get_operations_by_status(OperationStatus::Started);
789        assert_eq!(started.len(), 1);
790
791        let pending = result.get_operations_by_status(OperationStatus::Pending);
792        assert_eq!(pending.len(), 0);
793    }
794
795    #[test]
796    fn test_invocations() {
797        let mut result: TestResult<String> = TestResult::success("done".to_string(), vec![]);
798
799        assert_eq!(result.get_invocations().len(), 0);
800        assert_eq!(result.invocation_count(), 0);
801
802        result.add_invocation(Invocation::new());
803        result.add_invocation(Invocation::new());
804
805        assert_eq!(result.get_invocations().len(), 2);
806        assert_eq!(result.invocation_count(), 2);
807    }
808
809    #[test]
810    fn test_history_events() {
811        let mut result: TestResult<String> = TestResult::success("done".to_string(), vec![]);
812
813        assert_eq!(result.get_history_events().len(), 0);
814
815        result.add_history_event(HistoryEvent::new("ExecutionStarted"));
816        result.add_history_event(HistoryEvent::new("StepCompleted").with_operation_id("step-001"));
817
818        assert_eq!(result.get_history_events().len(), 2);
819        assert_eq!(
820            result.get_history_events()[0].event_type,
821            "ExecutionStarted"
822        );
823        assert_eq!(
824            result.get_history_events()[1].operation_id,
825            Some("step-001".to_string())
826        );
827    }
828
829    #[test]
830    fn test_nodejs_history_events() {
831        use crate::checkpoint_server::{
832            ExecutionStartedDetails, ExecutionStartedDetailsWrapper, NodeJsEventDetails,
833            NodeJsEventType, NodeJsHistoryEvent, PayloadWrapper,
834        };
835
836        let mut result: TestResult<String> = TestResult::success("done".to_string(), vec![]);
837
838        // Initially empty
839        assert_eq!(result.get_nodejs_history_events().len(), 0);
840
841        // Create Node.js-compatible events
842        let event1 = NodeJsHistoryEvent {
843            event_type: NodeJsEventType::ExecutionStarted,
844            event_id: 1,
845            id: Some("exec-123".to_string()),
846            event_timestamp: "2025-12-03T22:58:35.094Z".to_string(),
847            sub_type: None,
848            name: None,
849            parent_id: None,
850            details: NodeJsEventDetails::ExecutionStarted(ExecutionStartedDetailsWrapper {
851                execution_started_details: ExecutionStartedDetails {
852                    input: PayloadWrapper::new("{}"),
853                    execution_timeout: None,
854                },
855            }),
856        };
857
858        let event2 = NodeJsHistoryEvent {
859            event_type: NodeJsEventType::StepStarted,
860            event_id: 2,
861            id: Some("step-456".to_string()),
862            event_timestamp: "2025-12-03T22:58:35.096Z".to_string(),
863            sub_type: Some("Step".to_string()),
864            name: Some("my-step".to_string()),
865            parent_id: Some("exec-123".to_string()),
866            details: NodeJsEventDetails::default(),
867        };
868
869        // Add events using add method
870        result.add_nodejs_history_event(event1.clone());
871        result.add_nodejs_history_event(event2.clone());
872
873        assert_eq!(result.get_nodejs_history_events().len(), 2);
874        assert_eq!(
875            result.get_nodejs_history_events()[0].event_type,
876            NodeJsEventType::ExecutionStarted
877        );
878        assert_eq!(result.get_nodejs_history_events()[0].event_id, 1);
879        assert_eq!(
880            result.get_nodejs_history_events()[1].event_type,
881            NodeJsEventType::StepStarted
882        );
883        assert_eq!(result.get_nodejs_history_events()[1].event_id, 2);
884        assert_eq!(
885            result.get_nodejs_history_events()[1].name,
886            Some("my-step".to_string())
887        );
888
889        // Test set_nodejs_history_events
890        let mut result2: TestResult<String> = TestResult::success("done".to_string(), vec![]);
891        result2.set_nodejs_history_events(vec![event1, event2]);
892        assert_eq!(result2.get_nodejs_history_events().len(), 2);
893    }
894
895    #[test]
896    fn test_print_config_default() {
897        let config = PrintConfig::default();
898        assert!(!config.show_id);
899        assert!(!config.show_parent_id);
900        assert!(config.show_name);
901        assert!(config.show_type);
902        assert!(config.show_status);
903        assert!(config.show_start_time);
904        assert!(config.show_end_time);
905        assert!(config.show_duration);
906        assert!(!config.show_result);
907    }
908
909    #[test]
910    fn test_print_config_all() {
911        let config = PrintConfig::all();
912        assert!(config.show_id);
913        assert!(config.show_parent_id);
914        assert!(config.show_name);
915        assert!(config.show_type);
916        assert!(config.show_status);
917        assert!(config.show_start_time);
918        assert!(config.show_end_time);
919        assert!(config.show_duration);
920        assert!(config.show_result);
921    }
922
923    #[test]
924    fn test_print_config_minimal() {
925        let config = PrintConfig::minimal();
926        assert!(!config.show_id);
927        assert!(!config.show_parent_id);
928        assert!(config.show_name);
929        assert!(config.show_type);
930        assert!(config.show_status);
931        assert!(!config.show_start_time);
932        assert!(!config.show_end_time);
933        assert!(!config.show_duration);
934        assert!(!config.show_result);
935    }
936
937    #[test]
938    fn test_format_duration() {
939        assert_eq!(format_duration(500), "500ms");
940        assert_eq!(format_duration(1500), "1.50s");
941        assert_eq!(format_duration(65000), "1m 5s");
942        assert_eq!(format_duration(3665000), "1h 1m");
943    }
944
945    #[test]
946    fn test_truncate_string() {
947        assert_eq!(truncate_string("short", 10), "short");
948        assert_eq!(truncate_string("this is a long string", 10), "this is...");
949    }
950
951    #[test]
952    fn test_history_event_builder() {
953        let event = HistoryEvent::new("TestEvent")
954            .with_timestamp(1234567890)
955            .with_operation_id("op-001")
956            .with_data("some data");
957
958        assert_eq!(event.event_type, "TestEvent");
959        assert_eq!(event.timestamp, Some(1234567890));
960        assert_eq!(event.operation_id, Some("op-001".to_string()));
961        assert_eq!(event.data, Some("some data".to_string()));
962    }
963
964    #[test]
965    fn test_running_status() {
966        let result: TestResult<String> = TestResult::with_status(ExecutionStatus::Running, vec![]);
967
968        assert!(result.is_running());
969        assert!(!result.is_success());
970        assert!(!result.is_failure());
971        assert!(result.get_result().is_err());
972        assert!(result.get_error().is_err());
973    }
974
975    #[test]
976    fn test_cancelled_status() {
977        let mut result: TestResult<String> =
978            TestResult::with_status(ExecutionStatus::Cancelled, vec![]);
979        result.set_error(TestResultError::new(
980            "CancelledError",
981            "Execution was cancelled",
982        ));
983
984        assert!(!result.is_running());
985        assert!(!result.is_success());
986        assert!(result.is_failure());
987        assert!(result.get_result().is_err());
988        assert!(result.get_error().is_ok());
989    }
990
991    #[test]
992    fn test_timed_out_status() {
993        let mut result: TestResult<String> =
994            TestResult::with_status(ExecutionStatus::TimedOut, vec![]);
995        result.set_error(TestResultError::new("TimeoutError", "Execution timed out"));
996
997        assert!(!result.is_running());
998        assert!(!result.is_success());
999        assert!(result.is_failure());
1000        assert!(result.get_result().is_err());
1001        assert!(result.get_error().is_ok());
1002    }
1003}
1004
1005/// Property-based tests for TestResult
1006///
1007/// These tests verify the correctness properties defined in the design document.
1008#[cfg(test)]
1009mod property_tests {
1010    use super::*;
1011    use durable_execution_sdk::{Operation, OperationStatus, OperationType};
1012    use proptest::prelude::*;
1013
1014    /// Strategy to generate a random operation type
1015    fn operation_type_strategy() -> impl Strategy<Value = OperationType> {
1016        prop_oneof![
1017            Just(OperationType::Step),
1018            Just(OperationType::Wait),
1019            Just(OperationType::Callback),
1020            Just(OperationType::Invoke),
1021            Just(OperationType::Context),
1022        ]
1023    }
1024
1025    /// Strategy to generate a random operation status
1026    fn operation_status_strategy() -> impl Strategy<Value = OperationStatus> {
1027        prop_oneof![
1028            Just(OperationStatus::Started),
1029            Just(OperationStatus::Pending),
1030            Just(OperationStatus::Ready),
1031            Just(OperationStatus::Succeeded),
1032            Just(OperationStatus::Failed),
1033            Just(OperationStatus::Cancelled),
1034            Just(OperationStatus::TimedOut),
1035            Just(OperationStatus::Stopped),
1036        ]
1037    }
1038
1039    /// Strategy to generate a random operation
1040    fn operation_strategy() -> impl Strategy<Value = Operation> {
1041        (
1042            "[a-zA-Z0-9_-]{1,20}", // operation_id
1043            operation_type_strategy(),
1044            operation_status_strategy(),
1045            proptest::option::of("[a-zA-Z0-9_-]{1,20}"), // name
1046        )
1047            .prop_map(|(id, op_type, status, name)| {
1048                let mut op = Operation::new(id, op_type);
1049                op.status = status;
1050                op.name = name;
1051                op
1052            })
1053    }
1054
1055    /// Strategy to generate a list of operations
1056    fn operations_strategy() -> impl Strategy<Value = Vec<Operation>> {
1057        prop::collection::vec(operation_strategy(), 0..=20)
1058    }
1059
1060    /// Strategy to generate a result value (string for simplicity)
1061    fn result_value_strategy() -> impl Strategy<Value = String> {
1062        "[a-zA-Z0-9 _-]{0,100}"
1063    }
1064
1065    /// Strategy to generate an error
1066    fn error_strategy() -> impl Strategy<Value = TestResultError> {
1067        (
1068            proptest::option::of("[a-zA-Z0-9_]{1,30}"), // error_type
1069            proptest::option::of("[a-zA-Z0-9 _-]{1,100}"), // error_message
1070        )
1071            .prop_map(|(error_type, error_message)| TestResultError {
1072                error_type,
1073                error_message,
1074                error_data: None,
1075                stack_trace: None,
1076            })
1077    }
1078
1079    proptest! {
1080        /// **Feature: rust-testing-utilities, Property 5: Result Retrieval Consistency**
1081        ///
1082        /// *For any* successful execution with result value V, calling `get_result()` SHALL
1083        /// return a value equal to V. *For any* failed execution, calling `get_result()`
1084        /// SHALL return an error.
1085        ///
1086        /// **Validates: Requirements 3.2, 3.3**
1087        #[test]
1088        fn prop_result_retrieval_consistency(
1089            result_value in result_value_strategy(),
1090            error in error_strategy(),
1091            operations in operations_strategy(),
1092        ) {
1093            // Test successful execution
1094            let success_result: TestResult<String> = TestResult::success(result_value.clone(), operations.clone());
1095
1096            // get_result() should return the value for successful execution
1097            let retrieved = success_result.get_result();
1098            prop_assert!(retrieved.is_ok(), "get_result() should succeed for successful execution");
1099            prop_assert_eq!(retrieved.unwrap(), &result_value, "Retrieved value should match original");
1100
1101            // get_error() should fail for successful execution
1102            let error_result = success_result.get_error();
1103            prop_assert!(error_result.is_err(), "get_error() should fail for successful execution");
1104
1105            // Test failed execution
1106            let failure_result: TestResult<String> = TestResult::failure(error.clone(), operations.clone());
1107
1108            // get_result() should fail for failed execution
1109            let retrieved = failure_result.get_result();
1110            prop_assert!(retrieved.is_err(), "get_result() should fail for failed execution");
1111
1112            // get_error() should return the error for failed execution
1113            let error_result = failure_result.get_error();
1114            prop_assert!(error_result.is_ok(), "get_error() should succeed for failed execution");
1115            let retrieved_error = error_result.unwrap();
1116            prop_assert_eq!(&retrieved_error.error_type, &error.error_type, "Error type should match");
1117            prop_assert_eq!(&retrieved_error.error_message, &error.error_message, "Error message should match");
1118        }
1119
1120        /// **Feature: rust-testing-utilities, Property 6: Operation Filtering Correctness**
1121        ///
1122        /// *For any* TestResult with operations, calling `get_operations_by_status(S)` SHALL
1123        /// return only operations with status S, and the count SHALL equal the number of
1124        /// operations with that status.
1125        ///
1126        /// **Validates: Requirements 3.6**
1127        #[test]
1128        fn prop_operation_filtering_correctness(
1129            operations in operations_strategy(),
1130            filter_status in operation_status_strategy(),
1131        ) {
1132            let result: TestResult<String> = TestResult::success("test".to_string(), operations.clone());
1133
1134            // Get filtered operations
1135            let filtered = result.get_operations_by_status(filter_status);
1136
1137            // Count expected operations with the filter status
1138            let expected_count = operations.iter().filter(|op| op.status == filter_status).count();
1139
1140            // Verify the count matches
1141            prop_assert_eq!(
1142                filtered.len(),
1143                expected_count,
1144                "Filtered count should match expected count for status {:?}",
1145                filter_status
1146            );
1147
1148            // Verify all filtered operations have the correct status
1149            for op in &filtered {
1150                prop_assert_eq!(
1151                    op.status,
1152                    filter_status,
1153                    "All filtered operations should have status {:?}",
1154                    filter_status
1155                );
1156            }
1157
1158            // Verify no operations with the filter status were missed
1159            let all_ops = result.get_operations();
1160            let missed_count = all_ops
1161                .iter()
1162                .filter(|op| op.status == filter_status)
1163                .filter(|op| !filtered.iter().any(|f| f.operation_id == op.operation_id))
1164                .count();
1165
1166            prop_assert_eq!(
1167                missed_count,
1168                0,
1169                "No operations with status {:?} should be missed",
1170                filter_status
1171            );
1172        }
1173    }
1174}