1use super::error::ExecutionError;
59use super::execution_model::{Execution, Step};
60use super::execution_state::{ExecutionState, StepState, WaitReason};
61use super::ids::{StepId, StepSource, StepType};
62use std::time::Instant;
63
64#[derive(Debug, Clone)]
69pub enum ExecutionAction {
70 Start,
72 StepStarted {
74 step_id: StepId,
75 parent_step_id: Option<StepId>,
76 step_type: StepType,
77 name: String,
78 source: Option<StepSource>,
80 },
81 StepCompleted {
83 step_id: StepId,
84 output: Option<String>,
85 duration_ms: u64,
86 },
87 StepFailed {
89 step_id: StepId,
90 error: ExecutionError,
91 },
92 Pause { reason: String },
94 Resume,
96 Wait { reason: WaitReason },
98 InputReceived,
100 Complete { output: Option<String> },
102 Fail { error: ExecutionError },
104 Cancel { reason: String },
106}
107
108pub fn reduce(execution: &mut Execution, action: ExecutionAction) -> Result<(), ReducerError> {
113 match action {
114 ExecutionAction::Start => {
115 if execution.state != ExecutionState::Created {
116 return Err(ReducerError::InvalidTransition {
117 from: execution.state,
118 action: "Start".to_string(),
119 });
120 }
121 execution.state = ExecutionState::Running;
122 execution.started_at = Some(Instant::now());
123 Ok(())
124 }
125
126 ExecutionAction::StepStarted {
127 step_id,
128 parent_step_id,
129 step_type,
130 name,
131 source,
132 } => {
133 if !matches!(execution.state, ExecutionState::Running) {
134 return Err(ReducerError::InvalidTransition {
135 from: execution.state,
136 action: "StepStarted".to_string(),
137 });
138 }
139 let mut step = Step::new(step_type, name);
140 step.id = step_id;
141 step.parent_step_id = parent_step_id;
142 step.state = StepState::Running;
143 step.started_at = Some(now_millis());
144 step.source = source;
145 execution.add_step(step);
146 Ok(())
147 }
148
149 ExecutionAction::StepCompleted {
150 step_id,
151 output,
152 duration_ms,
153 } => {
154 if let Some(step) = execution.get_step_mut(&step_id) {
155 step.state = StepState::Completed;
156 step.output = output;
157 step.duration_ms = Some(duration_ms);
158 step.ended_at = Some(now_millis());
159 Ok(())
160 } else {
161 Err(ReducerError::StepNotFound(step_id))
162 }
163 }
164
165 ExecutionAction::StepFailed { step_id, error } => {
166 if let Some(step) = execution.get_step_mut(&step_id) {
167 step.state = StepState::Failed;
168 step.error = Some(error);
169 step.ended_at = Some(now_millis());
170 Ok(())
171 } else {
172 Err(ReducerError::StepNotFound(step_id))
173 }
174 }
175
176 ExecutionAction::Pause { reason: _ } => {
177 if execution.state != ExecutionState::Running {
178 return Err(ReducerError::InvalidTransition {
179 from: execution.state,
180 action: "Pause".to_string(),
181 });
182 }
183 execution.state = ExecutionState::Paused;
184 Ok(())
185 }
186
187 ExecutionAction::Resume => {
188 if !execution.state.can_resume() {
189 return Err(ReducerError::InvalidTransition {
190 from: execution.state,
191 action: "Resume".to_string(),
192 });
193 }
194 execution.state = ExecutionState::Running;
195 Ok(())
196 }
197
198 ExecutionAction::Wait { reason } => {
199 if execution.state != ExecutionState::Running {
200 return Err(ReducerError::InvalidTransition {
201 from: execution.state,
202 action: "Wait".to_string(),
203 });
204 }
205 execution.state = ExecutionState::Waiting(reason);
206 Ok(())
207 }
208
209 ExecutionAction::InputReceived => {
210 if !matches!(execution.state, ExecutionState::Waiting(_)) {
211 return Err(ReducerError::InvalidTransition {
212 from: execution.state,
213 action: "InputReceived".to_string(),
214 });
215 }
216 execution.state = ExecutionState::Running;
217 Ok(())
218 }
219
220 ExecutionAction::Complete { output } => {
221 if execution.state != ExecutionState::Running {
222 return Err(ReducerError::InvalidTransition {
223 from: execution.state,
224 action: "Complete".to_string(),
225 });
226 }
227 execution.state = ExecutionState::Completed;
228 execution.output = output;
229 execution.ended_at = Some(Instant::now());
230 Ok(())
231 }
232
233 ExecutionAction::Fail { error } => {
234 if execution.state.is_terminal() {
236 return Err(ReducerError::InvalidTransition {
237 from: execution.state,
238 action: "Fail".to_string(),
239 });
240 }
241 execution.state = ExecutionState::Failed;
242 execution.error = Some(error);
243 execution.ended_at = Some(Instant::now());
244 Ok(())
245 }
246
247 ExecutionAction::Cancel { reason: _ } => {
248 if execution.state.is_terminal() {
250 return Err(ReducerError::InvalidTransition {
251 from: execution.state,
252 action: "Cancel".to_string(),
253 });
254 }
255 execution.state = ExecutionState::Cancelled;
256 execution.ended_at = Some(Instant::now());
257 Ok(())
258 }
259 }
260}
261
262#[derive(Debug, thiserror::Error)]
264pub enum ReducerError {
265 #[error("Invalid state transition from {from:?} via {action}")]
266 InvalidTransition {
267 from: ExecutionState,
268 action: String,
269 },
270 #[error("Step not found: {0}")]
271 StepNotFound(StepId),
272}
273
274fn now_millis() -> i64 {
276 std::time::SystemTime::now()
277 .duration_since(std::time::UNIX_EPOCH)
278 .unwrap_or_default()
279 .as_millis() as i64
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::kernel::error::ExecutionErrorCategory;
286
287 #[test]
288 fn test_valid_execution_lifecycle() {
289 let mut exec = Execution::new();
290
291 assert!(reduce(&mut exec, ExecutionAction::Start).is_ok());
293 assert_eq!(exec.state, ExecutionState::Running);
294
295 assert!(reduce(
297 &mut exec,
298 ExecutionAction::Complete {
299 output: Some("done".into())
300 }
301 )
302 .is_ok());
303 assert_eq!(exec.state, ExecutionState::Completed);
304 assert_eq!(exec.output, Some("done".to_string()));
305 }
306
307 #[test]
308 fn test_invalid_start_from_running() {
309 let mut exec = Execution::new();
310 reduce(&mut exec, ExecutionAction::Start).unwrap();
311
312 assert!(reduce(&mut exec, ExecutionAction::Start).is_err());
314 }
315
316 #[test]
317 fn test_pause_resume() {
318 let mut exec = Execution::new();
319 reduce(&mut exec, ExecutionAction::Start).unwrap();
320
321 assert!(reduce(
323 &mut exec,
324 ExecutionAction::Pause {
325 reason: "test".into()
326 }
327 )
328 .is_ok());
329 assert_eq!(exec.state, ExecutionState::Paused);
330
331 assert!(reduce(&mut exec, ExecutionAction::Resume).is_ok());
333 assert_eq!(exec.state, ExecutionState::Running);
334 }
335
336 #[test]
337 fn test_fail_with_structured_error() {
338 let mut exec = Execution::new();
339 reduce(&mut exec, ExecutionAction::Start).unwrap();
340
341 let error = ExecutionError::llm(
343 crate::kernel::error::LlmErrorCode::RateLimit,
344 "Too many requests",
345 )
346 .with_http_status(429);
347
348 assert!(reduce(
349 &mut exec,
350 ExecutionAction::Fail {
351 error: error.clone()
352 }
353 )
354 .is_ok());
355 assert_eq!(exec.state, ExecutionState::Failed);
356 assert!(exec.error.is_some());
357
358 let stored_error = exec.error.unwrap();
359 assert_eq!(stored_error.category, ExecutionErrorCategory::LlmError);
360 assert!(stored_error.is_retryable());
361 assert_eq!(stored_error.http_status, Some(429));
362 }
363
364 #[test]
365 fn test_step_fail_with_structured_error() {
366 let mut exec = Execution::new();
367 reduce(&mut exec, ExecutionAction::Start).unwrap();
368
369 let step_id = StepId::new();
370 reduce(
371 &mut exec,
372 ExecutionAction::StepStarted {
373 step_id: step_id.clone(),
374 parent_step_id: None,
375 step_type: StepType::ToolNode,
376 name: "test_tool".into(),
377 source: None,
378 },
379 )
380 .unwrap();
381
382 let error = ExecutionError::tool(
384 crate::kernel::error::ToolErrorCode::ExecutionFailed,
385 "Tool crashed",
386 );
387
388 assert!(reduce(
389 &mut exec,
390 ExecutionAction::StepFailed {
391 step_id: step_id.clone(),
392 error: error.clone(),
393 }
394 )
395 .is_ok());
396
397 let step = exec.get_step(&step_id).unwrap();
398 assert_eq!(step.state, StepState::Failed);
399 assert!(step.error.is_some());
400
401 let stored_error = step.error.as_ref().unwrap();
402 assert_eq!(stored_error.category, ExecutionErrorCategory::ToolError);
403 assert!(stored_error.is_retryable());
404 }
405
406 #[test]
407 fn test_fatal_error_not_retryable() {
408 let error = ExecutionError::policy_violation("Content blocked");
409 assert!(!error.is_retryable());
410 assert!(error.is_fatal());
411 assert!(!error.should_retry());
412 }
413
414 #[test]
419 fn test_start_sets_started_at() {
420 let mut exec = Execution::new();
421 assert!(exec.started_at.is_none());
422
423 reduce(&mut exec, ExecutionAction::Start).unwrap();
424 assert!(exec.started_at.is_some());
425 }
426
427 #[test]
428 fn test_complete_sets_ended_at() {
429 let mut exec = Execution::new();
430 reduce(&mut exec, ExecutionAction::Start).unwrap();
431 assert!(exec.ended_at.is_none());
432
433 reduce(&mut exec, ExecutionAction::Complete { output: None }).unwrap();
434 assert!(exec.ended_at.is_some());
435 }
436
437 #[test]
438 fn test_fail_sets_ended_at() {
439 let mut exec = Execution::new();
440 reduce(&mut exec, ExecutionAction::Start).unwrap();
441
442 let error = ExecutionError::kernel_internal("Test failure");
443 reduce(&mut exec, ExecutionAction::Fail { error }).unwrap();
444 assert!(exec.ended_at.is_some());
445 }
446
447 #[test]
448 fn test_cancel_sets_ended_at() {
449 let mut exec = Execution::new();
450 reduce(&mut exec, ExecutionAction::Start).unwrap();
451
452 reduce(
453 &mut exec,
454 ExecutionAction::Cancel {
455 reason: "User cancelled".into(),
456 },
457 )
458 .unwrap();
459 assert!(exec.ended_at.is_some());
460 }
461
462 #[test]
467 fn test_wait_for_approval() {
468 let mut exec = Execution::new();
469 reduce(&mut exec, ExecutionAction::Start).unwrap();
470
471 reduce(
472 &mut exec,
473 ExecutionAction::Wait {
474 reason: WaitReason::Approval,
475 },
476 )
477 .unwrap();
478 assert!(matches!(
479 exec.state,
480 ExecutionState::Waiting(WaitReason::Approval)
481 ));
482 }
483
484 #[test]
485 fn test_wait_for_tool_result() {
486 let mut exec = Execution::new();
487 reduce(&mut exec, ExecutionAction::Start).unwrap();
488
489 reduce(
490 &mut exec,
491 ExecutionAction::Wait {
492 reason: WaitReason::External,
493 },
494 )
495 .unwrap();
496 assert!(matches!(
497 exec.state,
498 ExecutionState::Waiting(WaitReason::External)
499 ));
500 }
501
502 #[test]
503 fn test_wait_for_user_input() {
504 let mut exec = Execution::new();
505 reduce(&mut exec, ExecutionAction::Start).unwrap();
506
507 reduce(
508 &mut exec,
509 ExecutionAction::Wait {
510 reason: WaitReason::UserInput,
511 },
512 )
513 .unwrap();
514 assert!(matches!(
515 exec.state,
516 ExecutionState::Waiting(WaitReason::UserInput)
517 ));
518 }
519
520 #[test]
521 fn test_input_received_resumes_from_waiting() {
522 let mut exec = Execution::new();
523 reduce(&mut exec, ExecutionAction::Start).unwrap();
524 reduce(
525 &mut exec,
526 ExecutionAction::Wait {
527 reason: WaitReason::Approval,
528 },
529 )
530 .unwrap();
531
532 reduce(&mut exec, ExecutionAction::InputReceived).unwrap();
533 assert_eq!(exec.state, ExecutionState::Running);
534 }
535
536 #[test]
537 fn test_resume_from_waiting() {
538 let mut exec = Execution::new();
539 reduce(&mut exec, ExecutionAction::Start).unwrap();
540 reduce(
541 &mut exec,
542 ExecutionAction::Wait {
543 reason: WaitReason::Approval,
544 },
545 )
546 .unwrap();
547
548 reduce(&mut exec, ExecutionAction::Resume).unwrap();
549 assert_eq!(exec.state, ExecutionState::Running);
550 }
551
552 #[test]
557 fn test_step_started_adds_step() {
558 let mut exec = Execution::new();
559 reduce(&mut exec, ExecutionAction::Start).unwrap();
560
561 let step_id = StepId::from_string("step_test");
562 reduce(
563 &mut exec,
564 ExecutionAction::StepStarted {
565 step_id: step_id.clone(),
566 parent_step_id: None,
567 step_type: StepType::LlmNode,
568 name: "test_step".into(),
569 source: None,
570 },
571 )
572 .unwrap();
573
574 assert_eq!(exec.steps.len(), 1);
575 let step = exec.get_step(&step_id).unwrap();
576 assert_eq!(step.state, StepState::Running);
577 assert_eq!(step.name, "test_step");
578 assert!(step.started_at.is_some());
579 }
580
581 #[test]
582 fn test_step_completed_sets_output() {
583 let mut exec = Execution::new();
584 reduce(&mut exec, ExecutionAction::Start).unwrap();
585
586 let step_id = StepId::from_string("step_complete");
587 reduce(
588 &mut exec,
589 ExecutionAction::StepStarted {
590 step_id: step_id.clone(),
591 parent_step_id: None,
592 step_type: StepType::LlmNode,
593 name: "complete_step".into(),
594 source: None,
595 },
596 )
597 .unwrap();
598
599 reduce(
600 &mut exec,
601 ExecutionAction::StepCompleted {
602 step_id: step_id.clone(),
603 output: Some("Result".into()),
604 duration_ms: 1000,
605 },
606 )
607 .unwrap();
608
609 let step = exec.get_step(&step_id).unwrap();
610 assert_eq!(step.state, StepState::Completed);
611 assert_eq!(step.output, Some("Result".to_string()));
612 assert_eq!(step.duration_ms, Some(1000));
613 assert!(step.ended_at.is_some());
614 }
615
616 #[test]
617 fn test_nested_step_with_parent() {
618 let mut exec = Execution::new();
619 reduce(&mut exec, ExecutionAction::Start).unwrap();
620
621 let parent_id = StepId::from_string("step_parent");
622 let child_id = StepId::from_string("step_child");
623
624 reduce(
625 &mut exec,
626 ExecutionAction::StepStarted {
627 step_id: parent_id.clone(),
628 parent_step_id: None,
629 step_type: StepType::GraphNode,
630 name: "parent".into(),
631 source: None,
632 },
633 )
634 .unwrap();
635
636 reduce(
637 &mut exec,
638 ExecutionAction::StepStarted {
639 step_id: child_id.clone(),
640 parent_step_id: Some(parent_id.clone()),
641 step_type: StepType::LlmNode,
642 name: "child".into(),
643 source: None,
644 },
645 )
646 .unwrap();
647
648 let child = exec.get_step(&child_id).unwrap();
649 assert_eq!(child.parent_step_id, Some(parent_id));
650 }
651
652 #[test]
653 fn test_multiple_steps_preserved_order() {
654 let mut exec = Execution::new();
655 reduce(&mut exec, ExecutionAction::Start).unwrap();
656
657 let step1 = StepId::from_string("step_1");
658 let step2 = StepId::from_string("step_2");
659 let step3 = StepId::from_string("step_3");
660
661 for (step_id, name) in [(&step1, "first"), (&step2, "second"), (&step3, "third")] {
662 reduce(
663 &mut exec,
664 ExecutionAction::StepStarted {
665 step_id: step_id.clone(),
666 parent_step_id: None,
667 step_type: StepType::FunctionNode,
668 name: name.into(),
669 source: None,
670 },
671 )
672 .unwrap();
673 }
674
675 assert_eq!(exec.step_order.len(), 3);
676 assert_eq!(exec.step_order[0], step1);
677 assert_eq!(exec.step_order[1], step2);
678 assert_eq!(exec.step_order[2], step3);
679 }
680
681 #[test]
686 fn test_cannot_complete_from_created() {
687 let mut exec = Execution::new();
688 let result = reduce(&mut exec, ExecutionAction::Complete { output: None });
689 assert!(result.is_err());
690 }
691
692 #[test]
693 fn test_cannot_pause_from_created() {
694 let mut exec = Execution::new();
695 let result = reduce(
696 &mut exec,
697 ExecutionAction::Pause {
698 reason: "test".into(),
699 },
700 );
701 assert!(result.is_err());
702 }
703
704 #[test]
705 fn test_cannot_resume_from_created() {
706 let mut exec = Execution::new();
707 let result = reduce(&mut exec, ExecutionAction::Resume);
708 assert!(result.is_err());
709 }
710
711 #[test]
712 fn test_cannot_resume_from_running() {
713 let mut exec = Execution::new();
714 reduce(&mut exec, ExecutionAction::Start).unwrap();
715 let result = reduce(&mut exec, ExecutionAction::Resume);
716 assert!(result.is_err());
717 }
718
719 #[test]
720 fn test_cannot_wait_from_paused() {
721 let mut exec = Execution::new();
722 reduce(&mut exec, ExecutionAction::Start).unwrap();
723 reduce(
724 &mut exec,
725 ExecutionAction::Pause {
726 reason: "test".into(),
727 },
728 )
729 .unwrap();
730 let result = reduce(
731 &mut exec,
732 ExecutionAction::Wait {
733 reason: WaitReason::Approval,
734 },
735 );
736 assert!(result.is_err());
737 }
738
739 #[test]
740 fn test_cannot_input_received_when_not_waiting() {
741 let mut exec = Execution::new();
742 reduce(&mut exec, ExecutionAction::Start).unwrap();
743 let result = reduce(&mut exec, ExecutionAction::InputReceived);
744 assert!(result.is_err());
745 }
746
747 #[test]
748 fn test_cannot_fail_from_completed() {
749 let mut exec = Execution::new();
750 reduce(&mut exec, ExecutionAction::Start).unwrap();
751 reduce(&mut exec, ExecutionAction::Complete { output: None }).unwrap();
752
753 let error = ExecutionError::kernel_internal("Too late");
754 let result = reduce(&mut exec, ExecutionAction::Fail { error });
755 assert!(result.is_err());
756 }
757
758 #[test]
759 fn test_cannot_cancel_from_completed() {
760 let mut exec = Execution::new();
761 reduce(&mut exec, ExecutionAction::Start).unwrap();
762 reduce(&mut exec, ExecutionAction::Complete { output: None }).unwrap();
763
764 let result = reduce(
765 &mut exec,
766 ExecutionAction::Cancel {
767 reason: "test".into(),
768 },
769 );
770 assert!(result.is_err());
771 }
772
773 #[test]
774 fn test_cannot_cancel_from_failed() {
775 let mut exec = Execution::new();
776 reduce(&mut exec, ExecutionAction::Start).unwrap();
777 let error = ExecutionError::kernel_internal("Failed");
778 reduce(&mut exec, ExecutionAction::Fail { error }).unwrap();
779
780 let result = reduce(
781 &mut exec,
782 ExecutionAction::Cancel {
783 reason: "test".into(),
784 },
785 );
786 assert!(result.is_err());
787 }
788
789 #[test]
790 fn test_cannot_start_step_when_not_running() {
791 let mut exec = Execution::new();
792 reduce(&mut exec, ExecutionAction::Start).unwrap();
793 reduce(
794 &mut exec,
795 ExecutionAction::Pause {
796 reason: "test".into(),
797 },
798 )
799 .unwrap();
800
801 let result = reduce(
802 &mut exec,
803 ExecutionAction::StepStarted {
804 step_id: StepId::new(),
805 parent_step_id: None,
806 step_type: StepType::LlmNode,
807 name: "test".into(),
808 source: None,
809 },
810 );
811 assert!(result.is_err());
812 }
813
814 #[test]
815 fn test_step_not_found_error() {
816 let mut exec = Execution::new();
817 reduce(&mut exec, ExecutionAction::Start).unwrap();
818
819 let nonexistent = StepId::from_string("step_nonexistent");
820 let result = reduce(
821 &mut exec,
822 ExecutionAction::StepCompleted {
823 step_id: nonexistent,
824 output: None,
825 duration_ms: 0,
826 },
827 );
828 assert!(matches!(result, Err(ReducerError::StepNotFound(_))));
829 }
830
831 #[test]
832 fn test_step_failed_not_found() {
833 let mut exec = Execution::new();
834 reduce(&mut exec, ExecutionAction::Start).unwrap();
835
836 let nonexistent = StepId::from_string("step_missing");
837 let error = ExecutionError::kernel_internal("Test");
838 let result = reduce(
839 &mut exec,
840 ExecutionAction::StepFailed {
841 step_id: nonexistent,
842 error,
843 },
844 );
845 assert!(matches!(result, Err(ReducerError::StepNotFound(_))));
846 }
847
848 #[test]
853 fn test_can_fail_from_running() {
854 let mut exec = Execution::new();
855 reduce(&mut exec, ExecutionAction::Start).unwrap();
856
857 let error = ExecutionError::kernel_internal("Runtime error");
858 let result = reduce(&mut exec, ExecutionAction::Fail { error });
859 assert!(result.is_ok());
860 assert_eq!(exec.state, ExecutionState::Failed);
861 }
862
863 #[test]
864 fn test_can_fail_from_paused() {
865 let mut exec = Execution::new();
866 reduce(&mut exec, ExecutionAction::Start).unwrap();
867 reduce(
868 &mut exec,
869 ExecutionAction::Pause {
870 reason: "test".into(),
871 },
872 )
873 .unwrap();
874
875 let error = ExecutionError::kernel_internal("Error while paused");
876 let result = reduce(&mut exec, ExecutionAction::Fail { error });
877 assert!(result.is_ok());
878 assert_eq!(exec.state, ExecutionState::Failed);
879 }
880
881 #[test]
882 fn test_can_fail_from_waiting() {
883 let mut exec = Execution::new();
884 reduce(&mut exec, ExecutionAction::Start).unwrap();
885 reduce(
886 &mut exec,
887 ExecutionAction::Wait {
888 reason: WaitReason::Approval,
889 },
890 )
891 .unwrap();
892
893 let error = ExecutionError::timeout("Approval timed out");
894 let result = reduce(&mut exec, ExecutionAction::Fail { error });
895 assert!(result.is_ok());
896 assert_eq!(exec.state, ExecutionState::Failed);
897 }
898
899 #[test]
904 fn test_can_cancel_from_running() {
905 let mut exec = Execution::new();
906 reduce(&mut exec, ExecutionAction::Start).unwrap();
907
908 let result = reduce(
909 &mut exec,
910 ExecutionAction::Cancel {
911 reason: "User request".into(),
912 },
913 );
914 assert!(result.is_ok());
915 assert_eq!(exec.state, ExecutionState::Cancelled);
916 }
917
918 #[test]
919 fn test_can_cancel_from_paused() {
920 let mut exec = Execution::new();
921 reduce(&mut exec, ExecutionAction::Start).unwrap();
922 reduce(
923 &mut exec,
924 ExecutionAction::Pause {
925 reason: "test".into(),
926 },
927 )
928 .unwrap();
929
930 let result = reduce(
931 &mut exec,
932 ExecutionAction::Cancel {
933 reason: "Cancelled".into(),
934 },
935 );
936 assert!(result.is_ok());
937 assert_eq!(exec.state, ExecutionState::Cancelled);
938 }
939
940 #[test]
941 fn test_can_cancel_from_waiting() {
942 let mut exec = Execution::new();
943 reduce(&mut exec, ExecutionAction::Start).unwrap();
944 reduce(
945 &mut exec,
946 ExecutionAction::Wait {
947 reason: WaitReason::Approval,
948 },
949 )
950 .unwrap();
951
952 let result = reduce(
953 &mut exec,
954 ExecutionAction::Cancel {
955 reason: "Timeout".into(),
956 },
957 );
958 assert!(result.is_ok());
959 assert_eq!(exec.state, ExecutionState::Cancelled);
960 }
961
962 #[test]
967 fn test_reducer_error_display_invalid_transition() {
968 let error = ReducerError::InvalidTransition {
969 from: ExecutionState::Paused,
970 action: "Start".to_string(),
971 };
972 let display = format!("{}", error);
973 assert!(display.contains("Paused"));
974 assert!(display.contains("Start"));
975 }
976
977 #[test]
978 fn test_reducer_error_display_step_not_found() {
979 let step_id = StepId::from_string("step_missing");
980 let error = ReducerError::StepNotFound(step_id);
981 let display = format!("{}", error);
982 assert!(display.contains("step_missing"));
983 }
984}