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