Fix invariant violation if we get a message without piggyback ack while expecting an ack. (#23282)

* Fix invariant violation if we get a message without piggyback ack while expecting an ack.

Such messages are not allowed per spec, so we should just ignore them.

Fixes https://github.com/project-chip/connectedhomeip/issues/22854

* Fix tests.

* Address review comment.
diff --git a/src/app/tests/TestCommandInteraction.cpp b/src/app/tests/TestCommandInteraction.cpp
index fde62b5..2ad2bc7 100644
--- a/src/app/tests/TestCommandInteraction.cpp
+++ b/src/app/tests/TestCommandInteraction.cpp
@@ -714,6 +714,40 @@
     exchange->Close();
 }
 
+/**
+ * Helper macro we can use to pretend we got a reply from the server in cases
+ * when the reply was actually dropped due to us not wanting the client's state
+ * machine to advance.
+ *
+ * When this macro is used, the client has sent a message and is waiting for an
+ * ack+response, and the server has sent a response that got dropped and is
+ * waiting for an ack (and maybe a response).
+ *
+ * What this macro then needs to do is:
+ *
+ * 1. Pretend that the client got an ack (and clear out the corresponding ack
+ *    state).
+ * 2. Pretend that the client got a message from the server, with the id of the
+ *    message that was dropped, which requires an ack, so the client will send
+ *    that ack in its next message.
+ *
+ * This is a macro so we get useful line numbers on assertion failures
+ */
+#define PretendWeGotReplyFromServer(aSuite, aContext, aClientExchange)                                                             \
+    {                                                                                                                              \
+        Messaging::ReliableMessageMgr * localRm    = (aContext).GetExchangeManager().GetReliableMessageMgr();                      \
+        Messaging::ExchangeContext * localExchange = aClientExchange;                                                              \
+        NL_TEST_ASSERT(aSuite, localRm->TestGetCountRetransTable() == 2);                                                          \
+                                                                                                                                   \
+        localRm->ClearRetransTable(localExchange);                                                                                 \
+        NL_TEST_ASSERT(aSuite, localRm->TestGetCountRetransTable() == 1);                                                          \
+                                                                                                                                   \
+        localRm->EnumerateRetransTable([localExchange](auto * entry) {                                                             \
+            localExchange->SetPendingPeerAckMessageCounter(entry->retainedBuf.GetMessageCounter());                                \
+            return Loop::Break;                                                                                                    \
+        });                                                                                                                        \
+    }
+
 // Command Sender sends invoke request, command handler drops invoke response, then test injects status response message with
 // busy to client, client sends out a status response with invalid action.
 void TestCommandInteraction::TestCommandInvalidMessage1(nlTestSuite * apSuite, void * apContext)
@@ -757,8 +791,11 @@
     Test::MessageCapturer messageLog(ctx);
     messageLog.mCaptureStandaloneAcks = false;
 
-    Messaging::ReliableMessageMgr * rm = ctx.GetExchangeManager().GetReliableMessageMgr();
-    rm->ClearRetransTable(commandSender.mExchangeCtx.Get());
+    // Since we are dropping packets, things are not getting acked.  Set up our
+    // MRP state to look like what it would have looked like if the packet had
+    // not gotten dropped.
+    PretendWeGotReplyFromServer(apSuite, ctx, commandSender.mExchangeCtx.Get());
+
     ctx.GetLoopback().mSentMessageCount                 = 0;
     ctx.GetLoopback().mNumMessagesToDrop                = 0;
     ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
@@ -823,9 +860,13 @@
     payloadHeader.SetExchangeID(0);
     payloadHeader.SetMessageType(chip::Protocols::InteractionModel::MsgType::ReportData);
     Test::MessageCapturer messageLog(ctx);
-    messageLog.mCaptureStandaloneAcks  = false;
-    Messaging::ReliableMessageMgr * rm = ctx.GetExchangeManager().GetReliableMessageMgr();
-    rm->ClearRetransTable(commandSender.mExchangeCtx.Get());
+    messageLog.mCaptureStandaloneAcks = false;
+
+    // Since we are dropping packets, things are not getting acked.  Set up our
+    // MRP state to look like what it would have looked like if the packet had
+    // not gotten dropped.
+    PretendWeGotReplyFromServer(apSuite, ctx, commandSender.mExchangeCtx.Get());
+
     ctx.GetLoopback().mSentMessageCount                 = 0;
     ctx.GetLoopback().mNumMessagesToDrop                = 0;
     ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
