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