From 1da6d64ce31b72f81c2426cd4f532c4c44782ae7 Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Wed, 4 Feb 2026 13:46:04 +0100 Subject: [PATCH] Add test for Issue #819: Sequence vs ReactiveSequence behavior Demonstrates that regular Sequence does NOT re-evaluate conditions while a child action is RUNNING, whereas ReactiveSequence DOES. This is expected behavior - users should use ReactiveSequence when they need conditions to be re-evaluated every tick. Co-Authored-By: Claude Opus 4.5 --- tests/gtest_parallel.cpp | 103 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/tests/gtest_parallel.cpp b/tests/gtest_parallel.cpp index 8dd8769c9..8cc35dd8a 100644 --- a/tests/gtest_parallel.cpp +++ b/tests/gtest_parallel.cpp @@ -591,3 +591,106 @@ TEST(Parallel, PauseWithRetry) // the whole process should take about 300 milliseconds ASSERT_LE(toMsec(t2 - t1) - 300, margin_msec * 2); } + +// Issue #819: Demonstrates that Sequence does NOT re-evaluate conditions +// while a sibling action is RUNNING, whereas ReactiveSequence DOES. +// This is expected behavior, not a bug. +TEST(Parallel, Issue819_SequenceVsReactiveSequence) +{ + using namespace BT; + + // Test 1: Regular Sequence - condition NOT re-evaluated + { + static const char* xml_text = R"( + + + + + + + + + + + + + + +)"; + BehaviorTreeFactory factory; + std::array tick_counts = { 0, 0 }; + + // Register conditions that count their ticks + factory.registerSimpleCondition("TestCondition", [&](TreeNode& node) { + const std::string& name = node.name(); + if(name == "cond1") + tick_counts[0]++; + else if(name == "cond2") + tick_counts[1]++; + return NodeStatus::SUCCESS; + }); + + auto tree = factory.createTreeFromText(xml_text); + + // First tick: both conditions evaluated + auto status = tree.tickExactlyOnce(); + ASSERT_EQ(NodeStatus::RUNNING, status); + ASSERT_EQ(1, tick_counts[0]); // cond1 ticked once + ASSERT_EQ(1, tick_counts[1]); // cond2 ticked once + + // Second tick: conditions should NOT be re-evaluated (Sequence behavior) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + status = tree.tickExactlyOnce(); + ASSERT_EQ(NodeStatus::RUNNING, status); + // Conditions are NOT re-ticked because Sequence remembers current_child_idx_ + ASSERT_EQ(1, tick_counts[0]); // Still 1 - NOT re-evaluated + ASSERT_EQ(1, tick_counts[1]); // Still 1 - NOT re-evaluated + } + + // Test 2: ReactiveSequence - condition IS re-evaluated every tick + { + static const char* xml_text = R"( + + + + + + + + + + + + + + +)"; + BehaviorTreeFactory factory; + std::array tick_counts = { 0, 0 }; + + factory.registerSimpleCondition("TestCondition", [&](TreeNode& node) { + const std::string& name = node.name(); + if(name == "cond1") + tick_counts[0]++; + else if(name == "cond2") + tick_counts[1]++; + return NodeStatus::SUCCESS; + }); + + auto tree = factory.createTreeFromText(xml_text); + + // First tick + auto status = tree.tickExactlyOnce(); + ASSERT_EQ(NodeStatus::RUNNING, status); + ASSERT_EQ(1, tick_counts[0]); + ASSERT_EQ(1, tick_counts[1]); + + // Second tick: conditions SHOULD be re-evaluated (ReactiveSequence behavior) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + status = tree.tickExactlyOnce(); + ASSERT_EQ(NodeStatus::RUNNING, status); + // Conditions ARE re-ticked because ReactiveSequence always starts from index 0 + ASSERT_EQ(2, tick_counts[0]); // Re-evaluated! + ASSERT_EQ(2, tick_counts[1]); // Re-evaluated! + } +}