@@ -892,8 +933,11 @@
     Test::MessageCapturer messageLog(ctx);
     messageLog.mCaptureStandaloneAcks = false;
 
-    Messaging::ReliableMessageMgr * rm = ctx.GetExchangeManager().GetReliableMessageMgr();
-    rm->ClearRetransTable(commandSender.mExchangeCtx.Get());
+    // Since we are dropping packets, things are not getting acked.  Set up our
+    // MRP state to look like what it would have looked like if the packet had
+    // not gotten dropped.
+    PretendWeGotReplyFromServer(apSuite, ctx, commandSender.mExchangeCtx.Get());
+
     ctx.GetLoopback().mSentMessageCount                 = 0;
     ctx.GetLoopback().mNumMessagesToDrop                = 0;
     ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
@@ -960,8 +1004,11 @@
     Test::MessageCapturer messageLog(ctx);
     messageLog.mCaptureStandaloneAcks = false;
 
-    Messaging::ReliableMessageMgr * rm = ctx.GetExchangeManager().GetReliableMessageMgr();
-    rm->ClearRetransTable(commandSender.mExchangeCtx.Get());
+    // Since we are dropping packets, things are not getting acked.  Set up our
+    // MRP state to look like what it would have looked like if the packet had
+    // not gotten dropped.
+    PretendWeGotReplyFromServer(apSuite, ctx, commandSender.mExchangeCtx.Get());
+
     ctx.GetLoopback().mSentMessageCount                 = 0;
     ctx.GetLoopback().mNumMessagesToDrop                = 0;
     ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
diff --git a/src/app/tests/TestReadInteraction.cpp b/src/app/tests/TestReadInteraction.cpp
index fc9e379..9a27ba7 100644
--- a/src/app/tests/TestReadInteraction.cpp
+++ b/src/app/tests/TestReadInteraction.cpp
@@ -2926,6 +2926,40 @@
 
 } // anonymous namespace
 
+/**
+ * Helper macro we can use to pretend we got a reply from the server in cases
+ * when the reply was actually dropped due to us not wanting the client's state
+ * machine to advance.
+ *
+ * When this macro is used, the client has sent a message and is waiting for an
+ * ack+response, and the server has sent a response that got dropped and is
+ * waiting for an ack (and maybe a response).
+ *
+ * What this macro then needs to do is:
+ *
+ * 1. Pretend that the client got an ack (and clear out the corresponding ack
+ *    state).
+ * 2. Pretend that the client got a message from the server, with the id of the
+ *    message that was dropped, which requires an ack, so the client will send
+ *    that ack in its next message.
+ *
+ * This is a macro so we get useful line numbers on assertion failures
+ */
+#define PretendWeGotReplyFromServer(aSuite, aContext, aClientExchange)                                                             \
+    {                                                                                                                              \
+        Messaging::ReliableMessageMgr * localRm    = (aContext).GetExchangeManager().GetReliableMessageMgr();                      \
+        Messaging::ExchangeContext * localExchange = aClientExchange;                                                              \
+        NL_TEST_ASSERT(aSuite, localRm->TestGetCountRetransTable() == 2);                                                          \
+                                                                                                                                   \
+        localRm->ClearRetransTable(localExchange);                                                                                 \
+        NL_TEST_ASSERT(aSuite, localRm->TestGetCountRetransTable() == 1);                                                          \
+                                                                                                                                   \
+        localRm->EnumerateRetransTable([localExchange](auto * entry) {                                                             \
+            localExchange->SetPendingPeerAckMessageCounter(entry->retainedBuf.GetMessageCounter());                                \
+            return Loop::Break;                                                                                                    \
+        });                                                                                                                        \
+    }
+
 // Read Client sends the read request, Read Handler drops the response, then test injects unknown status reponse message for Read
 // Client.
 void TestReadInteraction::TestReadClientReceiveInvalidMessage(nlTestSuite * apSuite, void * apContext)
