pw_bluetooth_sapphire: Acquire wake leases in SignalingChannel

Acquire wake leases while commands are pending in SignalingChannel.

Bug: 408059126
Change-Id: Ie4ffce7bae78a95ec0edc67c3a20b9536f463d7c
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/280974
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jason Graffius <jgraff@google.com>
Docs-Not-Needed: Ben Lawson <benlawson@google.com>
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Commit-Queue: Ben Lawson <benlawson@google.com>
diff --git a/pw_bluetooth_sapphire/host/l2cap/BUILD.bazel b/pw_bluetooth_sapphire/host/l2cap/BUILD.bazel
index 2cd2acd..31284b5 100644
--- a/pw_bluetooth_sapphire/host/l2cap/BUILD.bazel
+++ b/pw_bluetooth_sapphire/host/l2cap/BUILD.bazel
@@ -257,6 +257,7 @@
     deps = [
         ":l2cap",
         ":testing",
+        "//pw_bluetooth_sapphire:null_lease_provider",
         "//pw_random:fuzzer_generator",
     ],
 )
diff --git a/pw_bluetooth_sapphire/host/l2cap/BUILD.gn b/pw_bluetooth_sapphire/host/l2cap/BUILD.gn
index 6ae18aa..9a7dfc1 100644
--- a/pw_bluetooth_sapphire/host/l2cap/BUILD.gn
+++ b/pw_bluetooth_sapphire/host/l2cap/BUILD.gn
@@ -232,6 +232,7 @@
   deps = [
     ":l2cap",
     ":testing",
+    "$dir_pw_bluetooth_sapphire:null_lease_provider",
     "$dir_pw_random:fuzzer_generator",
   ]
 }
diff --git a/pw_bluetooth_sapphire/host/l2cap/bredr_dynamic_channel_registry_fuzztest.cc b/pw_bluetooth_sapphire/host/l2cap/bredr_dynamic_channel_registry_fuzztest.cc
index f710733..5579ec6 100644
--- a/pw_bluetooth_sapphire/host/l2cap/bredr_dynamic_channel_registry_fuzztest.cc
+++ b/pw_bluetooth_sapphire/host/l2cap/bredr_dynamic_channel_registry_fuzztest.cc
@@ -21,6 +21,7 @@
 #include "pw_bluetooth_sapphire/internal/host/l2cap/bredr_dynamic_channel.h"
 #include "pw_bluetooth_sapphire/internal/host/l2cap/bredr_signaling_channel.h"
 #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel.h"
+#include "pw_bluetooth_sapphire/null_lease_provider.h"
 
 constexpr static bt::hci_spec::ConnectionHandle kTestHandle = 0x0001;
 
@@ -49,6 +50,8 @@
   // Dispatcher needed for signaling channel response timeout.
   pw::async::test::FakeDispatcher dispatcher;
 
+  pw::bluetooth_sapphire::NullLeaseProvider lease_provider;
+
   auto fake_chan = std::make_unique<bt::l2cap::testing::FakeChannel>(
       bt::l2cap::kSignalingChannelId,
       bt::l2cap::kSignalingChannelId,
@@ -61,7 +64,8 @@
   bt::l2cap::internal::BrEdrSignalingChannel sig_chan(
       fake_chan->GetWeakPtr(),
       pw::bluetooth::emboss::ConnectionRole::CENTRAL,
-      dispatcher);
+      dispatcher,
+      lease_provider);
 
   auto open_cb = []([[maybe_unused]] auto chan) {};
   auto close_cb = []([[maybe_unused]] auto chan) {};
