1use crate::{BehaviorNode, BoxedBehavior, NodeStatus};
8use async_trait::async_trait;
9use mecha10_core::Context;
10use std::time::{Duration, Instant};
11use tracing::{debug, info, warn};
12
13#[derive(Debug)]
14pub struct SequenceNode {
15 children: Vec<BoxedBehavior>,
16 current_index: usize,
17}
18
19impl SequenceNode {
20 pub fn new(children: Vec<BoxedBehavior>) -> Self {
22 Self {
23 children,
24 current_index: 0,
25 }
26 }
27
28 pub fn add_child(&mut self, child: BoxedBehavior) {
30 self.children.push(child);
31 }
32
33 pub fn len(&self) -> usize {
35 self.children.len()
36 }
37
38 pub fn is_empty(&self) -> bool {
40 self.children.is_empty()
41 }
42}
43
44#[async_trait]
45impl BehaviorNode for SequenceNode {
46 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
47 if self.children.is_empty() {
48 return Ok(NodeStatus::Success);
49 }
50
51 let total_children = self.children.len();
52 while self.current_index < total_children {
53 debug!(
54 "Sequence: ticking child {} of {}",
55 self.current_index + 1,
56 total_children
57 );
58
59 let child = &mut self.children[self.current_index];
60 match child.tick(ctx).await? {
61 NodeStatus::Success => {
62 self.current_index += 1;
63 }
64 NodeStatus::Failure => {
65 debug!("Sequence: child {} failed", self.current_index + 1);
66 return Ok(NodeStatus::Failure);
67 }
68 NodeStatus::Running => {
69 return Ok(NodeStatus::Running);
70 }
71 }
72 }
73
74 debug!("Sequence: all children succeeded");
75 Ok(NodeStatus::Success)
76 }
77
78 crate::reset_index_field!(current_index);
79
80 fn name(&self) -> &str {
81 "sequence"
82 }
83
84 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
85 crate::boxed_children_mut(self.children.iter_mut())
86 }
87
88 fn active_capability(&self) -> Option<&str> {
89 self.children
90 .get(self.current_index)
91 .and_then(|c| c.active_capability())
92 }
93}
94
95#[derive(Debug)]
96pub struct SelectOrNode {
97 children: Vec<BoxedBehavior>,
98 current_index: usize,
99}
100
101impl SelectOrNode {
102 pub fn new(children: Vec<BoxedBehavior>) -> Self {
104 Self {
105 children,
106 current_index: 0,
107 }
108 }
109
110 pub fn add_child(&mut self, child: BoxedBehavior) {
112 self.children.push(child);
113 }
114
115 pub fn len(&self) -> usize {
117 self.children.len()
118 }
119
120 pub fn is_empty(&self) -> bool {
122 self.children.is_empty()
123 }
124}
125
126#[async_trait]
127impl BehaviorNode for SelectOrNode {
128 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
129 if self.children.is_empty() {
130 return Ok(NodeStatus::Failure);
131 }
132
133 let total_children = self.children.len();
134 while self.current_index < total_children {
135 debug!(
136 "Selector: ticking child {} of {}",
137 self.current_index + 1,
138 total_children
139 );
140
141 let child = &mut self.children[self.current_index];
142 match child.tick(ctx).await? {
143 NodeStatus::Success => {
144 debug!("Selector: child {} succeeded", self.current_index + 1);
145 return Ok(NodeStatus::Success);
146 }
147 NodeStatus::Failure => {
148 warn!("Selector: child {} failed, trying next", self.current_index + 1);
149 self.current_index += 1;
150 }
151 NodeStatus::Running => {
152 return Ok(NodeStatus::Running);
153 }
154 }
155 }
156
157 debug!("Selector: all children failed");
158 Ok(NodeStatus::Failure)
159 }
160
161 crate::reset_index_field!(current_index);
162
163 fn name(&self) -> &str {
164 "selector"
165 }
166
167 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
168 crate::boxed_children_mut(self.children.iter_mut())
169 }
170
171 fn active_capability(&self) -> Option<&str> {
172 self.children
173 .get(self.current_index)
174 .and_then(|c| c.active_capability())
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum ParallelPolicy {
180 RequireAll,
181
182 RequireOne,
183
184 RequireN(usize),
185}
186
187#[derive(Debug)]
188pub struct ParallelNode {
189 children: Vec<BoxedBehavior>,
190 policy: ParallelPolicy,
191 child_statuses: Vec<NodeStatus>,
192}
193
194impl ParallelNode {
195 pub fn new(children: Vec<BoxedBehavior>, policy: ParallelPolicy) -> Self {
197 let child_statuses = vec![NodeStatus::Running; children.len()];
198 Self {
199 children,
200 policy,
201 child_statuses,
202 }
203 }
204
205 pub fn add_child(&mut self, child: BoxedBehavior) {
207 self.children.push(child);
208 self.child_statuses.push(NodeStatus::Running);
209 }
210
211 pub fn len(&self) -> usize {
213 self.children.len()
214 }
215
216 pub fn is_empty(&self) -> bool {
218 self.children.is_empty()
219 }
220}
221
222#[async_trait]
223impl BehaviorNode for ParallelNode {
224 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
225 if self.children.is_empty() {
226 return Ok(NodeStatus::Success);
227 }
228
229 for (i, child) in self.children.iter_mut().enumerate() {
230 if self.child_statuses[i].is_running() {
231 let status = child.tick(ctx).await?;
232 self.child_statuses[i] = status;
233 debug!("Parallel: child {} status: {}", i + 1, status);
234 }
235 }
236
237 let success_count = self.child_statuses.iter().filter(|s| s.is_success()).count();
238 let failure_count = self.child_statuses.iter().filter(|s| s.is_failure()).count();
239 let _running_count = self.child_statuses.iter().filter(|s| s.is_running()).count();
240
241 match self.policy {
242 ParallelPolicy::RequireAll => {
243 if failure_count > 0 {
244 debug!("Parallel (RequireAll): at least one child failed");
245 Ok(NodeStatus::Failure)
246 } else if success_count == self.children.len() {
247 debug!("Parallel (RequireAll): all children succeeded");
248 Ok(NodeStatus::Success)
249 } else {
250 Ok(NodeStatus::Running)
251 }
252 }
253 ParallelPolicy::RequireOne => {
254 if success_count > 0 {
255 debug!("Parallel (RequireOne): at least one child succeeded");
256 Ok(NodeStatus::Success)
257 } else if failure_count == self.children.len() {
258 debug!("Parallel (RequireOne): all children failed");
259 Ok(NodeStatus::Failure)
260 } else {
261 Ok(NodeStatus::Running)
262 }
263 }
264 ParallelPolicy::RequireN(n) => {
265 if success_count >= n {
266 debug!("Parallel (RequireN({})): {} children succeeded", n, success_count);
267 Ok(NodeStatus::Success)
268 } else if (self.children.len() - failure_count) < n {
269 debug!("Parallel (RequireN({})): not enough children can succeed", n);
270 Ok(NodeStatus::Failure)
271 } else {
272 Ok(NodeStatus::Running)
273 }
274 }
275 }
276 }
277
278 crate::reset_child_statuses_field!(child_statuses);
279
280 fn name(&self) -> &str {
281 "parallel"
282 }
283
284 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
285 crate::boxed_children_mut(self.children.iter_mut())
286 }
287
288 fn active_capability(&self) -> Option<&str> {
293 self.children
294 .iter()
295 .zip(self.child_statuses.iter())
296 .find(|(_, status)| status.is_running())
297 .and_then(|(child, _)| child.active_capability())
298 }
299}
300
301#[derive(Debug)]
307pub struct WatchNode {
308 children: Vec<BoxedBehavior>,
309 policy: ParallelPolicy,
310 last_active: Option<usize>,
311}
312
313impl WatchNode {
314 pub fn new(children: Vec<BoxedBehavior>, policy: ParallelPolicy) -> Self {
316 Self {
317 children,
318 policy,
319 last_active: None,
320 }
321 }
322
323 pub fn add_child(&mut self, child: BoxedBehavior) {
325 self.children.push(child);
326 }
327
328 pub fn len(&self) -> usize {
330 self.children.len()
331 }
332
333 pub fn is_empty(&self) -> bool {
335 self.children.is_empty()
336 }
337}
338
339#[async_trait]
340impl BehaviorNode for WatchNode {
341 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
342 if self.children.is_empty() {
343 self.last_active = None;
344 return Ok(NodeStatus::Failure);
345 }
346
347 let mut statuses = Vec::with_capacity(self.children.len());
348 for child in &mut self.children {
349 statuses.push(child.tick(ctx).await?);
350 }
351
352 self.last_active = statuses.iter().position(|s| s.is_running());
353
354 let success_count = statuses.iter().filter(|s| s.is_success()).count();
355 let failure_count = statuses.iter().filter(|s| s.is_failure()).count();
356
357 match self.policy {
358 ParallelPolicy::RequireAll => {
359 if failure_count > 0 {
360 debug!("Watch (RequireAll): at least one child failed");
361 Ok(NodeStatus::Failure)
362 } else if success_count == self.children.len() {
363 debug!("Watch (RequireAll): all children succeeded");
364 Ok(NodeStatus::Success)
365 } else {
366 Ok(NodeStatus::Running)
367 }
368 }
369 ParallelPolicy::RequireOne => {
370 if success_count > 0 {
371 debug!("Watch (RequireOne): at least one child succeeded");
372 Ok(NodeStatus::Success)
373 } else if failure_count == self.children.len() {
374 debug!("Watch (RequireOne): all children failed");
375 Ok(NodeStatus::Failure)
376 } else {
377 Ok(NodeStatus::Running)
378 }
379 }
380 ParallelPolicy::RequireN(n) => {
381 if success_count >= n {
382 debug!("Watch (RequireN({})): {} children succeeded", n, success_count);
383 Ok(NodeStatus::Success)
384 } else if (self.children.len() - failure_count) < n {
385 debug!("Watch (RequireN({})): not enough children can succeed", n);
386 Ok(NodeStatus::Failure)
387 } else {
388 Ok(NodeStatus::Running)
389 }
390 }
391 }
392 }
393
394 fn name(&self) -> &str {
395 "watch"
396 }
397
398 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
399 crate::boxed_children_mut(self.children.iter_mut())
400 }
401
402 fn active_capability(&self) -> Option<&str> {
407 self.last_active
408 .and_then(|i| self.children.get(i))
409 .and_then(|child| child.active_capability())
410 }
411}
412
413#[derive(Debug)]
416pub struct SubsumptionLayer {
417 pub priority: u8,
418 pub behavior: BoxedBehavior,
419 pub name: Option<String>,
420}
421
422impl SubsumptionLayer {
423 mecha10_core::new_with_defaults!(
424 pub fn new(priority: u8, behavior: BoxedBehavior) -> Self {
426 name: None
427 }
428 );
429
430 mecha10_core::builder_setter!(into_some, with_name, name, impl Into<String>);
431}
432
433#[derive(Debug)]
440pub struct SubsumptionNode {
441 pub layers: Vec<SubsumptionLayer>,
442 last_winning_layer: Option<usize>,
443}
444
445impl SubsumptionNode {
446 pub fn new() -> Self {
448 Self {
449 layers: Vec::new(),
450 last_winning_layer: None,
451 }
452 }
453
454 pub fn add_layer(mut self, priority: u8, behavior: BoxedBehavior) -> Self {
458 self.layers.push(SubsumptionLayer::new(priority, behavior));
459 self.layers.sort_by_key(|b| std::cmp::Reverse(b.priority));
460 self
461 }
462}
463
464impl Default for SubsumptionNode {
465 fn default() -> Self {
466 Self::new()
467 }
468}
469
470#[async_trait]
471impl BehaviorNode for SubsumptionNode {
472 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
473 if self.layers.is_empty() {
474 self.last_winning_layer = None;
475 return Ok(NodeStatus::Failure);
476 }
477
478 for (i, layer) in self.layers.iter_mut().enumerate() {
479 let layer_name = layer.name.as_deref().unwrap_or("unnamed");
480
481 debug!(
482 "Subsumption: trying layer '{}' (priority {})",
483 layer_name, layer.priority
484 );
485
486 let status = layer.behavior.tick(ctx).await?;
487
488 match status {
489 NodeStatus::Success | NodeStatus::Running => {
490 info!(
491 "Subsumption: layer '{}' (priority {}) taking control with status {}",
492 layer_name, layer.priority, status
493 );
494 self.last_winning_layer = Some(i);
495 return Ok(status);
496 }
497 NodeStatus::Failure => {
498 debug!("Subsumption: layer '{}' failed, trying next layer", layer_name);
499 continue;
500 }
501 }
502 }
503
504 debug!("Subsumption: all layers failed");
505 self.last_winning_layer = None;
506 Ok(NodeStatus::Failure)
507 }
508
509 fn name(&self) -> &str {
510 "subsumption"
511 }
512
513 crate::children_mut_via_wrapped_behavior!(layers, layer);
514
515 fn active_capability(&self) -> Option<&str> {
520 self.last_winning_layer
521 .and_then(|i| self.layers.get(i))
522 .and_then(|layer| layer.behavior.active_capability())
523 }
524}
525
526#[derive(Debug)]
530pub struct InverterNode {
531 child: BoxedBehavior,
532}
533
534impl InverterNode {
535 pub fn new(child: BoxedBehavior) -> Self {
537 Self { child }
538 }
539}
540
541#[async_trait]
542impl BehaviorNode for InverterNode {
543 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
544 Ok(match self.child.tick(ctx).await? {
545 NodeStatus::Success => NodeStatus::Failure,
546 NodeStatus::Failure => NodeStatus::Success,
547 NodeStatus::Running => NodeStatus::Running,
548 })
549 }
550
551 fn name(&self) -> &str {
552 "inverter"
553 }
554
555 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
556 vec![self.child.as_mut()]
557 }
558
559 fn active_capability(&self) -> Option<&str> {
560 self.child.active_capability()
561 }
562}
563
564#[derive(Debug)]
569pub struct ForceSuccessNode {
570 child: BoxedBehavior,
571}
572
573impl ForceSuccessNode {
574 pub fn new(child: BoxedBehavior) -> Self {
576 Self { child }
577 }
578}
579
580#[async_trait]
581impl BehaviorNode for ForceSuccessNode {
582 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
583 Ok(match self.child.tick(ctx).await? {
584 NodeStatus::Running => NodeStatus::Running,
585 NodeStatus::Success | NodeStatus::Failure => NodeStatus::Success,
586 })
587 }
588
589 fn name(&self) -> &str {
590 "force_success"
591 }
592
593 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
594 vec![self.child.as_mut()]
595 }
596
597 fn active_capability(&self) -> Option<&str> {
598 self.child.active_capability()
599 }
600}
601
602#[derive(Debug)]
607pub struct ForceFailureNode {
608 child: BoxedBehavior,
609}
610
611impl ForceFailureNode {
612 pub fn new(child: BoxedBehavior) -> Self {
614 Self { child }
615 }
616}
617
618#[async_trait]
619impl BehaviorNode for ForceFailureNode {
620 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
621 Ok(match self.child.tick(ctx).await? {
622 NodeStatus::Running => NodeStatus::Running,
623 NodeStatus::Success | NodeStatus::Failure => NodeStatus::Failure,
624 })
625 }
626
627 fn name(&self) -> &str {
628 "force_failure"
629 }
630
631 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
632 vec![self.child.as_mut()]
633 }
634
635 fn active_capability(&self) -> Option<&str> {
636 self.child.active_capability()
637 }
638}
639
640#[derive(Debug)]
645pub struct RepeatNode {
646 child: BoxedBehavior,
647 count: Option<u32>,
648 completed: u32,
649}
650
651impl RepeatNode {
652 pub fn new(child: BoxedBehavior, count: Option<u32>) -> Self {
654 Self {
655 child,
656 count,
657 completed: 0,
658 }
659 }
660}
661
662#[async_trait]
663impl BehaviorNode for RepeatNode {
664 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
665 if let Some(count) = self.count {
666 if self.completed >= count {
667 return Ok(NodeStatus::Success);
668 }
669 }
670
671 match self.child.tick(ctx).await? {
672 NodeStatus::Success => {
673 self.completed += 1;
674 self.child.reset().await?;
675
676 match self.count {
677 Some(count) if self.completed >= count => Ok(NodeStatus::Success),
678 _ => Ok(NodeStatus::Running),
679 }
680 }
681 NodeStatus::Failure => Ok(NodeStatus::Failure),
682 NodeStatus::Running => Ok(NodeStatus::Running),
683 }
684 }
685
686 fn reset_self(&mut self) {
687 self.completed = 0;
688 }
689
690 fn name(&self) -> &str {
691 "repeat"
692 }
693
694 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
695 vec![self.child.as_mut()]
696 }
697
698 fn active_capability(&self) -> Option<&str> {
699 self.child.active_capability()
700 }
701}
702
703#[derive(Debug)]
709pub struct RetryNode {
710 child: BoxedBehavior,
711 max_attempts: u32,
712 attempts: u32,
713}
714
715impl RetryNode {
716 pub fn new(child: BoxedBehavior, max_attempts: u32) -> Self {
719 Self {
720 child,
721 max_attempts: max_attempts.max(1),
722 attempts: 0,
723 }
724 }
725}
726
727#[async_trait]
728impl BehaviorNode for RetryNode {
729 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
730 match self.child.tick(ctx).await? {
731 NodeStatus::Success => Ok(NodeStatus::Success),
732 NodeStatus::Running => Ok(NodeStatus::Running),
733 NodeStatus::Failure => {
734 self.attempts += 1;
735 if self.attempts >= self.max_attempts {
736 debug!("Retry: child failed after {} attempt(s), giving up", self.attempts);
737 Ok(NodeStatus::Failure)
738 } else {
739 debug!(
740 "Retry: child failed on attempt {} of {}, retrying",
741 self.attempts, self.max_attempts
742 );
743 self.child.reset().await?;
744 Ok(NodeStatus::Running)
745 }
746 }
747 }
748 }
749
750 fn reset_self(&mut self) {
751 self.attempts = 0;
752 }
753
754 fn name(&self) -> &str {
755 "retry"
756 }
757
758 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
759 vec![self.child.as_mut()]
760 }
761
762 fn active_capability(&self) -> Option<&str> {
763 self.child.active_capability()
764 }
765}
766
767#[derive(Debug)]
774pub struct TimeoutNode {
775 child: BoxedBehavior,
776 duration: Duration,
777 start_time: Option<Instant>,
778}
779
780impl TimeoutNode {
781 pub fn new(child: BoxedBehavior, duration: Duration) -> Self {
783 Self {
784 child,
785 duration,
786 start_time: None,
787 }
788 }
789}
790
791#[async_trait]
792impl BehaviorNode for TimeoutNode {
793 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
794 let now = Instant::now();
795 let start = *self.start_time.get_or_insert(now);
796
797 if now.duration_since(start) >= self.duration {
798 warn!("Timeout: child did not resolve within {:?}, failing", self.duration);
799 self.start_time = None;
800 self.child.reset().await?;
801 return Ok(NodeStatus::Failure);
802 }
803
804 let status = self.child.tick(ctx).await?;
805 if status.is_complete() {
806 self.start_time = None;
807 }
808 Ok(status)
809 }
810
811 fn reset_self(&mut self) {
812 self.start_time = None;
813 }
814
815 fn name(&self) -> &str {
816 "timeout"
817 }
818
819 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
820 vec![self.child.as_mut()]
821 }
822
823 fn active_capability(&self) -> Option<&str> {
824 self.child.active_capability()
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831
832 #[derive(Debug, Default)]
833 struct DummyBehavior;
834
835 #[async_trait]
836 impl BehaviorNode for DummyBehavior {
837 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
838 unimplemented!("not exercised by these tests")
839 }
840 }
841
842 #[derive(Debug)]
843 struct CapabilityBehavior(&'static str);
844
845 #[async_trait]
846 impl BehaviorNode for CapabilityBehavior {
847 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
848 unimplemented!("not exercised by these tests")
849 }
850
851 fn active_capability(&self) -> Option<&str> {
852 Some(self.0)
853 }
854 }
855
856 #[test]
857 fn sequence_children_mut_exposes_every_child() {
858 let mut node = SequenceNode::new(vec![Box::new(DummyBehavior), Box::new(DummyBehavior)]);
859 assert_eq!(node.children_mut().len(), 2);
860 }
861
862 #[tokio::test]
863 async fn sequence_reset_rewinds_current_index() {
864 let mut node = SequenceNode::new(vec![Box::new(DummyBehavior)]);
865 node.current_index = 1;
866
867 node.reset().await.unwrap();
868
869 assert_eq!(node.current_index, 0);
870 }
871
872 #[test]
873 fn selector_children_mut_exposes_every_child() {
874 let mut node = SelectOrNode::new(vec![Box::new(DummyBehavior), Box::new(DummyBehavior)]);
875 assert_eq!(node.children_mut().len(), 2);
876 }
877
878 #[tokio::test]
879 async fn selector_reset_rewinds_current_index() {
880 let mut node = SelectOrNode::new(vec![Box::new(DummyBehavior)]);
881 node.current_index = 1;
882
883 node.reset().await.unwrap();
884
885 assert_eq!(node.current_index, 0);
886 }
887
888 #[test]
889 fn parallel_children_mut_exposes_every_child() {
890 let mut node = ParallelNode::new(
891 vec![Box::new(DummyBehavior), Box::new(DummyBehavior)],
892 ParallelPolicy::RequireAll,
893 );
894 assert_eq!(node.children_mut().len(), 2);
895 }
896
897 #[tokio::test]
898 async fn parallel_reset_clears_child_statuses_back_to_running() {
899 let mut node = ParallelNode::new(vec![Box::new(DummyBehavior)], ParallelPolicy::RequireAll);
900 node.child_statuses[0] = NodeStatus::Success;
901
902 node.reset().await.unwrap();
903
904 assert_eq!(node.child_statuses[0], NodeStatus::Running);
905 }
906
907 #[test]
908 fn sequence_forwards_the_current_childs_active_capability() {
909 let node = SequenceNode::new(vec![Box::new(CapabilityBehavior("rl")), Box::new(DummyBehavior)]);
910 assert_eq!(node.active_capability(), Some("rl"));
911 }
912
913 #[test]
914 fn sequence_has_no_active_capability_once_past_the_declaring_child() {
915 let mut node = SequenceNode::new(vec![Box::new(CapabilityBehavior("rl")), Box::new(DummyBehavior)]);
916 node.current_index = 1;
917 assert_eq!(node.active_capability(), None);
918 }
919
920 #[test]
921 fn selector_forwards_the_current_childs_active_capability() {
922 let node = SelectOrNode::new(vec![Box::new(CapabilityBehavior("rl")), Box::new(DummyBehavior)]);
923 assert_eq!(node.active_capability(), Some("rl"));
924 }
925
926 #[test]
927 fn parallel_forwards_the_first_running_childs_active_capability() {
928 let mut node = ParallelNode::new(
929 vec![Box::new(CapabilityBehavior("rl")), Box::new(DummyBehavior)],
930 ParallelPolicy::RequireAll,
931 );
932 node.child_statuses[0] = NodeStatus::Success;
933 assert_eq!(
934 node.active_capability(),
935 None,
936 "a non-running child's capability must not be reported"
937 );
938 }
939
940 #[derive(Debug)]
943 struct SequencedStatusBehavior {
944 statuses: Vec<NodeStatus>,
945 index: usize,
946 tick_count: usize,
947 capability: Option<&'static str>,
948 }
949
950 impl SequencedStatusBehavior {
951 fn new(statuses: Vec<NodeStatus>) -> Self {
952 Self {
953 statuses,
954 index: 0,
955 tick_count: 0,
956 capability: None,
957 }
958 }
959
960 fn with_capability(statuses: Vec<NodeStatus>, capability: &'static str) -> Self {
961 Self {
962 capability: Some(capability),
963 ..Self::new(statuses)
964 }
965 }
966 }
967
968 #[async_trait]
969 impl BehaviorNode for SequencedStatusBehavior {
970 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
971 self.tick_count += 1;
972 let status = self.statuses[self.index.min(self.statuses.len() - 1)];
973 if self.index + 1 < self.statuses.len() {
974 self.index += 1;
975 }
976 Ok(status)
977 }
978
979 fn active_capability(&self) -> Option<&str> {
980 self.capability
981 }
982 }
983
984 #[test]
985 fn watch_children_mut_exposes_every_child() {
986 let mut node = WatchNode::new(
987 vec![Box::new(DummyBehavior), Box::new(DummyBehavior)],
988 ParallelPolicy::RequireOne,
989 );
990 assert_eq!(node.children_mut().len(), 2);
991 }
992
993 #[tokio::test]
994 #[ignore = "Requires Redis infrastructure"]
995 async fn watch_is_empty_returns_failure() {
996 let ctx = crate::test_helpers::create_test_context("watch_empty").await;
997 let mut node = WatchNode::new(vec![], ParallelPolicy::RequireOne);
998 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
999 }
1000
1001 #[tokio::test]
1005 #[ignore = "Requires Redis infrastructure"]
1006 async fn watch_re_ticks_every_child_every_call_and_reflects_the_latest_statuses() {
1007 let ctx = crate::test_helpers::create_test_context("watch_re_ticks").await;
1008 let mut node = WatchNode::new(
1009 vec![Box::new(SequencedStatusBehavior::new(vec![
1010 NodeStatus::Failure,
1011 NodeStatus::Running,
1012 NodeStatus::Failure,
1013 ]))],
1014 ParallelPolicy::RequireOne,
1015 );
1016
1017 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1018 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1019 assert_eq!(
1020 node.tick(&ctx).await.unwrap(),
1021 NodeStatus::Failure,
1022 "a ParallelNode would have frozen on the first Failure and never re-ticked the child"
1023 );
1024 }
1025
1026 #[tokio::test]
1027 #[ignore = "Requires Redis infrastructure"]
1028 async fn watch_forwards_the_currently_running_childs_active_capability() {
1029 let ctx = crate::test_helpers::create_test_context("watch_capability").await;
1030 let mut node = WatchNode::new(
1031 vec![Box::new(SequencedStatusBehavior::with_capability(
1032 vec![NodeStatus::Running, NodeStatus::Failure],
1033 "instance-track",
1034 ))],
1035 ParallelPolicy::RequireOne,
1036 );
1037
1038 node.tick(&ctx).await.unwrap();
1039 assert_eq!(node.active_capability(), Some("instance-track"));
1040
1041 node.tick(&ctx).await.unwrap();
1042 assert_eq!(
1043 node.active_capability(),
1044 None,
1045 "a child that was running last tick and is not running this tick must not keep reporting its capability"
1046 );
1047 }
1048
1049 #[test]
1050 fn subsumption_children_mut_exposes_every_layer_behavior() {
1051 let mut node = SubsumptionNode::new()
1052 .add_layer(10, Box::new(DummyBehavior))
1053 .add_layer(5, Box::new(DummyBehavior));
1054 assert_eq!(node.children_mut().len(), 2);
1055 }
1056
1057 #[derive(Debug)]
1062 struct StatusAndCapabilityBehavior {
1063 status: NodeStatus,
1064 capability: &'static str,
1065 }
1066
1067 #[async_trait]
1068 impl BehaviorNode for StatusAndCapabilityBehavior {
1069 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1070 Ok(self.status)
1071 }
1072
1073 fn active_capability(&self) -> Option<&str> {
1074 Some(self.capability)
1075 }
1076 }
1077
1078 #[tokio::test]
1079 #[ignore = "Requires Redis infrastructure"]
1080 async fn subsumption_active_capability_forwards_the_winning_layers_capability() {
1081 let ctx = crate::test_helpers::create_test_context("subsumption_capability").await;
1082 let mut node = SubsumptionNode::new()
1083 .add_layer(
1084 10,
1085 Box::new(StatusAndCapabilityBehavior {
1086 status: NodeStatus::Failure,
1087 capability: "instance-track",
1088 }),
1089 )
1090 .add_layer(
1091 0,
1092 Box::new(StatusAndCapabilityBehavior {
1093 status: NodeStatus::Running,
1094 capability: "idle-wander",
1095 }),
1096 );
1097
1098 assert_eq!(node.active_capability(), None, "no tick has happened yet");
1099
1100 let status = node.tick(&ctx).await.unwrap();
1101 assert_eq!(
1102 status,
1103 NodeStatus::Running,
1104 "the high-priority layer fails, so the low layer wins"
1105 );
1106 assert_eq!(
1107 node.active_capability(),
1108 Some("idle-wander"),
1109 "active_capability must forward the winning (low-priority) layer's capability, not the failed high-priority one"
1110 );
1111 }
1112
1113 #[tokio::test]
1114 #[ignore = "Requires Redis infrastructure"]
1115 async fn subsumption_active_capability_none_when_all_layers_fail() {
1116 let ctx = crate::test_helpers::create_test_context("subsumption_capability_all_fail").await;
1117 let mut node =
1118 SubsumptionNode::new().add_layer(10, Box::new(SequencedStatusBehavior::new(vec![NodeStatus::Failure])));
1119
1120 node.tick(&ctx).await.unwrap();
1121 assert_eq!(node.active_capability(), None);
1122 }
1123
1124 #[derive(Debug)]
1129 struct ToggleableBehavior {
1130 engaged: std::sync::Arc<std::sync::atomic::AtomicBool>,
1131 }
1132
1133 #[async_trait]
1134 impl BehaviorNode for ToggleableBehavior {
1135 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1136 Ok(if self.engaged.load(std::sync::atomic::Ordering::SeqCst) {
1137 NodeStatus::Running
1138 } else {
1139 NodeStatus::Failure
1140 })
1141 }
1142 }
1143
1144 #[derive(Debug)]
1149 struct CountedLegBehavior {
1150 running_ticks_remaining: usize,
1151 tick_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
1152 }
1153
1154 #[async_trait]
1155 impl BehaviorNode for CountedLegBehavior {
1156 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1157 self.tick_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1158 if self.running_ticks_remaining == 0 {
1159 Ok(NodeStatus::Success)
1160 } else {
1161 self.running_ticks_remaining -= 1;
1162 Ok(NodeStatus::Running)
1163 }
1164 }
1165 }
1166
1167 #[tokio::test]
1173 #[ignore = "Requires Redis infrastructure"]
1174 async fn subsumption_preserves_lower_layer_progress_across_an_interrupt() {
1175 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1176 use std::sync::Arc;
1177
1178 let ctx = crate::test_helpers::create_test_context("subsumption_resume").await;
1179
1180 let engaged = Arc::new(AtomicBool::new(false));
1181 let leg1_ticks = Arc::new(AtomicUsize::new(0));
1182 let leg2_ticks = Arc::new(AtomicUsize::new(0));
1183
1184 let watch = WatchNode::new(
1185 vec![Box::new(ToggleableBehavior {
1186 engaged: engaged.clone(),
1187 })],
1188 ParallelPolicy::RequireOne,
1189 );
1190
1191 let sequence = SequenceNode::new(vec![
1192 Box::new(CountedLegBehavior {
1193 running_ticks_remaining: 2,
1194 tick_count: leg1_ticks.clone(),
1195 }),
1196 Box::new(CountedLegBehavior {
1197 running_ticks_remaining: 2,
1198 tick_count: leg2_ticks.clone(),
1199 }),
1200 ]);
1201
1202 let mut subsumption = SubsumptionNode::new()
1203 .add_layer(100, Box::new(watch))
1204 .add_layer(0, Box::new(sequence));
1205
1206 assert_eq!(subsumption.tick(&ctx).await.unwrap(), NodeStatus::Running);
1208 assert_eq!(leg1_ticks.load(Ordering::SeqCst), 1);
1209 assert_eq!(subsumption.tick(&ctx).await.unwrap(), NodeStatus::Running);
1210 assert_eq!(leg1_ticks.load(Ordering::SeqCst), 2);
1211
1212 assert_eq!(subsumption.tick(&ctx).await.unwrap(), NodeStatus::Running);
1214 assert_eq!(leg1_ticks.load(Ordering::SeqCst), 3);
1215 assert_eq!(leg2_ticks.load(Ordering::SeqCst), 1);
1216
1217 engaged.store(true, Ordering::SeqCst);
1220 for _ in 0..3 {
1221 assert_eq!(
1222 subsumption.tick(&ctx).await.unwrap(),
1223 NodeStatus::Running,
1224 "the instance-track layer wins while engaged"
1225 );
1226 }
1227 assert_eq!(
1228 leg2_ticks.load(Ordering::SeqCst),
1229 1,
1230 "the main layer must be frozen (not reset, not advanced) while the interrupt layer is engaged"
1231 );
1232
1233 engaged.store(false, Ordering::SeqCst);
1237 let status = subsumption.tick(&ctx).await.unwrap();
1238 assert_eq!(
1239 status,
1240 NodeStatus::Running,
1241 "handback must surface the Sequence's own status, not Success/Failure from the ceding interrupt layer"
1242 );
1243 assert_eq!(
1244 leg1_ticks.load(Ordering::SeqCst),
1245 3,
1246 "leg1 must not be re-ticked - the sequence was not reset"
1247 );
1248 assert_eq!(
1249 leg2_ticks.load(Ordering::SeqCst),
1250 2,
1251 "the Sequence resumes ticking leg2 (not leg1) - progress was preserved, not reset"
1252 );
1253 }
1254
1255 #[derive(Debug)]
1256 struct FixedStatusBehavior(NodeStatus);
1257
1258 #[async_trait]
1259 impl BehaviorNode for FixedStatusBehavior {
1260 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1261 Ok(self.0)
1262 }
1263 }
1264
1265 #[tokio::test]
1266 #[ignore = "Requires Redis infrastructure"]
1267 async fn inverter_flips_success_and_failure_but_passes_running_through() {
1268 let ctx = crate::test_helpers::create_test_context("inverter").await;
1269
1270 let mut success = InverterNode::new(Box::new(FixedStatusBehavior(NodeStatus::Success)));
1271 assert_eq!(success.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1272
1273 let mut failure = InverterNode::new(Box::new(FixedStatusBehavior(NodeStatus::Failure)));
1274 assert_eq!(failure.tick(&ctx).await.unwrap(), NodeStatus::Success);
1275
1276 let mut running = InverterNode::new(Box::new(FixedStatusBehavior(NodeStatus::Running)));
1277 assert_eq!(running.tick(&ctx).await.unwrap(), NodeStatus::Running);
1278 }
1279
1280 #[tokio::test]
1281 #[ignore = "Requires Redis infrastructure"]
1282 async fn force_success_reports_success_even_when_child_fails() {
1283 let ctx = crate::test_helpers::create_test_context("force_success").await;
1284
1285 let mut node = ForceSuccessNode::new(Box::new(FixedStatusBehavior(NodeStatus::Failure)));
1286 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
1287
1288 let mut running = ForceSuccessNode::new(Box::new(FixedStatusBehavior(NodeStatus::Running)));
1289 assert_eq!(running.tick(&ctx).await.unwrap(), NodeStatus::Running);
1290 }
1291
1292 #[tokio::test]
1293 #[ignore = "Requires Redis infrastructure"]
1294 async fn force_failure_reports_failure_even_when_child_succeeds() {
1295 let ctx = crate::test_helpers::create_test_context("force_failure").await;
1296
1297 let mut node = ForceFailureNode::new(Box::new(FixedStatusBehavior(NodeStatus::Success)));
1298 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1299
1300 let mut running = ForceFailureNode::new(Box::new(FixedStatusBehavior(NodeStatus::Running)));
1301 assert_eq!(running.tick(&ctx).await.unwrap(), NodeStatus::Running);
1302 }
1303
1304 #[tokio::test]
1305 #[ignore = "Requires Redis infrastructure"]
1306 async fn repeat_with_count_succeeds_after_n_successful_runs() {
1307 let ctx = crate::test_helpers::create_test_context("repeat_count").await;
1308
1309 let mut node = RepeatNode::new(Box::new(FixedStatusBehavior(NodeStatus::Success)), Some(3));
1310 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1311 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1312 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
1313 }
1314
1315 #[tokio::test]
1316 #[ignore = "Requires Redis infrastructure"]
1317 async fn repeat_stops_immediately_on_child_failure() {
1318 let ctx = crate::test_helpers::create_test_context("repeat_failure").await;
1319
1320 let mut node = RepeatNode::new(Box::new(FixedStatusBehavior(NodeStatus::Failure)), None);
1321 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1322 }
1323
1324 #[tokio::test]
1325 #[ignore = "Requires Redis infrastructure"]
1326 async fn retry_succeeds_once_an_attempt_within_the_budget_succeeds() {
1327 let ctx = crate::test_helpers::create_test_context("retry_succeeds").await;
1328
1329 let mut node = RetryNode::new(
1330 Box::new(SequencedStatusBehavior::new(vec![
1331 NodeStatus::Failure,
1332 NodeStatus::Failure,
1333 NodeStatus::Success,
1334 ])),
1335 3,
1336 );
1337 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1338 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1339 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
1340 }
1341
1342 #[tokio::test]
1343 #[ignore = "Requires Redis infrastructure"]
1344 async fn retry_fails_once_every_attempt_is_exhausted() {
1345 let ctx = crate::test_helpers::create_test_context("retry_exhausted").await;
1346
1347 let mut node = RetryNode::new(Box::new(FixedStatusBehavior(NodeStatus::Failure)), 2);
1348 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1349 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1350 }
1351
1352 #[tokio::test]
1353 #[ignore = "Requires Redis infrastructure"]
1354 async fn timeout_passes_through_a_child_that_resolves_before_the_deadline() {
1355 let ctx = crate::test_helpers::create_test_context("timeout_resolves").await;
1356
1357 let mut node = TimeoutNode::new(
1358 Box::new(FixedStatusBehavior(NodeStatus::Success)),
1359 Duration::from_secs(60),
1360 );
1361 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
1362 }
1363
1364 #[tokio::test]
1365 #[ignore = "Requires Redis infrastructure"]
1366 async fn timeout_fails_once_the_child_has_been_running_too_long() {
1367 let ctx = crate::test_helpers::create_test_context("timeout_expires").await;
1368
1369 let mut node = TimeoutNode::new(Box::new(FixedStatusBehavior(NodeStatus::Running)), Duration::ZERO);
1370 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1372 }
1373}