@@ -2957,8 +2991,7 @@
     readPrepareParams.mAttributePathParamsListSize = 2;
 
     {
-        app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), delegate,
-                                   chip::app::ReadClient::InteractionType::Read);
+        app::ReadClient readClient(engine, &ctx.GetExchangeManager(), delegate, chip::app::ReadClient::InteractionType::Read);
 
         ctx.GetLoopback().mSentMessageCount                 = 0;
         ctx.GetLoopback().mNumMessagesToDrop                = 1;
@@ -2968,6 +3001,9 @@
         NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
         ctx.DrainAndServiceIO();
 
+        NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mSentMessageCount == 2);
+        NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mDroppedMessageCount == 1);
+
         System::PacketBufferHandle msgBuf = System::PacketBufferHandle::New(kMaxSecureSduLengthBytes);
         NL_TEST_ASSERT(apSuite, !msgBuf.IsNull());
         System::PacketBufferTLVWriter writer;
@@ -2983,9 +3019,11 @@
         Test::MessageCapturer messageLog(ctx);
         messageLog.mCaptureStandaloneAcks = false;
 
-        rm->ClearRetransTable(readClient.mExchange.Get());
-        NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mSentMessageCount == 2);
-        NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mDroppedMessageCount == 1);
+        // Since we are dropping packets, things are not getting acked.  Set up
+        // our MRP state to look like what it would have looked like if the
+        // packet had not gotten dropped.
+        PretendWeGotReplyFromServer(apSuite, ctx, readClient.mExchange.Get());
+
         ctx.GetLoopback().mSentMessageCount                 = 0;
         ctx.GetLoopback().mNumMessagesToDrop                = 0;
         ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
@@ -3065,13 +3103,16 @@
         payloadHeader.SetExchangeID(0);
         payloadHeader.SetMessageType(chip::Protocols::InteractionModel::MsgType::StatusResponse);
 
-        rm->ClearRetransTable(readClient.mExchange.Get());
+        // Since we are dropping packets, things are not getting acked.  Set up
+        // our MRP state to look like what it would have looked like if the
+        // packet had not gotten dropped.
+        PretendWeGotReplyFromServer(apSuite, ctx, readClient.mExchange.Get());
+
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mSentMessageCount == 2);
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mDroppedMessageCount == 1);
-
         NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadHandlers() == 1);
         NL_TEST_ASSERT(apSuite, engine->ActiveHandlerAt(0) != nullptr);
-        rm->ClearRetransTable(engine->ActiveHandlerAt(0)->mExchangeCtx.Get());
+
         ctx.GetLoopback().mSentMessageCount                 = 0;
         ctx.GetLoopback().mNumMessagesToDrop                = 0;
         ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
@@ -3155,13 +3196,16 @@
         payloadHeader.SetExchangeID(0);
         payloadHeader.SetMessageType(chip::Protocols::InteractionModel::MsgType::StatusResponse);
 
-        rm->ClearRetransTable(readClient.mExchange.Get());
+        // Since we are dropping packets, things are not getting acked.  Set up
+        // our MRP state to look like what it would have looked like if the
+        // packet had not gotten dropped.
+        PretendWeGotReplyFromServer(apSuite, ctx, readClient.mExchange.Get());
+
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mSentMessageCount == 2);
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mDroppedMessageCount == 1);
-
         NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadHandlers() == 1);
         NL_TEST_ASSERT(apSuite, engine->ActiveHandlerAt(0) != nullptr);
-        rm->ClearRetransTable(engine->ActiveHandlerAt(0)->mExchangeCtx.Get());
+
         ctx.GetLoopback().mSentMessageCount                 = 0;
         ctx.GetLoopback().mNumMessagesToDrop                = 0;
         ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
@@ -3243,12 +3287,15 @@
         payloadHeader.SetExchangeID(0);
         payloadHeader.SetMessageType(chip::Protocols::InteractionModel::MsgType::ReportData);
 