diff --git a/pw_bluetooth_sapphire/host/l2cap/bredr_signaling_channel.cc b/pw_bluetooth_sapphire/host/l2cap/bredr_signaling_channel.cc
index d836fff..84a37ad 100644
--- a/pw_bluetooth_sapphire/host/l2cap/bredr_signaling_channel.cc
+++ b/pw_bluetooth_sapphire/host/l2cap/bredr_signaling_channel.cc
@@ -24,8 +24,9 @@
 BrEdrSignalingChannel::BrEdrSignalingChannel(
     Channel::WeakPtr chan,
     pw::bluetooth::emboss::ConnectionRole role,
-    pw::async::Dispatcher& dispatcher)
-    : SignalingChannel(std::move(chan), role, dispatcher) {
+    pw::async::Dispatcher& dispatcher,
+    pw::bluetooth_sapphire::LeaseProvider& wake_lease_provider)
+    : SignalingChannel(std::move(chan), role, dispatcher, wake_lease_provider) {
   set_mtu(kDefaultMTU);
 
   // Add default handler for incoming Echo Request commands.
diff --git a/pw_bluetooth_sapphire/host/l2cap/bredr_signaling_channel_test.cc b/pw_bluetooth_sapphire/host/l2cap/bredr_signaling_channel_test.cc
index 3273432..dca64b2 100644
--- a/pw_bluetooth_sapphire/host/l2cap/bredr_signaling_channel_test.cc
+++ b/pw_bluetooth_sapphire/host/l2cap/bredr_signaling_channel_test.cc
@@ -14,6 +14,7 @@
 
 #include "pw_bluetooth_sapphire/internal/host/l2cap/bredr_signaling_channel.h"
 
+#include "pw_bluetooth_sapphire/fake_lease_provider.h"
 #include "pw_bluetooth_sapphire/internal/host/l2cap/mock_channel_test.h"
 #include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h"
 
@@ -41,7 +42,7 @@
 
     fake_chan_ = CreateFakeChannel(options);
     sig_ = std::make_unique<BrEdrSignalingChannel>(
-        fake_chan_->GetWeakPtr(), kDeviceRole, dispatcher());
+        fake_chan_->GetWeakPtr(), kDeviceRole, dispatcher(), lease_provider_);
   }
 
   void TearDown() override {
@@ -52,6 +53,7 @@
   BrEdrSignalingChannel* sig() const { return sig_.get(); }
 
  private:
+  pw::bluetooth_sapphire::testing::FakeLeaseProvider lease_provider_;
   testing::FakeChannel::WeakPtr fake_chan_;
   std::unique_ptr<BrEdrSignalingChannel> sig_;
 };
diff --git a/pw_bluetooth_sapphire/host/l2cap/channel_manager_test.cc b/pw_bluetooth_sapphire/host/l2cap/channel_manager_test.cc
index 2335a07..6188b28 100644
--- a/pw_bluetooth_sapphire/host/l2cap/channel_manager_test.cc
+++ b/pw_bluetooth_sapphire/host/l2cap/channel_manager_test.cc
@@ -759,6 +759,10 @@
         cmd_ids.extended_features_id,
         kTestHandle1,
         kExtendedFeaturesBitEnhancedRetransmission));
+    ReceiveAclDataPacket(testing::AclFixedChannelsSupportedInfoRsp(
+        cmd_ids.fixed_channels_supported_id,
+        kTestHandle1,
+        kFixedChannelsSupportedBitSignaling));
     EXPECT_TRUE(chanmgr()->RegisterService(
         kTestPsm, chan_params, std::move(channel_cb)));
 
diff --git a/pw_bluetooth_sapphire/host/l2cap/le_signaling_channel.cc b/pw_bluetooth_sapphire/host/l2cap/le_signaling_channel.cc
index 89db978..c6a48bd 100644
--- a/pw_bluetooth_sapphire/host/l2cap/le_signaling_channel.cc
+++ b/pw_bluetooth_sapphire/host/l2cap/le_signaling_channel.cc
@@ -25,8 +25,9 @@
 LESignalingChannel::LESignalingChannel(
     Channel::WeakPtr chan,
     pw::bluetooth::emboss::ConnectionRole role,
-    pw::async::Dispatcher& dispatcher)
-    : SignalingChannel(std::move(chan), role, dispatcher) {
+    pw::async::Dispatcher& dispatcher,
+    pw::bluetooth_sapphire::LeaseProvider& wake_lease_provider)
+    : SignalingChannel(std::move(chan), role, dispatcher, wake_lease_provider) {
   set_mtu(kMinLEMTU);
 }
 
diff --git a/pw_bluetooth_sapphire/host/l2cap/le_signaling_channel_test.cc b/pw_bluetooth_sapphire/host/l2cap/le_signaling_channel_test.cc
index a36360d..520b02c 100644
--- a/pw_bluetooth_sapphire/host/l2cap/le_signaling_channel_test.cc
+++ b/pw_bluetooth_sapphire/host/l2cap/le_signaling_channel_test.cc
@@ -16,6 +16,7 @@
 
 #include <pw_bytes/endian.h>
 
