Skip to main content

mecha10_behavior_runtime/
composition.rs

1//! Composition primitives for building behavior trees
2//!
3//! Includes Sequence, Selector, and Parallel, plus single-child decorators (Inverter,
4
5use 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    /// Create a new sequence node with the given children.
19    pub fn new(children: Vec<BoxedBehavior>) -> Self {
20        Self {
21            children,
22            current_index: 0,
23        }
24    }
25
26    /// Add a child behavior to the sequence.
27    pub fn add_child(&mut self, child: BoxedBehavior) {
28        self.children.push(child);
29    }
30
31    /// Get the number of children.
32    pub fn len(&self) -> usize {
33        self.children.len()
34    }
35
36    /// Check if the sequence is empty.
37    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    /// Create a new selector node with the given children.
101    pub fn new(children: Vec<BoxedBehavior>) -> Self {
102        Self {
103            children,
104            current_index: 0,
105        }
106    }
107
108    /// Add a child behavior to the selector.
109    pub fn add_child(&mut self, child: BoxedBehavior) {
110        self.children.push(child);
111    }
112
113    /// Get the number of children.
114    pub fn len(&self) -> usize {
115        self.children.len()
116    }
117
118    /// Check if the selector is empty.
119    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    /// Create a new parallel node with the given children and policy.
194    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    /// Add a child behavior to the parallel node.
204    pub fn add_child(&mut self, child: BoxedBehavior) {
205        self.children.push(child);
206        self.child_statuses.push(NodeStatus::Running);
207    }
208
209    /// Get the number of children.
210    pub fn len(&self) -> usize {
211        self.children.len()
212    }
213
214    /// Check if the parallel node is empty.
215    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    /// The first still-`Running` child's capability, if any - "first" is an arbitrary but
287    /// deterministic tie-break since at most one running child is expected to declare one.
288    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/// Like `ParallelNode`, but with no memory of a prior resolution: every child is ticked, every
298/// call, unconditionally. Needed for an interrupt-condition layer that must be re-evaluated
299/// indefinitely (e.g. the operator clicking/clearing repeatedly over a session).
300#[derive(Debug)]
301pub struct WatchNode {
302    children: Vec<BoxedBehavior>,
303    policy: ParallelPolicy,
304    last_active: Option<usize>,
305}
306
307impl WatchNode {
308    /// Create a new watch node with the given children and policy.
309    pub fn new(children: Vec<BoxedBehavior>, policy: ParallelPolicy) -> Self {
310        Self {
311            children,
312            policy,
313            last_active: None,
314        }
315    }
316
317    /// Add a child behavior to the watch node.
318    pub fn add_child(&mut self, child: BoxedBehavior) {
319        self.children.push(child);
320    }
321
322    /// Get the number of children.
323    pub fn len(&self) -> usize {
324        self.children.len()
325    }
326
327    /// Check if the watch node is empty.
328    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    /// Unlike Sequence/Selector/Parallel, every child here is ticked every time with no persisted
397    /// statuses to read after the fact - `tick()` records which child resolved `Running` *this*
398    /// call into `last_active`, and this forwards to that child.
399    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/// A single priority layer in a [`SubsumptionNode`]: a priority (higher wins) plus the behavior
407/// that implements it.
408#[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        /// Create a new subsumption layer.
418        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/// Priority-based preemption (subsumption architecture): on each tick, tries each layer
427/// highest-priority first. The first layer whose `tick()` returns `Running`/`Success` "wins";
428/// lower-priority layers aren't ticked that cycle, preserving their internal progress.
429#[derive(Debug)]
430pub struct SubsumptionNode {
431    pub layers: Vec<SubsumptionLayer>,
432    last_winning_layer: Option<usize>,
433}
434
435impl SubsumptionNode {
436    /// Create a new empty subsumption node.
437    pub fn new() -> Self {
438        Self {
439            layers: Vec::new(),
440            last_winning_layer: None,
441        }
442    }
443
444    /// Add a layer with the given priority and behavior.
445    ///
446    /// Layers are automatically sorted by priority (highest first).
447    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    /// Forwards the winning layer's capability (the highest-priority layer whose `tick()`
506    /// returned `Running`/`Success` this call) - `None` if every layer failed. Tracked separately
507    /// via `last_winning_layer` since it isn't otherwise recoverable after `tick()` returns.
508    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/// Negates a `Success`/`Failure` result from its single child; `Running` passes through
516/// unchanged. The canonical decorator for turning a "condition" leaf/subtree into its opposite
517/// (e.g. "target NOT in range") without duplicating the underlying check.
518#[derive(Debug)]
519pub struct InverterNode {
520    child: BoxedBehavior,
521}
522
523impl InverterNode {
524    /// Create a new inverter node wrapping `child`.
525    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/// Always reports `Success` once its child resolves, regardless of whether the child actually
554/// succeeded or failed; `Running` passes through unchanged. Useful for an optional step whose
555/// failure shouldn't abort the rest of a `Sequence`.
556#[derive(Debug)]
557pub struct ForceSuccessNode {
558    child: BoxedBehavior,
559}
560
561impl ForceSuccessNode {
562    /// Create a new force-success node wrapping `child`.
563    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/// Always reports `Failure` once its child resolves, regardless of whether the child actually
591/// succeeded or failed; `Running` passes through unchanged. The mirror of `ForceSuccessNode`,
592/// useful inside a `Selector` to force a fallback to the next option.
593#[derive(Debug)]
594pub struct ForceFailureNode {
595    child: BoxedBehavior,
596}
597
598impl ForceFailureNode {
599    /// Create a new force-failure node wrapping `child`.
600    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/// Re-runs its child every time it succeeds, up to an optional fixed count; a `Failure` from the
628/// child always ends the loop immediately with `Failure`. `count: None` repeats indefinitely
629/// (e.g. a patrol leg re-run forever until something fails it).
630#[derive(Debug)]
631pub struct RepeatNode {
632    child: BoxedBehavior,
633    count: Option<u32>,
634    completed: u32,
635}
636
637impl RepeatNode {
638    /// Create a new repeat node wrapping `child`. `count: None` repeats indefinitely.
639    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/// Re-runs its child up to `max_attempts` times whenever it fails, succeeding as soon as one
690/// attempt succeeds; only reports `Failure` once every attempt has been exhausted. The
691/// complement of `RepeatNode` (which loops on success).
692#[derive(Debug)]
693pub struct RetryNode {
694    child: BoxedBehavior,
695    max_attempts: u32,
696    attempts: u32,
697}
698
699impl RetryNode {
700    /// Create a new retry node wrapping `child`, allowing up to `max_attempts` tries (must be at
701    /// least 1).
702    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/// Bounds how long its child may stay `Running`: once `duration` elapses without the child
752/// resolving, immediately reports `Failure` (resetting the child's state). A child that resolves
753/// before the deadline passes its status straight through.
754#[derive(Debug)]
755pub struct TimeoutNode {
756    child: BoxedBehavior,
757    duration: Duration,
758    start_time: Option<Instant>,
759}
760
761impl TimeoutNode {
762    /// Create a new timeout node wrapping `child`, bounding it to `duration`.
763    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    /// A child whose status changes from tick to tick (e.g. a target that comes and goes), used
922    /// to prove `WatchNode` re-evaluates every call instead of freezing like `ParallelNode`.
923    #[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    /// Regression test: unlike `ParallelNode`, `WatchNode` has no `child_statuses` memory, so a
983    /// child that flips from `Failure` to `Running` across ticks must be re-ticked every call and
984    /// the aggregate status must reflect *that* tick's actual child statuses, not a stale one.
985    #[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    /// A double that both returns a fixed `NodeStatus` from `tick()` and declares a capability -
1039    /// `CapabilityBehavior` above only does the latter, so it can't exercise
1040    /// `SubsumptionNode::active_capability()` end-to-end through an actual `tick()` call.
1041    #[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    /// A stub mimicking `InstanceTrackTaskNode`/`SetInstanceTrackTargetNode`'s
1105    /// Failure-when-idle/Running-when-engaged convergence, controllable via a shared flag -
1106    /// zero `Context`/messaging dependency, same style as `DummyBehavior` above.
1107    #[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    /// Simulates one leg of `patrol-simple`'s timed-move sequence: `Running` for a fixed number
1124    /// of ticks, then `Success`. Records how many times it was ticked, so the test can observe
1125    /// whether a frozen `SequenceNode` child is skipped while a higher-priority layer is engaged.
1126    #[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    /// The property that matters most: a `Subsumption` tree with a `Watch`-wrapped interrupt layer
1146    /// must let the interrupt freeze the main layer mid-sequence, then hand control back on the
1147    /// *same* tick it disengages, resuming exactly where it was.
1148    #[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}