-        rm->ClearRetransTable(readClient.mExchange.Get());
+        // Since we are dropping packets, things are not getting acked.  Set up
+        // our MRP state to look like what it would have looked like if the
+        // packet had not gotten dropped.
+        PretendWeGotReplyFromServer(apSuite, ctx, readClient.mExchange.Get());
+
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mSentMessageCount == 2);
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mDroppedMessageCount == 1);
         NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadHandlers() == 1);
         NL_TEST_ASSERT(apSuite, engine->ActiveHandlerAt(0) != nullptr);
-        rm->ClearRetransTable(engine->ActiveHandlerAt(0)->mExchangeCtx.Get());
 
         ctx.GetLoopback().mSentMessageCount                 = 0;
         ctx.GetLoopback().mNumMessagesToDrop                = 0;
@@ -3407,12 +3454,15 @@
         payloadHeader.SetExchangeID(0);
         payloadHeader.SetMessageType(chip::Protocols::InteractionModel::MsgType::SubscribeResponse);
 
-        rm->ClearRetransTable(readClient.mExchange.Get());
+        // Since we are dropping packets, things are not getting acked.  Set up
+        // our MRP state to look like what it would have looked like if the
+        // packet had not gotten dropped.
+        PretendWeGotReplyFromServer(apSuite, ctx, readClient.mExchange.Get());
+
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mSentMessageCount == 4);
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mDroppedMessageCount == 1);
         NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadHandlers() == 1);
         NL_TEST_ASSERT(apSuite, engine->ActiveHandlerAt(0) != nullptr);
-        rm->ClearRetransTable(engine->ActiveHandlerAt(0)->mExchangeCtx.Get());
 
         ctx.GetLoopback().mSentMessageCount                 = 0;
         ctx.GetLoopback().mNumMessagesToDrop                = 0;
@@ -3574,7 +3624,11 @@
 
         NL_TEST_ASSERT(apSuite, writer.Finalize(&msgBuf) == CHIP_NO_ERROR);
 
-        rm->ClearRetransTable(readClient.mExchange.Get());
+        // Since we are dropping packets, things are not getting acked.  Set up
+        // our MRP state to look like what it would have looked like if the
+        // packet had not gotten dropped.
+        PretendWeGotReplyFromServer(apSuite, ctx, readClient.mExchange.Get());
+
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mSentMessageCount == 4);
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mDroppedMessageCount == 1);
         NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadHandlers() == 1);
@@ -3780,17 +3834,21 @@
         NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
         ctx.DrainAndServiceIO();
 
+        // Since we are dropping packets, things are not getting acked.  Set up
+        // our MRP state to look like what it would have looked like if the
+        // packet had not gotten dropped.
+        PretendWeGotReplyFromServer(apSuite, ctx, readClient.mExchange.Get());
+
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mSentMessageCount == 2);
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mDroppedMessageCount == 1);
-
-        ctx.GetLoopback().mSentMessageCount = 0;
-        rm->ClearRetransTable(readClient.mExchange.Get());
         NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadHandlers() == 1);
         NL_TEST_ASSERT(apSuite, engine->ActiveHandlerAt(0) != nullptr);
+
+        ctx.GetLoopback().mSentMessageCount = 0;
+
         // Server sends out status report, client should send status report along with Piggybacking ack, but we don't do that
-        // Instead, we send out unknown message to server,but server is still waiting for ack, so we need
-        // clear out retranstable
-        rm->ClearRetransTable(engine->ActiveHandlerAt(0)->mExchangeCtx.Get());
+        // Instead, we send out unknown message to server
+
         System::PacketBufferHandle msgBuf;
         ReadRequestMessage::Builder request;
         System::PacketBufferTLVWriter writer;
@@ -3853,13 +3911,17 @@
         NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
         ctx.DrainAndServiceIO();
 
+        // Since we are dropping packets, things are not getting acked.  Set up
+        // our MRP state to look like what it would have looked like if the
+        // packet had not gotten dropped.
+        PretendWeGotReplyFromServer(apSuite, ctx, readClient.mExchange.Get());
+
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mSentMessageCount == 2);
         NL_TEST_ASSERT(apSuite, ctx.GetLoopback().mDroppedMessageCount == 1);
         ctx.GetLoopback().mSentMessageCount = 0;
-        rm->ClearRetransTable(readClient.mExchange.Get());
+
         NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadHandlers() == 1);
         NL_TEST_ASSERT(apSuite, engine->ActiveHandlerAt(0) != nullptr);