+#include "pw_bluetooth_sapphire/fake_lease_provider.h"
 #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel_test.h"
 #include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h"
 
@@ -39,7 +40,7 @@
 
     fake_sig_chan_ = CreateFakeChannel(options);
     sig_ = std::make_unique<LESignalingChannel>(
-        fake_sig_chan_->GetWeakPtr(), Role, dispatcher());
+        fake_sig_chan_->GetWeakPtr(), Role, dispatcher(), lease_provider_);
   }
 
   void TearDown() override { sig_ = nullptr; }
@@ -47,6 +48,7 @@
   LESignalingChannel* sig() const { return sig_.get(); }
 
  private:
+  pw::bluetooth_sapphire::testing::FakeLeaseProvider lease_provider_;
   std::unique_ptr<testing::FakeChannel> fake_sig_chan_;
   std::unique_ptr<LESignalingChannel> sig_;
 
diff --git a/pw_bluetooth_sapphire/host/l2cap/logical_link.cc b/pw_bluetooth_sapphire/host/l2cap/logical_link.cc
index 9c89b25..e230022 100644
--- a/pw_bluetooth_sapphire/host/l2cap/logical_link.cc
+++ b/pw_bluetooth_sapphire/host/l2cap/logical_link.cc
@@ -127,7 +127,10 @@
   // Set up the signaling channel and dynamic channels.
   if (type_ == bt::LinkType::kLE) {
     signaling_channel_ = std::make_unique<LESignalingChannel>(
-        OpenFixedChannel(kLESignalingChannelId), role_, pw_dispatcher_);
+        OpenFixedChannel(kLESignalingChannelId),
+        role_,
+        pw_dispatcher_,
+        wake_lease_provider_);
     dynamic_registry_ = std::make_unique<LeDynamicChannelRegistry>(
         signaling_channel_.get(),
         fit::bind_member<&LogicalLink::OnChannelDisconnectRequest>(this),
@@ -138,7 +141,10 @@
     ServeFlowControlCreditInd();
   } else {
     signaling_channel_ = std::make_unique<BrEdrSignalingChannel>(
-        OpenFixedChannel(kSignalingChannelId), role_, pw_dispatcher_);
+        OpenFixedChannel(kSignalingChannelId),
+        role_,
+        pw_dispatcher_,
+        wake_lease_provider_);
     dynamic_registry_ = std::make_unique<BrEdrDynamicChannelRegistry>(
         signaling_channel_.get(),
         fit::bind_member<&LogicalLink::OnChannelDisconnectRequest>(this),
diff --git a/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/bredr_signaling_channel.h b/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/bredr_signaling_channel.h
index 9130999..03a977e 100644
--- a/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/bredr_signaling_channel.h
+++ b/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/bredr_signaling_channel.h
@@ -24,9 +24,11 @@
 // the L2CAP thread in production.
 class BrEdrSignalingChannel final : public SignalingChannel {
  public:
-  BrEdrSignalingChannel(Channel::WeakPtr chan,
-                        pw::bluetooth::emboss::ConnectionRole role,
-                        pw::async::Dispatcher& dispatcher);
+  BrEdrSignalingChannel(
+      Channel::WeakPtr chan,
+      pw::bluetooth::emboss::ConnectionRole role,
+      pw::async::Dispatcher& dispatcher,
+      pw::bluetooth_sapphire::LeaseProvider& wake_lease_provider);
   ~BrEdrSignalingChannel() override = default;
 
   // Test the link using an Echo Request command that can have an arbitrary
diff --git a/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/le_signaling_channel.h b/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/le_signaling_channel.h
index 1f3dbec..a63e78b 100644
--- a/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/le_signaling_channel.h
+++ b/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/le_signaling_channel.h
@@ -22,9 +22,11 @@
 // Implements the L2CAP LE signaling fixed channel.
 class LESignalingChannel final : public SignalingChannel {
  public:
-  LESignalingChannel(Channel::WeakPtr chan,
-                     pw::bluetooth::emboss::ConnectionRole role,
-                     pw::async::Dispatcher& dispatcher);
+  LESignalingChannel(
+      Channel::WeakPtr chan,
+      pw::bluetooth::emboss::ConnectionRole role,
+      pw::async::Dispatcher& dispatcher,
+      pw::bluetooth_sapphire::LeaseProvider& wake_lease_provider);
   ~LESignalingChannel() override = default;
 
  private:
diff --git a/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/signaling_channel.h b/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/signaling_channel.h
index 8b40a50..0f9da56 100644
--- a/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/signaling_channel.h
+++ b/pw_bluetooth_sapphire/host/l2cap/public/pw_bluetooth_sapphire/internal/host/l2cap/signaling_channel.h
@@ -22,6 +22,7 @@
 #include "pw_bluetooth_sapphire/internal/host/hci/connection.h"
 #include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h"
 #include "pw_bluetooth_sapphire/internal/host/l2cap/scoped_channel.h"
+#include "pw_bluetooth_sapphire/lease.h"
 
 namespace bt::l2cap {
 
@@ -119,7 +120,8 @@
  public:
   SignalingChannel(Channel::WeakPtr chan,
                    pw::bluetooth::emboss::ConnectionRole role,
-                   pw::async::Dispatcher& dispatcher);
+                   pw::async::Dispatcher& dispatcher,
+                   pw::bluetooth_sapphire::LeaseProvider& wake_lease_provider);
   ~SignalingChannel() override = default;
 
   // SignalingChannelInterface overrides
@@ -259,13 +261,15 @@
     PendingCommand(const ByteBuffer& request_packet,
                    CommandCode response_command_code,
                    ResponseHandler response_handler_cb,
-                   pw::async::Dispatcher& dispatcher)
+                   pw::async::Dispatcher& dispatcher,
+                   pw::bluetooth_sapphire::Lease wake_lease_in)
         : response_code(response_command_code),
           response_handler(std::move(response_handler_cb)),
           command_packet(std::make_unique<DynamicByteBuffer>(request_packet)),
           transmit_count(1u),
           timer_duration(0u),
-          response_timeout_task(dispatcher) {}
+          response_timeout_task(dispatcher),
+          wake_lease(std::move(wake_lease_in)) {}
     CommandCode response_code;
     ResponseHandler response_handler;
 
@@ -281,6 +285,9 @@
 
     // Automatically canceled by destruction if the response is received.
     SmartTask response_timeout_task;
+
+    // Keep the system awake while this command is pending.
+    pw::bluetooth_sapphire::Lease wake_lease;
   };
 
   // Retransmit the request corresponding to |pending_command| and reset the RTX
@@ -289,6 +296,8 @@
 
   pw::async::Dispatcher& pw_dispatcher_;
 
+  pw::bluetooth_sapphire::LeaseProvider& wake_lease_provider_;
+
   bool is_open_;
   l2cap::ScopedChannel chan_;
   pw::bluetooth::emboss::ConnectionRole role_;
diff --git a/pw_bluetooth_sapphire/host/l2cap/signaling_channel.cc b/pw_bluetooth_sapphire/host/l2cap/signaling_channel.cc
index 89bee74..854c69f 100644
--- a/pw_bluetooth_sapphire/host/l2cap/signaling_channel.cc
+++ b/pw_bluetooth_sapphire/host/l2cap/signaling_channel.cc
@@ -24,10 +24,13 @@
 
 namespace bt::l2cap::internal {
 
-SignalingChannel::SignalingChannel(Channel::WeakPtr chan,
-                                   pw::bluetooth::emboss::ConnectionRole role,
-                                   pw::async::Dispatcher& dispatcher)
+SignalingChannel::SignalingChannel(
+    Channel::WeakPtr chan,
+    pw::bluetooth::emboss::ConnectionRole role,
+    pw::async::Dispatcher& dispatcher,
+    pw::bluetooth_sapphire::LeaseProvider& wake_lease_provider)
     : pw_dispatcher_(dispatcher),
+      wake_lease_provider_(wake_lease_provider),
       is_open_(true),
       chan_(std::move(chan)),
       role_(role),
@@ -100,8 +103,17 @@
                                        ResponseHandler cb) {
   PW_CHECK(IsSupportedResponse(response_command_code));
 
-  const auto [iter, inserted] = pending_commands_.try_emplace(
-      id, request_packet, response_command_code, std::move(cb), pw_dispatcher_);
+  pw::bluetooth_sapphire::Lease wake_lease =
+      PW_SAPPHIRE_ACQUIRE_LEASE(wake_lease_provider_, "SignalingChannel")
+          .value_or(pw::bluetooth_sapphire::Lease());
+
+  const auto [iter, inserted] =
+      pending_commands_.try_emplace(id,
+                                    request_packet,
+                                    response_command_code,
+                                    std::move(cb),
+                                    pw_dispatcher_,
+                                    std::move(wake_lease));
   PW_CHECK(inserted);
 
   // Start the RTX timer per Core Spec v5.0, Volume 3, Part A, Sec 6.2.1 which
diff --git a/pw_bluetooth_sapphire/host/l2cap/signaling_channel_test.cc b/pw_bluetooth_sapphire/host/l2cap/signaling_channel_test.cc
index fbe5587..7219267 100644
--- a/pw_bluetooth_sapphire/host/l2cap/signaling_channel_test.cc
+++ b/pw_bluetooth_sapphire/host/l2cap/signaling_channel_test.cc
@@ -19,6 +19,7 @@
 
 #include <chrono>
 
+#include "pw_bluetooth_sapphire/fake_lease_provider.h"
 #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel_test.h"
 #include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h"
 
@@ -39,11 +40,14 @@
 
 class TestSignalingChannel : public SignalingChannel {
  public:
-  explicit TestSignalingChannel(Channel::WeakPtr chan,
-                                pw::async::Dispatcher& dispatcher)
+  explicit TestSignalingChannel(
+      Channel::WeakPtr chan,
+      pw::async::Dispatcher& dispatcher,
+      pw::bluetooth_sapphire::LeaseProvider& lease_provider)
       : SignalingChannel(std::move(chan),
                          pw::bluetooth::emboss::ConnectionRole::CENTRAL,
-                         dispatcher) {
+                         dispatcher,
+                         lease_provider) {
     set_mtu(kTestMTU);
   }
   ~TestSignalingChannel() override = default;
@@ -105,7 +109,7 @@
 
     fake_channel_inst_ = CreateFakeChannel(options);
     sig_ = std::make_unique<TestSignalingChannel>(
-        fake_channel_inst_->GetWeakPtr(), dispatcher());
+        fake_channel_inst_->GetWeakPtr(), dispatcher(), lease_provider_);
   }
 
   void TearDown() override {
@@ -119,7 +123,12 @@
 
   void DestroySig() { sig_ = nullptr; }
 
+  pw::bluetooth_sapphire::testing::FakeLeaseProvider& lease_provider() {
+    return lease_provider_;
+  }
+
  private:
+  pw::bluetooth_sapphire::testing::FakeLeaseProvider lease_provider_;
   std::unique_ptr<TestSignalingChannel> sig_;
 
   // Own the fake channel so that its lifetime can span beyond that of |sig_|.
@@ -685,6 +694,7 @@
 // for the following response.
 TEST_F(SignalingChannelTest,
        ExpectAdditionalResponseExtendsRtxTimeoutToErtxTimeout) {
+  EXPECT_EQ(lease_provider().lease_count(), 0u);
   bool tx_success = false;
   fake_chan()->SetSendCallback([&tx_success](auto) { tx_success = true; },
                                dispatcher());
@@ -692,8 +702,11 @@
   const StaticByteBuffer req_data{'h', 'e', 'l', 'l', 'o'};
   int rx_cb_calls = 0;
   EXPECT_TRUE(sig()->SendRequest(
-      kEchoRequest, req_data, [&rx_cb_calls](Status status, const ByteBuffer&) {
+      kEchoRequest,
+      req_data,
+      [this, &rx_cb_calls](Status status, const ByteBuffer&) {
         rx_cb_calls++;
+        EXPECT_GT(lease_provider().lease_count(), 0u);
         if (rx_cb_calls <= 2) {
           EXPECT_EQ(Status::kSuccess, status);
         } else {
@@ -703,9 +716,11 @@
             kExpectAdditionalResponse;
       }));
 
+  EXPECT_GT(lease_provider().lease_count(), 0u);
   RunUntilIdle();
   EXPECT_TRUE(tx_success);
   EXPECT_EQ(0, rx_cb_calls);
+  EXPECT_GT(lease_provider().lease_count(), 0u);
 
   const StaticByteBuffer echo_rsp(
       // Echo response with no payload.
@@ -715,12 +730,14 @@
       0x00);
   fake_chan()->Receive(echo_rsp);
   EXPECT_EQ(1, rx_cb_calls);
+  EXPECT_GT(lease_provider().lease_count(), 0u);
 
   // The handler expects more responses so the RTX timer shouldn't have expired.
   RunFor(kSignalingChannelResponseTimeout);
 
   fake_chan()->Receive(echo_rsp);
   EXPECT_EQ(2, rx_cb_calls);
+  EXPECT_GT(lease_provider().lease_count(), 0u);
 
   // The second response should have reset the ERTX timer, so it shouldn't fire
   // yet.
@@ -731,6 +748,7 @@
   // kTimeOut "response."
   RunFor(std::chrono::seconds(1));
   EXPECT_EQ(3, rx_cb_calls);
+  EXPECT_EQ(lease_provider().lease_count(), 0u);
 }
 
 TEST_F(SignalingChannelTest, RegisterRequestResponder) {
@@ -870,10 +888,71 @@
   fake_chan()->SetSendCallback(std::move(send_cb), dispatcher());
 
   sig()->SendCommandWithoutResponse(kLEFlowControlCredit, payload);
+  EXPECT_EQ(lease_provider().lease_count(), 0u);
 
   RunUntilIdle();
   EXPECT_TRUE(cb_called);
 }
 
+TEST_F(SignalingChannelTest, SendMultipleCommandsSimultaneously) {
+  EXPECT_EQ(lease_provider().lease_count(), 0u);
+  fake_chan()->SetSendCallback([](auto) {}, dispatcher());
+
+  const StaticByteBuffer req_data{'h', 'e', 'l', 'l', 'o'};
+  int rx_cb_calls_0 = 0;
+  EXPECT_TRUE(sig()->SendRequest(
+      kEchoRequest,
+      req_data,
+      [this, &rx_cb_calls_0](Status status, const ByteBuffer&) {
+        rx_cb_calls_0++;
+        EXPECT_EQ(Status::kSuccess, status);
+        EXPECT_GT(lease_provider().lease_count(), 0u);
+        return SignalingChannel::ResponseHandlerAction::
+            kCompleteOutboundTransaction;
+      }));
+
+  EXPECT_GT(lease_provider().lease_count(), 0u);
+  RunUntilIdle();
+  EXPECT_EQ(0, rx_cb_calls_0);
+  EXPECT_GT(lease_provider().lease_count(), 0u);
+
+  int rx_cb_calls_1 = 0;
+  EXPECT_TRUE(sig()->SendRequest(
+      kEchoRequest,
+      req_data,
+      [this, &rx_cb_calls_1](Status status, const ByteBuffer&) {
+        rx_cb_calls_1++;
+        EXPECT_EQ(Status::kSuccess, status);
+        EXPECT_GT(lease_provider().lease_count(), 0u);
+        return SignalingChannel::ResponseHandlerAction::
+            kCompleteOutboundTransaction;
+      }));
+
+  EXPECT_GT(lease_provider().lease_count(), 0u);
+  RunUntilIdle();
+  EXPECT_EQ(0, rx_cb_calls_1);
+  EXPECT_GT(lease_provider().lease_count(), 0u);
+
+  const StaticByteBuffer echo_rsp_0(
+      // Echo response with no payload.
+      0x09,
+      0x01,  // ID (1)
+      0x00,
+      0x00);
+  fake_chan()->Receive(echo_rsp_0);
+  EXPECT_EQ(1, rx_cb_calls_0);
+  EXPECT_GT(lease_provider().lease_count(), 0u);
+
+  const StaticByteBuffer echo_rsp_1(
+      // Echo response with no payload.
+      0x09,
+      0x02,  // ID (2)
+      0x00,
+      0x00);
+  fake_chan()->Receive(echo_rsp_1);
+  EXPECT_EQ(1, rx_cb_calls_1);
+  EXPECT_EQ(lease_provider().lease_count(), 0u);
+}
+
 }  // namespace
 }  // namespace bt::l2cap::internal