1use crate::{BehaviorNode, BoxedBehavior, NodeStatus, TreeStatusSnapshot};
6use async_trait::async_trait;
7use mecha10_core::config::{controller_mode, ControllerMode};
8use mecha10_core::Context;
9use rand::rngs::StdRng;
10use rand::{RngExt, SeedableRng};
11use std::time::{Duration, Instant};
12use tracing::{debug, info, warn};
13
14#[derive(Debug)]
15pub struct SequenceNode {
16 children: Vec<BoxedBehavior>,
17 current_index: usize,
18 last_status: Option<NodeStatus>,
19}
20
21impl SequenceNode {
22 pub fn new(children: Vec<BoxedBehavior>) -> Self {
24 Self {
25 children,
26 current_index: 0,
27 last_status: None,
28 }
29 }
30
31 pub fn add_child(&mut self, child: BoxedBehavior) {
33 self.children.push(child);
34 }
35
36 pub fn len(&self) -> usize {
38 self.children.len()
39 }
40
41 pub fn is_empty(&self) -> bool {
43 self.children.is_empty()
44 }
45}
46
47#[async_trait]
48impl BehaviorNode for SequenceNode {
49 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
50 if self.children.is_empty() {
51 self.last_status = Some(NodeStatus::Success);
52 return Ok(NodeStatus::Success);
53 }
54
55 let total_children = self.children.len();
56 while self.current_index < total_children {
57 debug!(
58 "Sequence: ticking child {} of {}",
59 self.current_index + 1,
60 total_children
61 );
62
63 let child = &mut self.children[self.current_index];
64 match child.tick(ctx).await? {
65 NodeStatus::Success => {
66 self.current_index += 1;
67 }
68 NodeStatus::Failure => {
69 debug!("Sequence: child {} failed", self.current_index + 1);
70 self.last_status = Some(NodeStatus::Failure);
71 return Ok(NodeStatus::Failure);
72 }
73 NodeStatus::Running => {
74 self.last_status = Some(NodeStatus::Running);
75 return Ok(NodeStatus::Running);
76 }
77 }
78 }
79
80 debug!("Sequence: all children succeeded");
81 self.last_status = Some(NodeStatus::Success);
82 Ok(NodeStatus::Success)
83 }
84
85 crate::reset_index_field!(current_index);
86
87 fn name(&self) -> &str {
88 "sequence"
89 }
90
91 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
92 crate::boxed_children_mut(self.children.iter_mut())
93 }
94
95 fn active_capability(&self) -> Option<&str> {
96 self.children
97 .get(self.current_index)
98 .and_then(|c| c.active_capability())
99 }
100
101 fn status_snapshot(&self) -> TreeStatusSnapshot {
102 TreeStatusSnapshot {
103 name: self.name().to_string(),
104 status: self.last_status,
105 active_capability: self.active_capability().map(String::from),
106 children: self.children.iter().map(|c| c.status_snapshot()).collect(),
107 }
108 }
109}
110
111#[derive(Debug)]
112pub struct SelectOrNode {
113 children: Vec<BoxedBehavior>,
114 current_index: usize,
115 last_status: Option<NodeStatus>,
116}
117
118impl SelectOrNode {
119 pub fn new(children: Vec<BoxedBehavior>) -> Self {
121 Self {
122 children,
123 current_index: 0,
124 last_status: None,
125 }
126 }
127
128 pub fn add_child(&mut self, child: BoxedBehavior) {
130 self.children.push(child);
131 }
132
133 pub fn len(&self) -> usize {
135 self.children.len()
136 }
137
138 pub fn is_empty(&self) -> bool {
140 self.children.is_empty()
141 }
142}
143
144#[async_trait]
145impl BehaviorNode for SelectOrNode {
146 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
147 if self.children.is_empty() {
148 self.last_status = Some(NodeStatus::Failure);
149 return Ok(NodeStatus::Failure);
150 }
151
152 let total_children = self.children.len();
153 while self.current_index < total_children {
154 debug!(
155 "Selector: ticking child {} of {}",
156 self.current_index + 1,
157 total_children
158 );
159
160 let child = &mut self.children[self.current_index];
161 match child.tick(ctx).await? {
162 NodeStatus::Success => {
163 debug!("Selector: child {} succeeded", self.current_index + 1);
164 self.last_status = Some(NodeStatus::Success);
165 return Ok(NodeStatus::Success);
166 }
167 NodeStatus::Failure => {
168 warn!("Selector: child {} failed, trying next", self.current_index + 1);
169 self.current_index += 1;
170 }
171 NodeStatus::Running => {
172 self.last_status = Some(NodeStatus::Running);
173 return Ok(NodeStatus::Running);
174 }
175 }
176 }
177
178 debug!("Selector: all children failed");
179 self.last_status = Some(NodeStatus::Failure);
180 Ok(NodeStatus::Failure)
181 }
182
183 crate::reset_index_field!(current_index);
184
185 fn name(&self) -> &str {
186 "selector"
187 }
188
189 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
190 crate::boxed_children_mut(self.children.iter_mut())
191 }
192
193 fn active_capability(&self) -> Option<&str> {
194 self.children
195 .get(self.current_index)
196 .and_then(|c| c.active_capability())
197 }
198
199 fn status_snapshot(&self) -> TreeStatusSnapshot {
200 TreeStatusSnapshot {
201 name: self.name().to_string(),
202 status: self.last_status,
203 active_capability: self.active_capability().map(String::from),
204 children: self.children.iter().map(|c| c.status_snapshot()).collect(),
205 }
206 }
207}
208
209#[derive(Debug)]
218pub struct WeightedSelectNode {
219 children: Vec<BoxedBehavior>,
220 weights: Vec<f64>,
221 tried: Vec<bool>,
225 current_index: Option<usize>,
228 rng: StdRng,
229 last_status: Option<NodeStatus>,
230}
231
232impl WeightedSelectNode {
233 pub fn new(weighted_children: Vec<(f64, BoxedBehavior)>) -> Self {
236 Self::with_rng(weighted_children, StdRng::from_rng(&mut rand::rng()))
237 }
238
239 pub fn with_rng(weighted_children: Vec<(f64, BoxedBehavior)>, rng: StdRng) -> Self {
242 let mut weights = Vec::with_capacity(weighted_children.len());
243 let mut children = Vec::with_capacity(weighted_children.len());
244 for (weight, child) in weighted_children {
245 weights.push(weight.max(0.0));
246 children.push(child);
247 }
248 let tried = vec![false; children.len()];
249
250 Self {
251 children,
252 weights,
253 tried,
254 current_index: None,
255 rng,
256 last_status: None,
257 }
258 }
259
260 pub fn len(&self) -> usize {
262 self.children.len()
263 }
264
265 pub fn is_empty(&self) -> bool {
267 self.children.is_empty()
268 }
269
270 fn pick_next(&mut self) -> Option<usize> {
273 let candidates: Vec<usize> = (0..self.children.len())
274 .filter(|&i| !self.tried[i] && self.weights[i] > 0.0)
275 .collect();
276
277 let total: f64 = candidates.iter().map(|&i| self.weights[i]).sum();
278 if candidates.is_empty() || total <= 0.0 {
279 return None;
280 }
281
282 let mut draw = self.rng.random::<f64>() * total;
283 for &i in &candidates {
284 draw -= self.weights[i];
285 if draw <= 0.0 {
286 return Some(i);
287 }
288 }
289
290 candidates.last().copied()
293 }
294}
295
296#[async_trait]
297impl BehaviorNode for WeightedSelectNode {
298 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
299 if self.children.is_empty() {
300 self.last_status = Some(NodeStatus::Failure);
301 return Ok(NodeStatus::Failure);
302 }
303
304 loop {
305 let idx = match self.current_index {
306 Some(idx) => idx,
307 None => match self.pick_next() {
308 Some(idx) => idx,
309 None => {
310 debug!("WeightedSelect: no eligible child remains, failing");
311 self.tried.iter_mut().for_each(|t| *t = false);
312 self.last_status = Some(NodeStatus::Failure);
313 return Ok(NodeStatus::Failure);
314 }
315 },
316 };
317
318 self.current_index = Some(idx);
319 self.tried[idx] = true;
320
321 let child = &mut self.children[idx];
322 match child.tick(ctx).await? {
323 NodeStatus::Success => {
324 debug!("WeightedSelect: child {} succeeded", idx);
325 self.current_index = None;
326 self.tried.iter_mut().for_each(|t| *t = false);
327 self.last_status = Some(NodeStatus::Success);
328 return Ok(NodeStatus::Success);
329 }
330 NodeStatus::Failure => {
331 debug!("WeightedSelect: child {} failed, rolling again", idx);
332 self.current_index = None;
333 }
334 NodeStatus::Running => {
335 self.last_status = Some(NodeStatus::Running);
336 return Ok(NodeStatus::Running);
337 }
338 }
339 }
340 }
341
342 fn reset_self(&mut self) {
343 self.current_index = None;
344 self.tried.iter_mut().for_each(|t| *t = false);
345 }
346
347 fn name(&self) -> &str {
348 "weighted_select"
349 }
350
351 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
352 crate::boxed_children_mut(self.children.iter_mut())
353 }
354
355 fn active_capability(&self) -> Option<&str> {
356 self.current_index
357 .and_then(|i| self.children.get(i))
358 .and_then(|c| c.active_capability())
359 }
360
361 fn status_snapshot(&self) -> TreeStatusSnapshot {
362 TreeStatusSnapshot {
363 name: self.name().to_string(),
364 status: self.last_status,
365 active_capability: self.active_capability().map(String::from),
366 children: self.children.iter().map(|c| c.status_snapshot()).collect(),
367 }
368 }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub enum ParallelPolicy {
373 RequireAll,
374
375 RequireOne,
376
377 RequireN(usize),
378}
379
380#[derive(Debug)]
381pub struct ParallelNode {
382 children: Vec<BoxedBehavior>,
383 policy: ParallelPolicy,
384 child_statuses: Vec<NodeStatus>,
385 last_status: Option<NodeStatus>,
386}
387
388impl ParallelNode {
389 pub fn new(children: Vec<BoxedBehavior>, policy: ParallelPolicy) -> Self {
391 let child_statuses = vec![NodeStatus::Running; children.len()];
392 Self {
393 children,
394 policy,
395 child_statuses,
396 last_status: None,
397 }
398 }
399
400 pub fn add_child(&mut self, child: BoxedBehavior) {
402 self.children.push(child);
403 self.child_statuses.push(NodeStatus::Running);
404 }
405
406 pub fn len(&self) -> usize {
408 self.children.len()
409 }
410
411 pub fn is_empty(&self) -> bool {
413 self.children.is_empty()
414 }
415}
416
417#[async_trait]
418impl BehaviorNode for ParallelNode {
419 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
420 if self.children.is_empty() {
421 self.last_status = Some(NodeStatus::Success);
422 return Ok(NodeStatus::Success);
423 }
424
425 for (i, child) in self.children.iter_mut().enumerate() {
426 if self.child_statuses[i].is_running() {
427 let status = child.tick(ctx).await?;
428 self.child_statuses[i] = status;
429 debug!("Parallel: child {} status: {}", i + 1, status);
430 }
431 }
432
433 let success_count = self.child_statuses.iter().filter(|s| s.is_success()).count();
434 let failure_count = self.child_statuses.iter().filter(|s| s.is_failure()).count();
435 let _running_count = self.child_statuses.iter().filter(|s| s.is_running()).count();
436
437 let status = match self.policy {
438 ParallelPolicy::RequireAll => {
439 if failure_count > 0 {
440 debug!("Parallel (RequireAll): at least one child failed");
441 NodeStatus::Failure
442 } else if success_count == self.children.len() {
443 debug!("Parallel (RequireAll): all children succeeded");
444 NodeStatus::Success
445 } else {
446 NodeStatus::Running
447 }
448 }
449 ParallelPolicy::RequireOne => {
450 if success_count > 0 {
451 debug!("Parallel (RequireOne): at least one child succeeded");
452 NodeStatus::Success
453 } else if failure_count == self.children.len() {
454 debug!("Parallel (RequireOne): all children failed");
455 NodeStatus::Failure
456 } else {
457 NodeStatus::Running
458 }
459 }
460 ParallelPolicy::RequireN(n) => {
461 if success_count >= n {
462 debug!("Parallel (RequireN({})): {} children succeeded", n, success_count);
463 NodeStatus::Success
464 } else if (self.children.len() - failure_count) < n {
465 debug!("Parallel (RequireN({})): not enough children can succeed", n);
466 NodeStatus::Failure
467 } else {
468 NodeStatus::Running
469 }
470 }
471 };
472
473 self.last_status = Some(status);
474 Ok(status)
475 }
476
477 crate::reset_child_statuses_field!(child_statuses);
478
479 fn name(&self) -> &str {
480 "parallel"
481 }
482
483 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
484 crate::boxed_children_mut(self.children.iter_mut())
485 }
486
487 fn active_capability(&self) -> Option<&str> {
490 self.children
491 .iter()
492 .zip(self.child_statuses.iter())
493 .find(|(_, status)| status.is_running())
494 .and_then(|(child, _)| child.active_capability())
495 }
496
497 fn status_snapshot(&self) -> TreeStatusSnapshot {
498 TreeStatusSnapshot {
499 name: self.name().to_string(),
500 status: self.last_status,
501 active_capability: self.active_capability().map(String::from),
502 children: self.children.iter().map(|c| c.status_snapshot()).collect(),
503 }
504 }
505}
506
507#[derive(Debug)]
511pub struct WatchNode {
512 children: Vec<BoxedBehavior>,
513 policy: ParallelPolicy,
514 last_active: Option<usize>,
515 last_status: Option<NodeStatus>,
516}
517
518impl WatchNode {
519 pub fn new(children: Vec<BoxedBehavior>, policy: ParallelPolicy) -> Self {
521 Self {
522 children,
523 policy,
524 last_active: None,
525 last_status: None,
526 }
527 }
528
529 pub fn add_child(&mut self, child: BoxedBehavior) {
531 self.children.push(child);
532 }
533
534 pub fn len(&self) -> usize {
536 self.children.len()
537 }
538
539 pub fn is_empty(&self) -> bool {
541 self.children.is_empty()
542 }
543}
544
545#[async_trait]
546impl BehaviorNode for WatchNode {
547 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
548 if self.children.is_empty() {
549 self.last_active = None;
550 self.last_status = Some(NodeStatus::Failure);
551 return Ok(NodeStatus::Failure);
552 }
553
554 let mut statuses = Vec::with_capacity(self.children.len());
555 for child in &mut self.children {
556 statuses.push(child.tick(ctx).await?);
557 }
558
559 self.last_active = statuses.iter().position(|s| s.is_running());
560
561 let success_count = statuses.iter().filter(|s| s.is_success()).count();
562 let failure_count = statuses.iter().filter(|s| s.is_failure()).count();
563
564 let status = match self.policy {
565 ParallelPolicy::RequireAll => {
566 if failure_count > 0 {
567 debug!("Watch (RequireAll): at least one child failed");
568 NodeStatus::Failure
569 } else if success_count == self.children.len() {
570 debug!("Watch (RequireAll): all children succeeded");
571 NodeStatus::Success
572 } else {
573 NodeStatus::Running
574 }
575 }
576 ParallelPolicy::RequireOne => {
577 if success_count > 0 {
578 debug!("Watch (RequireOne): at least one child succeeded");
579 NodeStatus::Success
580 } else if failure_count == self.children.len() {
581 debug!("Watch (RequireOne): all children failed");
582 NodeStatus::Failure
583 } else {
584 NodeStatus::Running
585 }
586 }
587 ParallelPolicy::RequireN(n) => {
588 if success_count >= n {
589 debug!("Watch (RequireN({})): {} children succeeded", n, success_count);
590 NodeStatus::Success
591 } else if (self.children.len() - failure_count) < n {
592 debug!("Watch (RequireN({})): not enough children can succeed", n);
593 NodeStatus::Failure
594 } else {
595 NodeStatus::Running
596 }
597 }
598 };
599
600 self.last_status = Some(status);
601 Ok(status)
602 }
603
604 fn name(&self) -> &str {
605 "watch"
606 }
607
608 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
609 crate::boxed_children_mut(self.children.iter_mut())
610 }
611
612 fn active_capability(&self) -> Option<&str> {
616 self.last_active
617 .and_then(|i| self.children.get(i))
618 .and_then(|child| child.active_capability())
619 }
620
621 fn status_snapshot(&self) -> TreeStatusSnapshot {
622 TreeStatusSnapshot {
623 name: self.name().to_string(),
624 status: self.last_status,
625 active_capability: self.active_capability().map(String::from),
626 children: self.children.iter().map(|c| c.status_snapshot()).collect(),
627 }
628 }
629}
630
631#[derive(Debug)]
634pub struct SubsumptionLayer {
635 pub priority: u8,
636 pub behavior: BoxedBehavior,
637 pub name: Option<String>,
638}
639
640impl SubsumptionLayer {
641 mecha10_core::new_with_defaults!(
642 pub fn new(priority: u8, behavior: BoxedBehavior) -> Self {
644 name: None
645 }
646 );
647
648 mecha10_core::builder_setter!(into_some, with_name, name, impl Into<String>);
649}
650
651#[derive(Debug)]
655pub struct SubsumptionNode {
656 pub layers: Vec<SubsumptionLayer>,
657 last_winning_layer: Option<usize>,
658 last_status: Option<NodeStatus>,
659}
660
661impl SubsumptionNode {
662 pub fn new() -> Self {
664 Self {
665 layers: Vec::new(),
666 last_winning_layer: None,
667 last_status: None,
668 }
669 }
670
671 pub fn add_layer(mut self, priority: u8, behavior: BoxedBehavior) -> Self {
675 self.layers.push(SubsumptionLayer::new(priority, behavior));
676 self.layers.sort_by_key(|b| std::cmp::Reverse(b.priority));
677 self
678 }
679}
680
681impl Default for SubsumptionNode {
682 fn default() -> Self {
683 Self::new()
684 }
685}
686
687#[async_trait]
688impl BehaviorNode for SubsumptionNode {
689 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
690 if self.layers.is_empty() {
691 self.last_winning_layer = None;
692 self.last_status = Some(NodeStatus::Failure);
693 return Ok(NodeStatus::Failure);
694 }
695
696 for (i, layer) in self.layers.iter_mut().enumerate() {
697 let layer_name = layer.name.as_deref().unwrap_or("unnamed");
698
699 debug!(
700 "Subsumption: trying layer '{}' (priority {})",
701 layer_name, layer.priority
702 );
703
704 let status = layer.behavior.tick(ctx).await?;
705
706 match status {
707 NodeStatus::Success | NodeStatus::Running => {
708 info!(
709 "Subsumption: layer '{}' (priority {}) taking control with status {}",
710 layer_name, layer.priority, status
711 );
712 self.last_winning_layer = Some(i);
713 self.last_status = Some(status);
714 return Ok(status);
715 }
716 NodeStatus::Failure => {
717 debug!("Subsumption: layer '{}' failed, trying next layer", layer_name);
718 continue;
719 }
720 }
721 }
722
723 debug!("Subsumption: all layers failed");
724 self.last_winning_layer = None;
725 self.last_status = Some(NodeStatus::Failure);
726 Ok(NodeStatus::Failure)
727 }
728
729 fn name(&self) -> &str {
730 "subsumption"
731 }
732
733 crate::children_mut_via_wrapped_behavior!(layers, layer);
734
735 fn active_capability(&self) -> Option<&str> {
739 self.last_winning_layer
740 .and_then(|i| self.layers.get(i))
741 .and_then(|layer| layer.behavior.active_capability())
742 }
743
744 fn status_snapshot(&self) -> TreeStatusSnapshot {
745 TreeStatusSnapshot {
746 name: self.name().to_string(),
747 status: self.last_status,
748 active_capability: self.active_capability().map(String::from),
749 children: self.layers.iter().map(|l| l.behavior.status_snapshot()).collect(),
750 }
751 }
752}
753
754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
760pub enum ControllerModeKind {
761 Locomotion,
762 Joints,
763}
764
765#[derive(Debug, Clone, PartialEq, Eq)]
771pub enum ConditionKind {
772 ControllerModeIs(ControllerModeKind),
777}
778
779impl ConditionKind {
780 fn evaluate(&self) -> bool {
782 match self {
783 ConditionKind::ControllerModeIs(kind) => {
784 let current = match controller_mode() {
785 ControllerMode::Locomotion => ControllerModeKind::Locomotion,
786 ControllerMode::Joints { .. } => ControllerModeKind::Joints,
787 };
788 current == *kind
789 }
790 }
791 }
792}
793
794#[derive(Debug)]
806pub struct ConditionNode {
807 condition: ConditionKind,
808 child: BoxedBehavior,
809 last_status: Option<NodeStatus>,
810}
811
812impl ConditionNode {
813 pub fn new(condition: ConditionKind, child: BoxedBehavior) -> Self {
815 Self {
816 condition,
817 child,
818 last_status: None,
819 }
820 }
821}
822
823#[async_trait]
824impl BehaviorNode for ConditionNode {
825 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
826 let status = if self.condition.evaluate() {
827 self.child.tick(ctx).await?
828 } else {
829 debug!(
830 "Condition: predicate {:?} is false, returning Failure without ticking child",
831 self.condition
832 );
833 NodeStatus::Failure
834 };
835
836 self.last_status = Some(status);
837 Ok(status)
838 }
839
840 fn name(&self) -> &str {
841 "condition"
842 }
843
844 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
845 vec![self.child.as_mut()]
846 }
847
848 fn active_capability(&self) -> Option<&str> {
849 self.child.active_capability()
850 }
851
852 fn status_snapshot(&self) -> TreeStatusSnapshot {
853 TreeStatusSnapshot {
854 name: self.name().to_string(),
855 status: self.last_status,
856 active_capability: self.active_capability().map(String::from),
857 children: vec![self.child.status_snapshot()],
858 }
859 }
860}
861
862#[derive(Debug)]
866pub struct InverterNode {
867 child: BoxedBehavior,
868 last_status: Option<NodeStatus>,
869}
870
871impl InverterNode {
872 pub fn new(child: BoxedBehavior) -> Self {
874 Self {
875 child,
876 last_status: None,
877 }
878 }
879}
880
881#[async_trait]
882impl BehaviorNode for InverterNode {
883 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
884 let status = match self.child.tick(ctx).await? {
885 NodeStatus::Success => NodeStatus::Failure,
886 NodeStatus::Failure => NodeStatus::Success,
887 NodeStatus::Running => NodeStatus::Running,
888 };
889 self.last_status = Some(status);
890 Ok(status)
891 }
892
893 fn name(&self) -> &str {
894 "inverter"
895 }
896
897 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
898 vec![self.child.as_mut()]
899 }
900
901 fn active_capability(&self) -> Option<&str> {
902 self.child.active_capability()
903 }
904
905 fn status_snapshot(&self) -> TreeStatusSnapshot {
906 TreeStatusSnapshot {
907 name: self.name().to_string(),
908 status: self.last_status,
909 active_capability: self.active_capability().map(String::from),
910 children: vec![self.child.status_snapshot()],
911 }
912 }
913}
914
915#[derive(Debug)]
919pub struct ForceSuccessNode {
920 child: BoxedBehavior,
921 last_status: Option<NodeStatus>,
922}
923
924impl ForceSuccessNode {
925 pub fn new(child: BoxedBehavior) -> Self {
927 Self {
928 child,
929 last_status: None,
930 }
931 }
932}
933
934#[async_trait]
935impl BehaviorNode for ForceSuccessNode {
936 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
937 let status = match self.child.tick(ctx).await? {
938 NodeStatus::Running => NodeStatus::Running,
939 NodeStatus::Success | NodeStatus::Failure => NodeStatus::Success,
940 };
941 self.last_status = Some(status);
942 Ok(status)
943 }
944
945 fn name(&self) -> &str {
946 "force_success"
947 }
948
949 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
950 vec![self.child.as_mut()]
951 }
952
953 fn active_capability(&self) -> Option<&str> {
954 self.child.active_capability()
955 }
956
957 fn status_snapshot(&self) -> TreeStatusSnapshot {
958 TreeStatusSnapshot {
959 name: self.name().to_string(),
960 status: self.last_status,
961 active_capability: self.active_capability().map(String::from),
962 children: vec![self.child.status_snapshot()],
963 }
964 }
965}
966
967#[derive(Debug)]
971pub struct ForceFailureNode {
972 child: BoxedBehavior,
973 last_status: Option<NodeStatus>,
974}
975
976impl ForceFailureNode {
977 pub fn new(child: BoxedBehavior) -> Self {
979 Self {
980 child,
981 last_status: None,
982 }
983 }
984}
985
986#[async_trait]
987impl BehaviorNode for ForceFailureNode {
988 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
989 let status = match self.child.tick(ctx).await? {
990 NodeStatus::Running => NodeStatus::Running,
991 NodeStatus::Success | NodeStatus::Failure => NodeStatus::Failure,
992 };
993 self.last_status = Some(status);
994 Ok(status)
995 }
996
997 fn name(&self) -> &str {
998 "force_failure"
999 }
1000
1001 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
1002 vec![self.child.as_mut()]
1003 }
1004
1005 fn active_capability(&self) -> Option<&str> {
1006 self.child.active_capability()
1007 }
1008
1009 fn status_snapshot(&self) -> TreeStatusSnapshot {
1010 TreeStatusSnapshot {
1011 name: self.name().to_string(),
1012 status: self.last_status,
1013 active_capability: self.active_capability().map(String::from),
1014 children: vec![self.child.status_snapshot()],
1015 }
1016 }
1017}
1018
1019#[derive(Debug)]
1023pub struct RepeatNode {
1024 child: BoxedBehavior,
1025 count: Option<u32>,
1026 completed: u32,
1027 last_status: Option<NodeStatus>,
1028}
1029
1030impl RepeatNode {
1031 pub fn new(child: BoxedBehavior, count: Option<u32>) -> Self {
1033 Self {
1034 child,
1035 count,
1036 completed: 0,
1037 last_status: None,
1038 }
1039 }
1040}
1041
1042#[async_trait]
1043impl BehaviorNode for RepeatNode {
1044 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
1045 if let Some(count) = self.count {
1046 if self.completed >= count {
1047 self.last_status = Some(NodeStatus::Success);
1048 return Ok(NodeStatus::Success);
1049 }
1050 }
1051
1052 let status = match self.child.tick(ctx).await? {
1053 NodeStatus::Success => {
1054 self.completed += 1;
1055 self.child.reset().await?;
1056
1057 match self.count {
1058 Some(count) if self.completed >= count => NodeStatus::Success,
1059 _ => NodeStatus::Running,
1060 }
1061 }
1062 NodeStatus::Failure => NodeStatus::Failure,
1063 NodeStatus::Running => NodeStatus::Running,
1064 };
1065
1066 self.last_status = Some(status);
1067 Ok(status)
1068 }
1069
1070 fn reset_self(&mut self) {
1071 self.completed = 0;
1072 }
1073
1074 fn name(&self) -> &str {
1075 "repeat"
1076 }
1077
1078 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
1079 vec![self.child.as_mut()]
1080 }
1081
1082 fn active_capability(&self) -> Option<&str> {
1083 self.child.active_capability()
1084 }
1085
1086 fn status_snapshot(&self) -> TreeStatusSnapshot {
1087 TreeStatusSnapshot {
1088 name: self.name().to_string(),
1089 status: self.last_status,
1090 active_capability: self.active_capability().map(String::from),
1091 children: vec![self.child.status_snapshot()],
1092 }
1093 }
1094}
1095
1096#[derive(Debug)]
1100pub struct RetryNode {
1101 child: BoxedBehavior,
1102 max_attempts: u32,
1103 attempts: u32,
1104 last_status: Option<NodeStatus>,
1105}
1106
1107impl RetryNode {
1108 pub fn new(child: BoxedBehavior, max_attempts: u32) -> Self {
1111 Self {
1112 child,
1113 max_attempts: max_attempts.max(1),
1114 attempts: 0,
1115 last_status: None,
1116 }
1117 }
1118}
1119
1120#[async_trait]
1121impl BehaviorNode for RetryNode {
1122 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
1123 let status = match self.child.tick(ctx).await? {
1124 NodeStatus::Success => NodeStatus::Success,
1125 NodeStatus::Running => NodeStatus::Running,
1126 NodeStatus::Failure => {
1127 self.attempts += 1;
1128 if self.attempts >= self.max_attempts {
1129 debug!("Retry: child failed after {} attempt(s), giving up", self.attempts);
1130 NodeStatus::Failure
1131 } else {
1132 debug!(
1133 "Retry: child failed on attempt {} of {}, retrying",
1134 self.attempts, self.max_attempts
1135 );
1136 self.child.reset().await?;
1137 NodeStatus::Running
1138 }
1139 }
1140 };
1141
1142 self.last_status = Some(status);
1143 Ok(status)
1144 }
1145
1146 fn reset_self(&mut self) {
1147 self.attempts = 0;
1148 }
1149
1150 fn name(&self) -> &str {
1151 "retry"
1152 }
1153
1154 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
1155 vec![self.child.as_mut()]
1156 }
1157
1158 fn active_capability(&self) -> Option<&str> {
1159 self.child.active_capability()
1160 }
1161
1162 fn status_snapshot(&self) -> TreeStatusSnapshot {
1163 TreeStatusSnapshot {
1164 name: self.name().to_string(),
1165 status: self.last_status,
1166 active_capability: self.active_capability().map(String::from),
1167 children: vec![self.child.status_snapshot()],
1168 }
1169 }
1170}
1171
1172#[derive(Debug)]
1176pub struct TimeoutNode {
1177 child: BoxedBehavior,
1178 duration: Duration,
1179 start_time: Option<Instant>,
1180 last_status: Option<NodeStatus>,
1181}
1182
1183impl TimeoutNode {
1184 pub fn new(child: BoxedBehavior, duration: Duration) -> Self {
1186 Self {
1187 child,
1188 duration,
1189 start_time: None,
1190 last_status: None,
1191 }
1192 }
1193}
1194
1195#[async_trait]
1196impl BehaviorNode for TimeoutNode {
1197 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
1198 let now = Instant::now();
1199 let start = *self.start_time.get_or_insert(now);
1200
1201 if now.duration_since(start) >= self.duration {
1202 warn!("Timeout: child did not resolve within {:?}, failing", self.duration);
1203 self.start_time = None;
1204 self.child.reset().await?;
1205 self.last_status = Some(NodeStatus::Failure);
1206 return Ok(NodeStatus::Failure);
1207 }
1208
1209 let status = self.child.tick(ctx).await?;
1210 if status.is_complete() {
1211 self.start_time = None;
1212 }
1213 self.last_status = Some(status);
1214 Ok(status)
1215 }
1216
1217 fn reset_self(&mut self) {
1218 self.start_time = None;
1219 }
1220
1221 fn name(&self) -> &str {
1222 "timeout"
1223 }
1224
1225 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
1226 vec![self.child.as_mut()]
1227 }
1228
1229 fn active_capability(&self) -> Option<&str> {
1230 self.child.active_capability()
1231 }
1232
1233 fn status_snapshot(&self) -> TreeStatusSnapshot {
1234 TreeStatusSnapshot {
1235 name: self.name().to_string(),
1236 status: self.last_status,
1237 active_capability: self.active_capability().map(String::from),
1238 children: vec![self.child.status_snapshot()],
1239 }
1240 }
1241}
1242
1243#[derive(Debug)]
1250pub struct CooldownNode {
1251 child: BoxedBehavior,
1252 cooldown: Duration,
1253 cooldown_until: Option<Instant>,
1257 last_status: Option<NodeStatus>,
1258}
1259
1260impl CooldownNode {
1261 pub fn new(child: BoxedBehavior, cooldown: Duration) -> Self {
1264 Self {
1265 child,
1266 cooldown,
1267 cooldown_until: None,
1268 last_status: None,
1269 }
1270 }
1271}
1272
1273#[async_trait]
1274impl BehaviorNode for CooldownNode {
1275 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
1276 if let Some(until) = self.cooldown_until {
1277 if Instant::now() < until {
1278 debug!("Cooldown: still cooling down, returning Failure without ticking child");
1279 self.last_status = Some(NodeStatus::Failure);
1280 return Ok(NodeStatus::Failure);
1281 }
1282 self.cooldown_until = None;
1283 }
1284
1285 let status = self.child.tick(ctx).await?;
1286 if status.is_complete() {
1287 self.cooldown_until = Some(Instant::now() + self.cooldown);
1288 }
1289 self.last_status = Some(status);
1290 Ok(status)
1291 }
1292
1293 fn reset_self(&mut self) {
1294 self.cooldown_until = None;
1295 }
1296
1297 fn name(&self) -> &str {
1298 "cooldown"
1299 }
1300
1301 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
1302 vec![self.child.as_mut()]
1303 }
1304
1305 fn active_capability(&self) -> Option<&str> {
1306 self.child.active_capability()
1307 }
1308
1309 fn status_snapshot(&self) -> TreeStatusSnapshot {
1310 TreeStatusSnapshot {
1311 name: self.name().to_string(),
1312 status: self.last_status,
1313 active_capability: self.active_capability().map(String::from),
1314 children: vec![self.child.status_snapshot()],
1315 }
1316 }
1317}
1318
1319#[cfg(test)]
1320mod tests {
1321 use super::*;
1322
1323 #[derive(Debug, Default)]
1324 struct DummyBehavior;
1325
1326 #[async_trait]
1327 impl BehaviorNode for DummyBehavior {
1328 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1329 unimplemented!("not exercised by these tests")
1330 }
1331 }
1332
1333 #[derive(Debug)]
1334 struct CapabilityBehavior(&'static str);
1335
1336 #[async_trait]
1337 impl BehaviorNode for CapabilityBehavior {
1338 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1339 unimplemented!("not exercised by these tests")
1340 }
1341
1342 fn active_capability(&self) -> Option<&str> {
1343 Some(self.0)
1344 }
1345 }
1346
1347 #[test]
1348 fn sequence_children_mut_exposes_every_child() {
1349 let mut node = SequenceNode::new(vec![Box::new(DummyBehavior), Box::new(DummyBehavior)]);
1350 assert_eq!(node.children_mut().len(), 2);
1351 }
1352
1353 #[tokio::test]
1354 async fn sequence_reset_rewinds_current_index() {
1355 let mut node = SequenceNode::new(vec![Box::new(DummyBehavior)]);
1356 node.current_index = 1;
1357
1358 node.reset().await.unwrap();
1359
1360 assert_eq!(node.current_index, 0);
1361 }
1362
1363 #[test]
1364 fn selector_children_mut_exposes_every_child() {
1365 let mut node = SelectOrNode::new(vec![Box::new(DummyBehavior), Box::new(DummyBehavior)]);
1366 assert_eq!(node.children_mut().len(), 2);
1367 }
1368
1369 #[tokio::test]
1370 async fn selector_reset_rewinds_current_index() {
1371 let mut node = SelectOrNode::new(vec![Box::new(DummyBehavior)]);
1372 node.current_index = 1;
1373
1374 node.reset().await.unwrap();
1375
1376 assert_eq!(node.current_index, 0);
1377 }
1378
1379 #[test]
1380 fn parallel_children_mut_exposes_every_child() {
1381 let mut node = ParallelNode::new(
1382 vec![Box::new(DummyBehavior), Box::new(DummyBehavior)],
1383 ParallelPolicy::RequireAll,
1384 );
1385 assert_eq!(node.children_mut().len(), 2);
1386 }
1387
1388 #[tokio::test]
1389 async fn parallel_reset_clears_child_statuses_back_to_running() {
1390 let mut node = ParallelNode::new(vec![Box::new(DummyBehavior)], ParallelPolicy::RequireAll);
1391 node.child_statuses[0] = NodeStatus::Success;
1392
1393 node.reset().await.unwrap();
1394
1395 assert_eq!(node.child_statuses[0], NodeStatus::Running);
1396 }
1397
1398 #[test]
1399 fn sequence_forwards_the_current_childs_active_capability() {
1400 let node = SequenceNode::new(vec![Box::new(CapabilityBehavior("rl")), Box::new(DummyBehavior)]);
1401 assert_eq!(node.active_capability(), Some("rl"));
1402 }
1403
1404 #[test]
1405 fn sequence_has_no_active_capability_once_past_the_declaring_child() {
1406 let mut node = SequenceNode::new(vec![Box::new(CapabilityBehavior("rl")), Box::new(DummyBehavior)]);
1407 node.current_index = 1;
1408 assert_eq!(node.active_capability(), None);
1409 }
1410
1411 #[test]
1412 fn sequence_status_snapshot_is_none_before_any_tick() {
1413 let node = SequenceNode::new(vec![Box::new(DummyBehavior)]);
1414 let snapshot = node.status_snapshot();
1415 assert_eq!(snapshot.status, None);
1416 assert_eq!(snapshot.name, "sequence");
1417 assert_eq!(
1418 snapshot.children.len(),
1419 1,
1420 "children must still be recursed into pre-tick"
1421 );
1422 }
1423
1424 #[tokio::test]
1429 #[ignore = "Requires Redis infrastructure"]
1430 async fn sequence_status_snapshot_matches_the_last_tick_result_and_recurses_into_children() {
1431 let ctx = crate::test_helpers::create_test_context("sequence_status_snapshot").await;
1432 let mut node = SequenceNode::new(vec![Box::new(StatusAndCapabilityBehavior {
1433 status: NodeStatus::Success,
1434 capability: "rl",
1435 })]);
1436
1437 let status = node.tick(&ctx).await.unwrap();
1438 assert_eq!(status, NodeStatus::Success);
1439
1440 let snapshot = node.status_snapshot();
1441 assert_eq!(snapshot.status, Some(status));
1442 assert_eq!(snapshot.name, "sequence");
1443 assert_eq!(
1444 snapshot.active_capability, None,
1445 "the only child already succeeded and was advanced past, so no capability is active"
1446 );
1447 assert_eq!(snapshot.children.len(), 1);
1448 }
1449
1450 #[test]
1451 fn selector_forwards_the_current_childs_active_capability() {
1452 let node = SelectOrNode::new(vec![Box::new(CapabilityBehavior("rl")), Box::new(DummyBehavior)]);
1453 assert_eq!(node.active_capability(), Some("rl"));
1454 }
1455
1456 #[test]
1457 fn parallel_forwards_the_first_running_childs_active_capability() {
1458 let mut node = ParallelNode::new(
1459 vec![Box::new(CapabilityBehavior("rl")), Box::new(DummyBehavior)],
1460 ParallelPolicy::RequireAll,
1461 );
1462 node.child_statuses[0] = NodeStatus::Success;
1463 assert_eq!(
1464 node.active_capability(),
1465 None,
1466 "a non-running child's capability must not be reported"
1467 );
1468 }
1469
1470 #[derive(Debug)]
1473 struct SequencedStatusBehavior {
1474 statuses: Vec<NodeStatus>,
1475 index: usize,
1476 tick_count: usize,
1477 capability: Option<&'static str>,
1478 }
1479
1480 impl SequencedStatusBehavior {
1481 fn new(statuses: Vec<NodeStatus>) -> Self {
1482 Self {
1483 statuses,
1484 index: 0,
1485 tick_count: 0,
1486 capability: None,
1487 }
1488 }
1489
1490 fn with_capability(statuses: Vec<NodeStatus>, capability: &'static str) -> Self {
1491 Self {
1492 capability: Some(capability),
1493 ..Self::new(statuses)
1494 }
1495 }
1496 }
1497
1498 #[async_trait]
1499 impl BehaviorNode for SequencedStatusBehavior {
1500 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1501 self.tick_count += 1;
1502 let status = self.statuses[self.index.min(self.statuses.len() - 1)];
1503 if self.index + 1 < self.statuses.len() {
1504 self.index += 1;
1505 }
1506 Ok(status)
1507 }
1508
1509 fn active_capability(&self) -> Option<&str> {
1510 self.capability
1511 }
1512 }
1513
1514 #[test]
1515 fn watch_children_mut_exposes_every_child() {
1516 let mut node = WatchNode::new(
1517 vec![Box::new(DummyBehavior), Box::new(DummyBehavior)],
1518 ParallelPolicy::RequireOne,
1519 );
1520 assert_eq!(node.children_mut().len(), 2);
1521 }
1522
1523 #[tokio::test]
1524 #[ignore = "Requires Redis infrastructure"]
1525 async fn watch_is_empty_returns_failure() {
1526 let ctx = crate::test_helpers::create_test_context("watch_empty").await;
1527 let mut node = WatchNode::new(vec![], ParallelPolicy::RequireOne);
1528 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1529 }
1530
1531 #[tokio::test]
1535 #[ignore = "Requires Redis infrastructure"]
1536 async fn watch_re_ticks_every_child_every_call_and_reflects_the_latest_statuses() {
1537 let ctx = crate::test_helpers::create_test_context("watch_re_ticks").await;
1538 let mut node = WatchNode::new(
1539 vec![Box::new(SequencedStatusBehavior::new(vec![
1540 NodeStatus::Failure,
1541 NodeStatus::Running,
1542 NodeStatus::Failure,
1543 ]))],
1544 ParallelPolicy::RequireOne,
1545 );
1546
1547 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1548 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1549 assert_eq!(
1550 node.tick(&ctx).await.unwrap(),
1551 NodeStatus::Failure,
1552 "a ParallelNode would have frozen on the first Failure and never re-ticked the child"
1553 );
1554 }
1555
1556 #[tokio::test]
1557 #[ignore = "Requires Redis infrastructure"]
1558 async fn watch_forwards_the_currently_running_childs_active_capability() {
1559 let ctx = crate::test_helpers::create_test_context("watch_capability").await;
1560 let mut node = WatchNode::new(
1561 vec![Box::new(SequencedStatusBehavior::with_capability(
1562 vec![NodeStatus::Running, NodeStatus::Failure],
1563 "instance_track",
1564 ))],
1565 ParallelPolicy::RequireOne,
1566 );
1567
1568 node.tick(&ctx).await.unwrap();
1569 assert_eq!(node.active_capability(), Some("instance_track"));
1570
1571 node.tick(&ctx).await.unwrap();
1572 assert_eq!(
1573 node.active_capability(),
1574 None,
1575 "a child that was running last tick and is not running this tick must not keep reporting its capability"
1576 );
1577 }
1578
1579 #[test]
1580 fn subsumption_children_mut_exposes_every_layer_behavior() {
1581 let mut node = SubsumptionNode::new()
1582 .add_layer(10, Box::new(DummyBehavior))
1583 .add_layer(5, Box::new(DummyBehavior));
1584 assert_eq!(node.children_mut().len(), 2);
1585 }
1586
1587 #[derive(Debug)]
1591 struct StatusAndCapabilityBehavior {
1592 status: NodeStatus,
1593 capability: &'static str,
1594 }
1595
1596 #[async_trait]
1597 impl BehaviorNode for StatusAndCapabilityBehavior {
1598 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1599 Ok(self.status)
1600 }
1601
1602 fn active_capability(&self) -> Option<&str> {
1603 Some(self.capability)
1604 }
1605 }
1606
1607 #[tokio::test]
1608 #[ignore = "Requires Redis infrastructure"]
1609 async fn subsumption_active_capability_forwards_the_winning_layers_capability() {
1610 let ctx = crate::test_helpers::create_test_context("subsumption_capability").await;
1611 let mut node = SubsumptionNode::new()
1612 .add_layer(
1613 10,
1614 Box::new(StatusAndCapabilityBehavior {
1615 status: NodeStatus::Failure,
1616 capability: "instance_track",
1617 }),
1618 )
1619 .add_layer(
1620 0,
1621 Box::new(StatusAndCapabilityBehavior {
1622 status: NodeStatus::Running,
1623 capability: "patrol",
1624 }),
1625 );
1626
1627 assert_eq!(node.active_capability(), None, "no tick has happened yet");
1628
1629 let status = node.tick(&ctx).await.unwrap();
1630 assert_eq!(
1631 status,
1632 NodeStatus::Running,
1633 "the high-priority layer fails, so the low layer wins"
1634 );
1635 assert_eq!(
1636 node.active_capability(),
1637 Some("patrol"),
1638 "active_capability must forward the winning (low-priority) layer's capability, not the failed high-priority one"
1639 );
1640 }
1641
1642 #[tokio::test]
1643 #[ignore = "Requires Redis infrastructure"]
1644 async fn subsumption_active_capability_none_when_all_layers_fail() {
1645 let ctx = crate::test_helpers::create_test_context("subsumption_capability_all_fail").await;
1646 let mut node =
1647 SubsumptionNode::new().add_layer(10, Box::new(SequencedStatusBehavior::new(vec![NodeStatus::Failure])));
1648
1649 node.tick(&ctx).await.unwrap();
1650 assert_eq!(node.active_capability(), None);
1651 }
1652
1653 #[derive(Debug)]
1657 struct ToggleableBehavior {
1658 engaged: std::sync::Arc<std::sync::atomic::AtomicBool>,
1659 }
1660
1661 #[async_trait]
1662 impl BehaviorNode for ToggleableBehavior {
1663 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1664 Ok(if self.engaged.load(std::sync::atomic::Ordering::SeqCst) {
1665 NodeStatus::Running
1666 } else {
1667 NodeStatus::Failure
1668 })
1669 }
1670 }
1671
1672 #[derive(Debug)]
1676 struct CountedLegBehavior {
1677 running_ticks_remaining: usize,
1678 tick_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
1679 }
1680
1681 #[async_trait]
1682 impl BehaviorNode for CountedLegBehavior {
1683 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1684 self.tick_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1685 if self.running_ticks_remaining == 0 {
1686 Ok(NodeStatus::Success)
1687 } else {
1688 self.running_ticks_remaining -= 1;
1689 Ok(NodeStatus::Running)
1690 }
1691 }
1692 }
1693
1694 #[tokio::test]
1698 #[ignore = "Requires Redis infrastructure"]
1699 async fn subsumption_preserves_lower_layer_progress_across_an_interrupt() {
1700 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1701 use std::sync::Arc;
1702
1703 let ctx = crate::test_helpers::create_test_context("subsumption_resume").await;
1704
1705 let engaged = Arc::new(AtomicBool::new(false));
1706 let leg1_ticks = Arc::new(AtomicUsize::new(0));
1707 let leg2_ticks = Arc::new(AtomicUsize::new(0));
1708
1709 let watch = WatchNode::new(
1710 vec![Box::new(ToggleableBehavior {
1711 engaged: engaged.clone(),
1712 })],
1713 ParallelPolicy::RequireOne,
1714 );
1715
1716 let sequence = SequenceNode::new(vec![
1717 Box::new(CountedLegBehavior {
1718 running_ticks_remaining: 2,
1719 tick_count: leg1_ticks.clone(),
1720 }),
1721 Box::new(CountedLegBehavior {
1722 running_ticks_remaining: 2,
1723 tick_count: leg2_ticks.clone(),
1724 }),
1725 ]);
1726
1727 let mut subsumption = SubsumptionNode::new()
1728 .add_layer(100, Box::new(watch))
1729 .add_layer(0, Box::new(sequence));
1730
1731 assert_eq!(subsumption.tick(&ctx).await.unwrap(), NodeStatus::Running);
1732 assert_eq!(leg1_ticks.load(Ordering::SeqCst), 1);
1733 assert_eq!(subsumption.tick(&ctx).await.unwrap(), NodeStatus::Running);
1734 assert_eq!(leg1_ticks.load(Ordering::SeqCst), 2);
1735
1736 assert_eq!(subsumption.tick(&ctx).await.unwrap(), NodeStatus::Running);
1737 assert_eq!(leg1_ticks.load(Ordering::SeqCst), 3);
1738 assert_eq!(leg2_ticks.load(Ordering::SeqCst), 1);
1739
1740 engaged.store(true, Ordering::SeqCst);
1741 for _ in 0..3 {
1742 assert_eq!(
1743 subsumption.tick(&ctx).await.unwrap(),
1744 NodeStatus::Running,
1745 "the instance_track layer wins while engaged"
1746 );
1747 }
1748 assert_eq!(
1749 leg2_ticks.load(Ordering::SeqCst),
1750 1,
1751 "the main layer must be frozen (not reset, not advanced) while the interrupt layer is engaged"
1752 );
1753
1754 engaged.store(false, Ordering::SeqCst);
1755 let status = subsumption.tick(&ctx).await.unwrap();
1756 assert_eq!(
1757 status,
1758 NodeStatus::Running,
1759 "handback must surface the Sequence's own status, not Success/Failure from the ceding interrupt layer"
1760 );
1761 assert_eq!(
1762 leg1_ticks.load(Ordering::SeqCst),
1763 3,
1764 "leg1 must not be re-ticked - the sequence was not reset"
1765 );
1766 assert_eq!(
1767 leg2_ticks.load(Ordering::SeqCst),
1768 2,
1769 "the Sequence resumes ticking leg2 (not leg1) - progress was preserved, not reset"
1770 );
1771 }
1772
1773 #[tokio::test]
1782 #[ignore = "Requires Redis infrastructure"]
1783 async fn emergency_stop_layer_preempts_both_instance_track_and_the_main_layer_and_resumes_on_clear() {
1784 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1785 use std::sync::Arc;
1786
1787 let ctx = crate::test_helpers::create_test_context("subsumption_estop_resume").await;
1788
1789 let estop_active = Arc::new(AtomicBool::new(false));
1790 let instance_track_engaged = Arc::new(AtomicBool::new(false));
1791 let leg1_ticks = Arc::new(AtomicUsize::new(0));
1792 let leg2_ticks = Arc::new(AtomicUsize::new(0));
1793
1794 let estop_watch = WatchNode::new(
1795 vec![Box::new(ToggleableBehavior {
1796 engaged: estop_active.clone(),
1797 })],
1798 ParallelPolicy::RequireOne,
1799 );
1800
1801 let instance_track_watch = WatchNode::new(
1802 vec![Box::new(ToggleableBehavior {
1803 engaged: instance_track_engaged.clone(),
1804 })],
1805 ParallelPolicy::RequireOne,
1806 );
1807
1808 let main_sequence = SequenceNode::new(vec![
1809 Box::new(CountedLegBehavior {
1810 running_ticks_remaining: 2,
1811 tick_count: leg1_ticks.clone(),
1812 }),
1813 Box::new(CountedLegBehavior {
1814 running_ticks_remaining: 2,
1815 tick_count: leg2_ticks.clone(),
1816 }),
1817 ]);
1818
1819 let mut subsumption = SubsumptionNode::new()
1820 .add_layer(255, Box::new(estop_watch))
1821 .add_layer(100, Box::new(instance_track_watch))
1822 .add_layer(0, Box::new(main_sequence));
1823
1824 assert_eq!(subsumption.tick(&ctx).await.unwrap(), NodeStatus::Running);
1826 assert_eq!(leg1_ticks.load(Ordering::SeqCst), 1);
1827
1828 instance_track_engaged.store(true, Ordering::SeqCst);
1830 assert_eq!(subsumption.tick(&ctx).await.unwrap(), NodeStatus::Running);
1831 assert_eq!(
1832 leg1_ticks.load(Ordering::SeqCst),
1833 1,
1834 "main layer frozen while instance_track is engaged"
1835 );
1836
1837 estop_active.store(true, Ordering::SeqCst);
1840 for _ in 0..3 {
1841 assert_eq!(
1842 subsumption.tick(&ctx).await.unwrap(),
1843 NodeStatus::Running,
1844 "the emergency-stop layer must win even over an already-engaged instance_track layer"
1845 );
1846 }
1847 assert_eq!(
1848 leg1_ticks.load(Ordering::SeqCst),
1849 1,
1850 "main layer must stay frozen while e-stop is active"
1851 );
1852
1853 instance_track_engaged.store(false, Ordering::SeqCst);
1855 assert_eq!(
1856 subsumption.tick(&ctx).await.unwrap(),
1857 NodeStatus::Running,
1858 "e-stop still active, so it keeps control even after instance_track clears"
1859 );
1860 assert_eq!(leg1_ticks.load(Ordering::SeqCst), 1, "main layer still frozen");
1861
1862 estop_active.store(false, Ordering::SeqCst);
1864 let status = subsumption.tick(&ctx).await.unwrap();
1865 assert_eq!(
1866 status,
1867 NodeStatus::Running,
1868 "handback surfaces the Sequence's own status once e-stop clears"
1869 );
1870 assert_eq!(
1871 leg1_ticks.load(Ordering::SeqCst),
1872 2,
1873 "leg1 resumes from where it left off (1 prior tick), not reset to 0"
1874 );
1875 assert_eq!(leg2_ticks.load(Ordering::SeqCst), 0, "leg2 not yet reached");
1876 }
1877
1878 #[derive(Debug)]
1879 struct FixedStatusBehavior(NodeStatus);
1880
1881 #[async_trait]
1882 impl BehaviorNode for FixedStatusBehavior {
1883 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
1884 Ok(self.0)
1885 }
1886 }
1887
1888 #[tokio::test]
1889 #[ignore = "Requires Redis infrastructure"]
1890 async fn inverter_flips_success_and_failure_but_passes_running_through() {
1891 let ctx = crate::test_helpers::create_test_context("inverter").await;
1892
1893 let mut success = InverterNode::new(Box::new(FixedStatusBehavior(NodeStatus::Success)));
1894 assert_eq!(success.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1895
1896 let mut failure = InverterNode::new(Box::new(FixedStatusBehavior(NodeStatus::Failure)));
1897 assert_eq!(failure.tick(&ctx).await.unwrap(), NodeStatus::Success);
1898
1899 let mut running = InverterNode::new(Box::new(FixedStatusBehavior(NodeStatus::Running)));
1900 assert_eq!(running.tick(&ctx).await.unwrap(), NodeStatus::Running);
1901 }
1902
1903 #[tokio::test]
1904 #[ignore = "Requires Redis infrastructure"]
1905 async fn force_success_reports_success_even_when_child_fails() {
1906 let ctx = crate::test_helpers::create_test_context("force_success").await;
1907
1908 let mut node = ForceSuccessNode::new(Box::new(FixedStatusBehavior(NodeStatus::Failure)));
1909 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
1910
1911 let mut running = ForceSuccessNode::new(Box::new(FixedStatusBehavior(NodeStatus::Running)));
1912 assert_eq!(running.tick(&ctx).await.unwrap(), NodeStatus::Running);
1913 }
1914
1915 #[tokio::test]
1916 #[ignore = "Requires Redis infrastructure"]
1917 async fn force_failure_reports_failure_even_when_child_succeeds() {
1918 let ctx = crate::test_helpers::create_test_context("force_failure").await;
1919
1920 let mut node = ForceFailureNode::new(Box::new(FixedStatusBehavior(NodeStatus::Success)));
1921 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1922
1923 let mut running = ForceFailureNode::new(Box::new(FixedStatusBehavior(NodeStatus::Running)));
1924 assert_eq!(running.tick(&ctx).await.unwrap(), NodeStatus::Running);
1925 }
1926
1927 #[tokio::test]
1928 #[ignore = "Requires Redis infrastructure"]
1929 async fn repeat_with_count_succeeds_after_n_successful_runs() {
1930 let ctx = crate::test_helpers::create_test_context("repeat_count").await;
1931
1932 let mut node = RepeatNode::new(Box::new(FixedStatusBehavior(NodeStatus::Success)), Some(3));
1933 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1934 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1935 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
1936 }
1937
1938 #[tokio::test]
1939 #[ignore = "Requires Redis infrastructure"]
1940 async fn repeat_stops_immediately_on_child_failure() {
1941 let ctx = crate::test_helpers::create_test_context("repeat_failure").await;
1942
1943 let mut node = RepeatNode::new(Box::new(FixedStatusBehavior(NodeStatus::Failure)), None);
1944 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1945 }
1946
1947 #[tokio::test]
1948 #[ignore = "Requires Redis infrastructure"]
1949 async fn retry_succeeds_once_an_attempt_within_the_budget_succeeds() {
1950 let ctx = crate::test_helpers::create_test_context("retry_succeeds").await;
1951
1952 let mut node = RetryNode::new(
1953 Box::new(SequencedStatusBehavior::new(vec![
1954 NodeStatus::Failure,
1955 NodeStatus::Failure,
1956 NodeStatus::Success,
1957 ])),
1958 3,
1959 );
1960 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1961 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1962 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
1963 }
1964
1965 #[tokio::test]
1966 #[ignore = "Requires Redis infrastructure"]
1967 async fn retry_fails_once_every_attempt_is_exhausted() {
1968 let ctx = crate::test_helpers::create_test_context("retry_exhausted").await;
1969
1970 let mut node = RetryNode::new(Box::new(FixedStatusBehavior(NodeStatus::Failure)), 2);
1971 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
1972 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1973 }
1974
1975 #[tokio::test]
1976 #[ignore = "Requires Redis infrastructure"]
1977 async fn timeout_passes_through_a_child_that_resolves_before_the_deadline() {
1978 let ctx = crate::test_helpers::create_test_context("timeout_resolves").await;
1979
1980 let mut node = TimeoutNode::new(
1981 Box::new(FixedStatusBehavior(NodeStatus::Success)),
1982 Duration::from_secs(60),
1983 );
1984 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
1985 }
1986
1987 #[tokio::test]
1988 #[ignore = "Requires Redis infrastructure"]
1989 async fn timeout_fails_once_the_child_has_been_running_too_long() {
1990 let ctx = crate::test_helpers::create_test_context("timeout_expires").await;
1991
1992 let mut node = TimeoutNode::new(Box::new(FixedStatusBehavior(NodeStatus::Running)), Duration::ZERO);
1993 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
1994 }
1995
1996 static CONTROLLER_ENV_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
2007
2008 fn clear_controller_env() {
2009 std::env::remove_var(mecha10_core::config::CAPABILITIES_ENV_VAR);
2010 std::env::remove_var(mecha10_core::config::ACTUATION_JOINT_NAMES_ENV_VAR);
2011 }
2012
2013 #[test]
2014 fn controller_mode_is_evaluates_against_the_current_controller_mode() {
2015 let _guard = CONTROLLER_ENV_LOCK.blocking_lock();
2016 clear_controller_env();
2017
2018 assert!(ConditionKind::ControllerModeIs(ControllerModeKind::Locomotion).evaluate());
2021 assert!(!ConditionKind::ControllerModeIs(ControllerModeKind::Joints).evaluate());
2022
2023 std::env::set_var(mecha10_core::config::CAPABILITIES_ENV_VAR, r#"["joints"]"#);
2024 assert!(ConditionKind::ControllerModeIs(ControllerModeKind::Joints).evaluate());
2025 assert!(!ConditionKind::ControllerModeIs(ControllerModeKind::Locomotion).evaluate());
2026
2027 clear_controller_env();
2028 }
2029
2030 #[tokio::test]
2031 #[ignore = "Requires Redis infrastructure"]
2032 async fn condition_ticks_child_and_forwards_its_status_when_the_predicate_holds() {
2033 let _guard = CONTROLLER_ENV_LOCK.lock().await;
2034 clear_controller_env(); let ctx = crate::test_helpers::create_test_context("condition_true").await;
2037 let mut node = ConditionNode::new(
2038 ConditionKind::ControllerModeIs(ControllerModeKind::Locomotion),
2039 Box::new(FixedStatusBehavior(NodeStatus::Running)),
2040 );
2041
2042 assert_eq!(
2043 node.tick(&ctx).await.unwrap(),
2044 NodeStatus::Running,
2045 "a true predicate must tick the child and forward its status unchanged"
2046 );
2047
2048 clear_controller_env();
2049 }
2050
2051 #[tokio::test]
2052 #[ignore = "Requires Redis infrastructure"]
2053 async fn condition_returns_failure_without_ticking_child_when_the_predicate_is_false() {
2054 let _guard = CONTROLLER_ENV_LOCK.lock().await;
2055 clear_controller_env();
2056 std::env::set_var(mecha10_core::config::CAPABILITIES_ENV_VAR, r#"["joints"]"#);
2057
2058 let ctx = crate::test_helpers::create_test_context("condition_false").await;
2059 let mut node = ConditionNode::new(
2062 ConditionKind::ControllerModeIs(ControllerModeKind::Locomotion),
2063 Box::new(DummyBehavior),
2064 );
2065
2066 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Failure);
2067
2068 clear_controller_env();
2069 }
2070
2071 #[test]
2072 fn condition_children_mut_exposes_the_child() {
2073 let mut node = ConditionNode::new(
2074 ConditionKind::ControllerModeIs(ControllerModeKind::Joints),
2075 Box::new(DummyBehavior),
2076 );
2077 assert_eq!(node.children_mut().len(), 1);
2078 }
2079
2080 #[test]
2081 fn condition_forwards_its_childs_active_capability() {
2082 let node = ConditionNode::new(
2083 ConditionKind::ControllerModeIs(ControllerModeKind::Locomotion),
2084 Box::new(CapabilityBehavior("sweep")),
2085 );
2086 assert_eq!(node.active_capability(), Some("sweep"));
2087 }
2088
2089 #[test]
2090 fn weighted_select_children_mut_exposes_every_child() {
2091 let mut node = WeightedSelectNode::new(vec![
2092 (1.0, Box::new(DummyBehavior) as BoxedBehavior),
2093 (1.0, Box::new(DummyBehavior) as BoxedBehavior),
2094 ]);
2095 assert_eq!(node.children_mut().len(), 2);
2096 }
2097
2098 #[tokio::test]
2099 #[ignore = "Requires Redis infrastructure"]
2100 async fn weighted_select_never_picks_a_zero_weight_child() {
2101 let ctx = crate::test_helpers::create_test_context("weighted_select_zero_weight").await;
2102
2103 let mut node = WeightedSelectNode::with_rng(
2107 vec![
2108 (1.0, Box::new(FixedStatusBehavior(NodeStatus::Failure)) as BoxedBehavior),
2109 (0.0, Box::new(DummyBehavior) as BoxedBehavior),
2110 ],
2111 StdRng::seed_from_u64(42),
2112 );
2113
2114 assert_eq!(
2115 node.tick(&ctx).await.unwrap(),
2116 NodeStatus::Failure,
2117 "the only eligible (positive-weight) child fails, and the zero-weight one must not be tried"
2118 );
2119 }
2120
2121 #[tokio::test]
2122 #[ignore = "Requires Redis infrastructure"]
2123 async fn weighted_select_does_not_reroll_away_from_a_running_child() {
2124 use std::sync::atomic::{AtomicUsize, Ordering};
2125 use std::sync::Arc;
2126
2127 let ctx = crate::test_helpers::create_test_context("weighted_select_sticky").await;
2128
2129 let leg_a_ticks = Arc::new(AtomicUsize::new(0));
2130 let leg_b_ticks = Arc::new(AtomicUsize::new(0));
2131
2132 let mut node = WeightedSelectNode::with_rng(
2133 vec![
2134 (
2135 1.0,
2136 Box::new(CountedLegBehavior {
2137 running_ticks_remaining: 1,
2138 tick_count: leg_a_ticks.clone(),
2139 }) as BoxedBehavior,
2140 ),
2141 (
2142 1.0,
2143 Box::new(CountedLegBehavior {
2144 running_ticks_remaining: 1,
2145 tick_count: leg_b_ticks.clone(),
2146 }) as BoxedBehavior,
2147 ),
2148 ],
2149 StdRng::seed_from_u64(7),
2150 );
2151
2152 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
2153 let a_after_first = leg_a_ticks.load(Ordering::SeqCst);
2154 let b_after_first = leg_b_ticks.load(Ordering::SeqCst);
2155 assert_eq!(
2156 a_after_first + b_after_first,
2157 1,
2158 "exactly one child should have been ticked"
2159 );
2160
2161 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
2164 assert_eq!(
2165 leg_a_ticks.load(Ordering::SeqCst) + leg_b_ticks.load(Ordering::SeqCst),
2166 2,
2167 "the same child that was Running must be re-ticked (reaching 2 ticks), not the other \
2168 one (which must remain untouched at 0)"
2169 );
2170 assert!(
2171 leg_a_ticks.load(Ordering::SeqCst) == 0 || leg_b_ticks.load(Ordering::SeqCst) == 0,
2172 "the child that was never chosen must never be ticked at all"
2173 );
2174 }
2175
2176 #[tokio::test]
2177 #[ignore = "Requires Redis infrastructure"]
2178 async fn weighted_select_forwards_the_current_childs_active_capability() {
2179 let ctx = crate::test_helpers::create_test_context("weighted_select_capability").await;
2180
2181 let mut node = WeightedSelectNode::with_rng(
2182 vec![(
2183 1.0,
2184 Box::new(StatusAndCapabilityBehavior {
2185 status: NodeStatus::Running,
2186 capability: "patrol",
2187 }) as BoxedBehavior,
2188 )],
2189 StdRng::seed_from_u64(1),
2190 );
2191
2192 assert_eq!(node.active_capability(), None, "no tick has happened yet");
2193
2194 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
2195 assert_eq!(node.active_capability(), Some("patrol"));
2196 }
2197
2198 #[tokio::test]
2199 async fn weighted_select_reset_clears_current_index_and_tried() {
2200 let mut node = WeightedSelectNode::with_rng(
2201 vec![
2202 (1.0, Box::new(DummyBehavior) as BoxedBehavior),
2203 (1.0, Box::new(DummyBehavior) as BoxedBehavior),
2204 ],
2205 StdRng::seed_from_u64(3),
2206 );
2207 node.current_index = Some(1);
2208 node.tried[0] = true;
2209
2210 node.reset().await.unwrap();
2211
2212 assert_eq!(node.current_index, None);
2213 assert!(node.tried.iter().all(|t| !t));
2214 }
2215
2216 #[derive(Debug)]
2217 struct CountingFixedStatusBehavior {
2218 status: NodeStatus,
2219 tick_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
2220 }
2221
2222 #[async_trait]
2223 impl BehaviorNode for CountingFixedStatusBehavior {
2224 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
2225 self.tick_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2226 Ok(self.status)
2227 }
2228 }
2229
2230 #[tokio::test]
2231 #[ignore = "Requires Redis infrastructure"]
2232 async fn cooldown_blocks_reentry_immediately_after_a_terminal_result() {
2233 use std::sync::atomic::{AtomicUsize, Ordering};
2234 use std::sync::Arc;
2235
2236 let ctx = crate::test_helpers::create_test_context("cooldown_blocks").await;
2237
2238 let tick_count = Arc::new(AtomicUsize::new(0));
2239 let mut node = CooldownNode::new(
2240 Box::new(CountingFixedStatusBehavior {
2241 status: NodeStatus::Success,
2242 tick_count: tick_count.clone(),
2243 }),
2244 Duration::from_secs(60),
2245 );
2246
2247 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
2248 assert_eq!(tick_count.load(Ordering::SeqCst), 1);
2249
2250 assert_eq!(
2251 node.tick(&ctx).await.unwrap(),
2252 NodeStatus::Failure,
2253 "within the cooldown window, the node must fail without ticking the child"
2254 );
2255 assert_eq!(
2256 tick_count.load(Ordering::SeqCst),
2257 1,
2258 "the child must not have been ticked again while cooling down"
2259 );
2260 }
2261
2262 #[tokio::test]
2263 #[ignore = "Requires Redis infrastructure"]
2264 async fn cooldown_expires_and_ticking_resumes() {
2265 use std::sync::atomic::{AtomicUsize, Ordering};
2266 use std::sync::Arc;
2267
2268 let ctx = crate::test_helpers::create_test_context("cooldown_expires").await;
2269
2270 let tick_count = Arc::new(AtomicUsize::new(0));
2271 let mut node = CooldownNode::new(
2272 Box::new(CountingFixedStatusBehavior {
2273 status: NodeStatus::Success,
2274 tick_count: tick_count.clone(),
2275 }),
2276 Duration::ZERO,
2277 );
2278
2279 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Success);
2280 assert_eq!(tick_count.load(Ordering::SeqCst), 1);
2281
2282 assert_eq!(
2284 node.tick(&ctx).await.unwrap(),
2285 NodeStatus::Success,
2286 "once the cooldown window elapses, ticking must resume normally"
2287 );
2288 assert_eq!(tick_count.load(Ordering::SeqCst), 2);
2289 }
2290
2291 #[tokio::test]
2292 #[ignore = "Requires Redis infrastructure"]
2293 async fn cooldown_does_not_start_while_the_child_is_running() {
2294 use std::sync::atomic::{AtomicUsize, Ordering};
2295 use std::sync::Arc;
2296
2297 let ctx = crate::test_helpers::create_test_context("cooldown_running").await;
2298
2299 let tick_count = Arc::new(AtomicUsize::new(0));
2300 let mut node = CooldownNode::new(
2301 Box::new(CountingFixedStatusBehavior {
2302 status: NodeStatus::Running,
2303 tick_count: tick_count.clone(),
2304 }),
2305 Duration::from_secs(60),
2306 );
2307
2308 assert_eq!(node.tick(&ctx).await.unwrap(), NodeStatus::Running);
2309 assert_eq!(tick_count.load(Ordering::SeqCst), 1);
2310
2311 assert_eq!(
2312 node.tick(&ctx).await.unwrap(),
2313 NodeStatus::Running,
2314 "Running must not start the cooldown clock, so the child must be ticked again"
2315 );
2316 assert_eq!(tick_count.load(Ordering::SeqCst), 2);
2317 }
2318
2319 #[test]
2320 fn cooldown_children_mut_exposes_the_child() {
2321 let mut node = CooldownNode::new(Box::new(DummyBehavior), Duration::from_secs(1));
2322 assert_eq!(node.children_mut().len(), 1);
2323 }
2324
2325 #[test]
2326 fn cooldown_forwards_its_childs_active_capability() {
2327 let node = CooldownNode::new(Box::new(CapabilityBehavior("sweep")), Duration::from_secs(1));
2328 assert_eq!(node.active_capability(), Some("sweep"));
2329 }
2330
2331 #[test]
2332 fn cooldown_reset_clears_cooldown_until() {
2333 let mut node = CooldownNode::new(Box::new(DummyBehavior), Duration::from_secs(1));
2334 node.cooldown_until = Some(Instant::now() + Duration::from_secs(60));
2335
2336 node.reset_self();
2337
2338 assert_eq!(node.cooldown_until, None);
2339 }
2340}