-        rm->ClearRetransTable(engine->ActiveHandlerAt(0)->mExchangeCtx.Get());
 
         System::PacketBufferHandle msgBuf;
         StatusResponseMessage::Builder request;
diff --git a/src/app/tests/TestWriteInteraction.cpp b/src/app/tests/TestWriteInteraction.cpp
index 65f6e8b..6437b25 100644
--- a/src/app/tests/TestWriteInteraction.cpp
+++ b/src/app/tests/TestWriteInteraction.cpp
@@ -679,6 +679,40 @@
 
 #endif
 
+/**
+ * Helper macro we can use to pretend we got a reply from the server in cases
+ * when the reply was actually dropped due to us not wanting the client's state
+ * machine to advance.
+ *
+ * When this macro is used, the client has sent a message and is waiting for an
+ * ack+response, and the server has sent a response that got dropped and is
+ * waiting for an ack (and maybe a response).
+ *
+ * What this macro then needs to do is:
+ *
+ * 1. Pretend that the client got an ack (and clear out the corresponding ack
+ *    state).
+ * 2. Pretend that the client got a message from the server, with the id of the
+ *    message that was dropped, which requires an ack, so the client will send
+ *    that ack in its next message.
+ *
+ * This is a macro so we get useful line numbers on assertion failures
+ */
+#define PretendWeGotReplyFromServer(aSuite, aContext, aClientExchange)                                                             \
+    {                                                                                                                              \
+        Messaging::ReliableMessageMgr * localRm    = (aContext).GetExchangeManager().GetReliableMessageMgr();                      \
+        Messaging::ExchangeContext * localExchange = aClientExchange;                                                              \
+        NL_TEST_ASSERT(aSuite, localRm->TestGetCountRetransTable() == 2);                                                          \
+                                                                                                                                   \
+        localRm->ClearRetransTable(localExchange);                                                                                 \
+        NL_TEST_ASSERT(aSuite, localRm->TestGetCountRetransTable() == 1);                                                          \
+                                                                                                                                   \
+        localRm->EnumerateRetransTable([localExchange](auto * entry) {                                                             \
+            localExchange->SetPendingPeerAckMessageCounter(entry->retainedBuf.GetMessageCounter());                                \
+            return Loop::Break;                                                                                                    \
+        });                                                                                                                        \
+    }
+
 // Write Client sends a write request, receives an unexpected message type, sends a status response to that.
 void TestWriteInteraction::TestWriteInvalidMessage1(nlTestSuite * apSuite, void * apContext)
 {
@@ -724,7 +758,11 @@
     payloadHeader.SetExchangeID(0);
     payloadHeader.SetMessageType(chip::Protocols::InteractionModel::MsgType::ReportData);
 
-    rm->ClearRetransTable(writeClient.mExchangeCtx.Get());
+    // Since we are dropping packets, things are not getting acked.  Set up
+    // our MRP state to look like what it would have looked like if the
+    // packet had not gotten dropped.
+    PretendWeGotReplyFromServer(apSuite, ctx, writeClient.mExchangeCtx.Get());
+
     ctx.GetLoopback().mSentMessageCount                 = 0;
     ctx.GetLoopback().mNumMessagesToDrop                = 0;
     ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
@@ -791,7 +829,11 @@
     payloadHeader.SetExchangeID(0);
     payloadHeader.SetMessageType(chip::Protocols::InteractionModel::MsgType::WriteResponse);
 
-    rm->ClearRetransTable(writeClient.mExchangeCtx.Get());
+    // Since we are dropping packets, things are not getting acked.  Set up
+    // our MRP state to look like what it would have looked like if the
+    // packet had not gotten dropped.
+    PretendWeGotReplyFromServer(apSuite, ctx, writeClient.mExchangeCtx.Get());
+
     ctx.GetLoopback().mSentMessageCount                 = 0;
     ctx.GetLoopback().mNumMessagesToDrop                = 0;
     ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
@@ -857,7 +899,11 @@
     payloadHeader.SetExchangeID(0);
     payloadHeader.SetMessageType(chip::Protocols::InteractionModel::MsgType::StatusResponse);
 
-    rm->ClearRetransTable(writeClient.mExchangeCtx.Get());
+    // Since we are dropping packets, things are not getting acked.  Set up
+    // our MRP state to look like what it would have looked like if the
+    // packet had not gotten dropped.
+    PretendWeGotReplyFromServer(apSuite, ctx, writeClient.mExchangeCtx.Get());
+
     ctx.GetLoopback().mSentMessageCount                 = 0;
     ctx.GetLoopback().mNumMessagesToDrop                = 0;
     ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
@@ -925,7 +971,11 @@
     payloadHeader.SetExchangeID(0);
     payloadHeader.SetMessageType(chip::Protocols::InteractionModel::MsgType::StatusResponse);
 
-    rm->ClearRetransTable(writeClient.mExchangeCtx.Get());
+    // Since we are dropping packets, things are not getting acked.  Set up
+    // our MRP state to look like what it would have looked like if the
+    // packet had not gotten dropped.
+    PretendWeGotReplyFromServer(apSuite, ctx, writeClient.mExchangeCtx.Get());
+
     ctx.GetLoopback().mSentMessageCount                 = 0;
     ctx.GetLoopback().mNumMessagesToDrop                = 0;
     ctx.GetLoopback().mNumMessagesToAllowBeforeDropping = 0;
diff --git a/src/messaging/ExchangeContext.cpp b/src/messaging/ExchangeContext.cpp
index 9d3c8ec..080b73b 100644
--- a/src/messaging/ExchangeContext.cpp
+++ b/src/messaging/ExchangeContext.cpp
@@ -573,6 +573,18 @@
         return CHIP_NO_ERROR;
     }
 
+    if (IsMessageNotAcked())
+    {
+        // The only way we can get here is a spec violation on the other side:
+        // we sent a message that needs an ack, and the other side responded
+        // with a message that does not contain an ack for the message we sent.
+        // Just drop this message; if we delivered it to our delegate it might
+        // try to send another message-needing-an-ack in response, which would
+        // violate our internal invariants.
+        ChipLogError(ExchangeManager, "Dropping message without piggyback ack when we are waiting for an ack.");
+        return CHIP_ERROR_INCORRECT_STATE;
+    }
+
     // Since we got the response, cancel the response timer.
     CancelResponseTimer();
 
diff --git a/src/messaging/ReliableMessageContext.h b/src/messaging/ReliableMessageContext.h
index 886ed5c..57ec456 100644
--- a/src/messaging/ReliableMessageContext.h
+++ b/src/messaging/ReliableMessageContext.h
@@ -36,6 +36,11 @@
 #include <transport/raw/MessageHeader.h>
 
 namespace chip {
+namespace app {
+class TestCommandInteraction;
+class TestReadInteraction;
+class TestWriteInteraction;
+} // namespace app
 namespace Messaging {
 
 class ChipMessageInfo;
@@ -193,6 +198,9 @@
     friend class ReliableMessageMgr;
     friend class ExchangeContext;
     friend class ExchangeMessageDispatch;
+    friend class ::chip::app::TestCommandInteraction;
+    friend class ::chip::app::TestReadInteraction;
+    friend class ::chip::app::TestWriteInteraction;
 
     System::Clock::Timestamp mNextAckTime; // Next time for triggering Solo Ack
     uint32_t mPendingPeerAckMessageCounter;
diff --git a/src/messaging/ReliableMessageMgr.h b/src/messaging/ReliableMessageMgr.h
index c50ca12..e2fc1e2 100644
--- a/src/messaging/ReliableMessageMgr.h
+++ b/src/messaging/ReliableMessageMgr.h
@@ -192,6 +192,15 @@
 #if CHIP_CONFIG_TEST
     // Functions for testing
     int TestGetCountRetransTable();
+
+    // Enumerate the retransmission table.  Clearing an entry while enumerating
+    // that entry is allowed.  F must take a RetransTableEntry as an argument
+    // and return Loop::Continue or Loop::Break.
+    template <typename F>
+    void EnumerateRetransTable(F && functor)
+    {
+        mRetransTable.ForEachActiveObject(std::forward<F>(functor));
+    }
 #endif // CHIP_CONFIG_TEST
 
 private: