Define a IM/DM decoupling interface for interaction model engine to interact with the data model bits (#32914)

* Start adding a interaction model decoupling

* Restyle

* Also add invoke responder bits

* Add the first model definition, for each operation

* Add all files into the build definition

* Start adding the side effect support classes to the interaction model definitions

* Add actions and integrate them to the IM model

* Add iteration methods

* Restyle

* Add all files to the build definition

* Start defining a unit test to at least validate compilation. Will later validate that we get events emitted

* Fix a dependency

* Move EventLoggingDelegate to be part of "events" in app.

- Remove unneeded includes from this header
- add dependency to core (due to TLV)

* Prepare for testing ... does not compile however TODOs are starting to go away

* Fix SFINAE logic on eventing

* test that something is actually emitted for events

* Tests actually do something

* Some tests actually pass

* Restyle

* Minor style updates

* Rename EmitEvent to GenerateEvent

* Add description for paths

* Cleanup some example text ... we do not need an exhaustive usage list

* Restyle

* Fix spelling

* Renames based on code review: use UpperCamelCase

* Updated proposal for retries and paths

* Restyle

* one more comment

* Restyled by clang-format

* Fix typo in parameter type

* Some updates for code review

* Undo submodule update

* Fix logic error in buffer-too-small check for buffer data

* Fix return error code when send encounters an error

* Fix logic for mCompleted handling in the auto-complete handling

* More comments

* Comments

* Use AAI types for interaction model. I think they will need some splitting, however for now they seem to be a solid catch-all

* Update some naming to not use the a... syntax since those are odd

* Code review updates

* Added extra comment

* Enforce lifetime in invoke responses

* More comments

* More comments

* Restyle

* Added comment on replyasync with nullptr

* Update src/app/interaction-model/InvokeResponder.h

Co-authored-by: Terence Hampson <thampson@google.com>

* Update src/app/interaction-model/InvokeResponder.h

Co-authored-by: Terence Hampson <thampson@google.com>

* Update src/app/interaction-model/InvokeResponder.h

Co-authored-by: Terence Hampson <thampson@google.com>

* Update src/app/interaction-model/InvokeResponder.h

Co-authored-by: Terence Hampson <thampson@google.com>

* Some code review updates

* Some code review updates

* Make the subject descriptor optional

* use std::optional instead of chip optiona

* Restyle

---------

Co-authored-by: Restyled.io <commits@restyled.io>
Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Terence Hampson <thampson@google.com>
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 61292e1..d1dfcc7 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -55,6 +55,7 @@
   chip_test_group("tests") {
     deps = []
     tests = [
+      "${chip_root}/src/app/interaction-model/tests",
       "${chip_root}/src/access/tests",
       "${chip_root}/src/crypto/tests",
       "${chip_root}/src/inet/tests",
diff --git a/src/app/interaction-model/Actions.h b/src/app/interaction-model/Actions.h
new file mode 100644
index 0000000..62021fb
--- /dev/null
+++ b/src/app/interaction-model/Actions.h
@@ -0,0 +1,37 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <app/interaction-model/Events.h>
+#include <app/interaction-model/Paths.h>
+#include <app/interaction-model/RequestContext.h>
+
+namespace chip {
+namespace app {
+namespace InteractionModel {
+
+/// Data provided to data models in order to interface with the interaction model environment.
+struct InteractionModelActions
+{
+    Events * events;
+    Paths * paths;
+    RequestContext * requestContext;
+};
+
+} // namespace InteractionModel
+} // namespace app
+} // namespace chip
diff --git a/src/app/interaction-model/BUILD.gn b/src/app/interaction-model/BUILD.gn
new file mode 100644
index 0000000..c91c2ae
--- /dev/null
+++ b/src/app/interaction-model/BUILD.gn
@@ -0,0 +1,41 @@
+# Copyright (c) 2024 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import("//build_overrides/chip.gni")
+
+source_set("interaction-model") {
+  sources = [
+    "Actions.h",
+    "Events.h",
+    "InvokeResponder.h",
+    "IterationTypes.h",
+    "Model.h",
+    "OperationTypes.h",
+    "Paths.h",
+    "RequestContext.h",
+  ]
+
+  public_deps = [
+    "${chip_root}/src/access:types",
+    "${chip_root}/src/app:attribute-access",
+    "${chip_root}/src/app:events",
+    "${chip_root}/src/app:paths",
+    "${chip_root}/src/app/MessageDef",
+    "${chip_root}/src/app/data-model",
+    "${chip_root}/src/lib/core",
+    "${chip_root}/src/lib/core:error",
+    "${chip_root}/src/lib/core:types",
+    "${chip_root}/src/lib/support",
+    "${chip_root}/src/messaging",
+  ]
+}
diff --git a/src/app/interaction-model/Events.h b/src/app/interaction-model/Events.h
new file mode 100644
index 0000000..255a55e
--- /dev/null
+++ b/src/app/interaction-model/Events.h
@@ -0,0 +1,130 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <app/EventLoggingDelegate.h>
+#include <app/EventLoggingTypes.h>
+#include <app/MessageDef/EventDataIB.h>
+#include <app/data-model/Encode.h>
+#include <app/data-model/FabricScoped.h>
+#include <lib/core/CHIPError.h>
+#include <lib/support/logging/CHIPLogging.h>
+
+#include <type_traits>
+
+namespace chip {
+namespace app {
+namespace InteractionModel {
+
+namespace internal {
+template <typename T>
+class SimpleEventLoggingDelegate : public EventLoggingDelegate
+{
+public:
+    SimpleEventLoggingDelegate(const T & aEventData) : mEventData(aEventData){};
+    CHIP_ERROR WriteEvent(chip::TLV::TLVWriter & aWriter) final override
+    {
+        return DataModel::Encode(aWriter, TLV::ContextTag(EventDataIB::Tag::kData), mEventData);
+    }
+
+private:
+    const T & mEventData;
+};
+
+template <typename E, typename T, std::enable_if_t<DataModel::IsFabricScoped<T>::value, bool> = true>
+EventNumber GenerateEvent(E & emittor, const T & aEventData, EndpointId aEndpoint)
+{
+    internal::SimpleEventLoggingDelegate<T> eventData(aEventData);
+    ConcreteEventPath path(aEndpoint, aEventData.GetClusterId(), aEventData.GetEventId());
+    EventOptions eventOptions;
+    eventOptions.mPath        = path;
+    eventOptions.mPriority    = aEventData.GetPriorityLevel();
+    eventOptions.mFabricIndex = aEventData.GetFabricIndex();
+
+    // this skips logging the event if it's fabric-scoped but no fabric association exists yet.
+
+    if (eventOptions.mFabricIndex == kUndefinedFabricIndex)
+    {
+        ChipLogError(EventLogging, "Event encode failure: no fabric index for fabric scoped event");
+        return kInvalidEventId;
+    }
+
+    //
+    // Unlike attributes which have a different 'EncodeForRead' for fabric-scoped structs,
+    // fabric-sensitive events don't require that since the actual omission of the event in its entirety
+    // happens within the event management framework itself at the time of access.
+    //
+    // The 'mFabricIndex' field in the event options above is encoded out-of-band alongside the event payload
+    // and used to match against the accessing fabric.
+    //
+    EventNumber eventNumber;
+    CHIP_ERROR err = emittor.GenerateEvent(&eventData, eventOptions, eventNumber);
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(EventLogging, "Failed to log event: %" CHIP_ERROR_FORMAT, err.Format());
+        return kInvalidEventId;
+    }
+
+    return eventNumber;
+}
+
+template <typename E, typename T, std::enable_if_t<!DataModel::IsFabricScoped<T>::value, bool> = true>
+EventNumber GenerateEvent(E & emittor, const T & aEventData, EndpointId endpointId)
+{
+    internal::SimpleEventLoggingDelegate<T> eventData(aEventData);
+    ConcreteEventPath path(endpointId, aEventData.GetClusterId(), aEventData.GetEventId());
+    EventOptions eventOptions;
+    eventOptions.mPath     = path;
+    eventOptions.mPriority = aEventData.GetPriorityLevel();
+    EventNumber eventNumber;
+    CHIP_ERROR err = emittor.GenerateEvent(&eventData, eventOptions, eventNumber);
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(EventLogging, "Failed to log event: %" CHIP_ERROR_FORMAT, err.Format());
+        return kInvalidEventId;
+    }
+
+    return eventNumber;
+}
+
+} // namespace internal
+
+class Events
+{
+public:
+    virtual ~Events() = default;
+
+    /// Generates the given event.
+    ///
+    /// Events are generally expected to be sent to subscribed clients and also
+    /// be available for read later until they get overwritten by new events
+    /// that are being generated.
+    virtual CHIP_ERROR GenerateEvent(EventLoggingDelegate * eventContentWriter, const EventOptions & options,
+                                     EventNumber & generatedEventNumber) = 0;
+
+    // Convenience methods for event logging using cluster-object structures
+    // On error, these log and return kInvalidEventId
+    template <typename T>
+    EventNumber GenerateEvent(const T & eventData, EndpointId endpointId)
+    {
+        return internal::GenerateEvent(*this, eventData, endpointId);
+    }
+};
+
+} // namespace InteractionModel
+} // namespace app
+} // namespace chip
diff --git a/src/app/interaction-model/InvokeResponder.h b/src/app/interaction-model/InvokeResponder.h
new file mode 100644
index 0000000..0a399b2
--- /dev/null
+++ b/src/app/interaction-model/InvokeResponder.h
@@ -0,0 +1,316 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <app/MessageDef/StatusIB.h>
+#include <app/data-model/WrappedStructEncoder.h>
+#include <lib/core/CHIPError.h>
+
+namespace chip {
+namespace app {
+namespace InteractionModel {
+
+/// Handles encoding of an invoke response for a specific invoke request.
+///
+/// This class handles a single response (i.e. a CommandDataIB within the
+/// matter protocol) and is responsible for constructing its corresponding
+/// response (i.e. a InvokeResponseIB within the matter protocol)
+///
+/// Invoke responses MUST contain exactly ONE of:
+///   - response data (accessed via `ResponseEncoder`)
+///   - A status, which may be success or failure, both of which may
+///     contain a cluster-specific error code.
+///
+/// To encode a response, `Complete` MUST be called.
+///
+/// `Complete` requirements
+///   - Complete with InteractionModel::Status::Success will respond with data
+///     some response data was written.
+///   - Any other case (including success with cluster specific codes) implies
+///     no response data and a status will be encoded instead
+///       - this includes the case when some response data was written already.
+///         In that case, the response data will be rolled back and only the status
+///         will be encoded.
+///
+/// Creating a response MAY be retried at most once, if and only if `Complete`
+/// returns CHIP_ERROR_BUFFER_TOO_SMALL. Retry attempts MUST not exceed 1:
+///   - FlushPendingResponses MUST be called to make as much buffer space as possible
+///     available for encoding
+///   - The response encoding (including `ResponseEncoder` usage and calling Complete)
+///     MUST be retried once more. If the final Complete returns an error, the result
+///     of the invoke will be an error status.
+///
+class InvokeResponder
+{
+public:
+    virtual ~InvokeResponder() = default;
+
+    // Copying not allowed since underlying requirement is that on deletion of this
+    // object, a reply will be sent.
+    InvokeResponder(const InvokeResponder &)             = delete;
+    InvokeResponder & operator=(const InvokeResponder &) = delete;
+
+    /// Flush any pending replies before encoding the current reply.
+    ///
+    /// MAY be called at most once.
+    ///
+    /// This function is intended to provided the ability to retry sending a reply
+    /// if a reply encoding fails due to insufficient buffer.
+    ///
+    /// Call this if `Complete(...)` returns CHIP_ERROR_BUFFER_TOO_SMALL and try
+    /// again. If reply data is needed, the complete ResponseEncoder + Complete
+    /// call chain MUST be re-run.
+    virtual CHIP_ERROR FlushPendingResponses() = 0;
+
+    /// Reply with a data payload.
+    ///
+    /// MUST be called at most once per reply.
+    /// Can be called a 2nd time after a `FlushPendingResponses()` call
+    ///
+    ///   - responseCommandId must correspond with the data encoded in the returned encoder
+    ///   - Complete(CHIP_NO_ERROR) MUST be called to flush the reply
+    ///
+    /// If encoder returns CHIP_ERROR_BUFFER_TOO_SMALL, FlushPendingResponses should be
+    /// used to attempt to free up buffer space then encoding should be tried again.
+    virtual DataModel::WrappedStructEncoder & ResponseEncoder(CommandId responseCommandId) = 0;
+
+    /// Signal completing of the reply.
+    ///
+    /// MUST be called exactly once to signal a response is to be recorded to be sent.
+    /// The error code (and the data encoded by ResponseEncoder) may be buffered for
+    /// sending among other batched responses.
+    ///
+    /// If this returns CHIP_ERROR_BUFFER_TOO_SMALL, this can be called a 2nd time after
+    /// a FlushPendingResponses.
+    ///
+    /// Argument behavior:
+    ///  - Commands can only be replied with ONE of the following (spec 8.9.4.4):
+    ///      - command data (i.e. ResponseEncoder contents)
+    ///      - A status (including success/error/cluster-specific-success-or-error )
+    ///  - As a result there are two possible paths:
+    ///      - IF a Status::Success is given (WITHOUT cluster specific status), then
+    ///        the data in ResponseEncoder is sent as a reply. If no data was sent,
+    ///        a invoke `Status::Success` with no cluster specific data is sent
+    ///      - OTHERWISE any previously encoded data via ResponseEncoder is discarded
+    ///        and the given reply (success with cluster status or failure) is sent
+    ///        as a reply to the invoke.
+    ///
+    ///
+    /// Returns success/failure state. One error code MUST be handled in particular:
+    ///
+    ///   - CHIP_ERROR_BUFFER_TOO_SMALL will return IF AND ONLY IF the responder was unable
+    ///     to fully serialize the given reply/error data.
+    ///
+    ///     If such an error is returned, the caller MUST retry by calling FlushPendingResponses
+    ///     first and then re-encoding the reply content (use ResponseEncoder if applicable and
+    ///     call Complete again)
+    ///
+    ///   - Any other error (i.e. different from CHIP_NO_ERROR) mean that the invoke response
+    ///     will contain an error and such an error is considered permanent.
+    ///
+    virtual CHIP_ERROR Complete(StatusIB error) = 0;
+};
+
+/// Enforces that once acquired, Complete will be called on the underlying writer
+class AutoCompleteInvokeResponder
+{
+public:
+    // non-copyable: once you have a handle, keep it
+    AutoCompleteInvokeResponder(const AutoCompleteInvokeResponder &)             = delete;
+    AutoCompleteInvokeResponder & operator=(const AutoCompleteInvokeResponder &) = delete;
+
+    AutoCompleteInvokeResponder(InvokeResponder * writer) : mWriter(writer) {}
+    ~AutoCompleteInvokeResponder()
+    {
+        if (mCompleteState != CompleteState::kComplete)
+        {
+            mWriter->Complete(Protocols::InteractionModel::Status::Failure);
+        }
+    }
+
+    /// Direct access to reply encoding.
+    ///
+    /// Use this only in conjunction with the other Raw* calls
+    DataModel::WrappedStructEncoder & RawResponseEncoder(CommandId replyCommandId)
+    {
+        return mWriter->ResponseEncoder(replyCommandId);
+    }
+
+    /// Direct access to flushing replies
+    ///
+    /// Use this only in conjunction with the other Raw* calls
+    CHIP_ERROR RawFlushPendingReplies()
+    {
+        // allow a flush if we never called it (this may not be reasonable, however
+        // we accept an early flush) or if flush is expected
+        VerifyOrReturnError((mCompleteState == CompleteState::kNeverCalled) || (mCompleteState == CompleteState::kFlushExpected),
+                            CHIP_ERROR_INCORRECT_STATE);
+        mCompleteState = CompleteState::kFlushed;
+        return mWriter->FlushPendingResponses();
+    }
+
+    /// Call "Complete" without the automatic retries.
+    ///
+    /// Use this in conjunction with the other Raw* calls
+    CHIP_ERROR RawComplete(StatusIB status)
+    {
+        VerifyOrReturnError((mCompleteState == CompleteState::kNeverCalled) || (mCompleteState == CompleteState::kFlushed),
+                            CHIP_ERROR_INCORRECT_STATE);
+        CHIP_ERROR err = mWriter->Complete(status);
+        if ((err == CHIP_ERROR_BUFFER_TOO_SMALL) && (mCompleteState == CompleteState::kNeverCalled))
+        {
+            mCompleteState = CompleteState::kFlushExpected;
+        }
+        else
+        {
+            mCompleteState = CompleteState::kComplete;
+        }
+        return err;
+    }
+
+    /// Complete the given command.
+    ///
+    /// Automatically handles retries for sending.
+    /// Cannot be called after Raw* methods are used.
+    ///
+    /// Any error returned by this are final and not retriable
+    /// as a retry for CHIP_ERROR_BUFFER_TOO_SMALL is already built in.
+    CHIP_ERROR Complete(StatusIB status)
+    {
+        VerifyOrReturnError(mCompleteState == CompleteState::kNeverCalled, CHIP_ERROR_INCORRECT_STATE);
+        // this is a final complete, including retry handling
+        mCompleteState = CompleteState::kComplete;
+        CHIP_ERROR err = mWriter->Complete(status);
+
+        if (err != CHIP_ERROR_BUFFER_TOO_SMALL)
+        {
+            return err;
+        }
+
+        // retry once. Failure to flush is permanent.
+        ReturnErrorOnFailure(mWriter->FlushPendingResponses());
+        return mWriter->Complete(status);
+    }
+
+    /// Sends the specified data structure as a response
+    ///
+    /// This version of the send has built-in RETRY and handles
+    /// Flush/Complete automatically.
+    /// Cannot be called after Raw* methods are used.
+    ///
+    /// Any error returned by this are final and not retriable
+    /// as a retry for CHIP_ERROR_BUFFER_TOO_SMALL is already built in.
+    template <typename ReplyData>
+    CHIP_ERROR Send(const ReplyData & data)
+    {
+        VerifyOrReturnError(mCompleteState == CompleteState::kNeverCalled, CHIP_ERROR_INCORRECT_STATE);
+        // this is a final complete, including retry handling
+        mCompleteState = CompleteState::kComplete;
+        CHIP_ERROR err = data.Encode(ResponseEncoder(ReplyData::GetCommandId()));
+        if (err != CHIP_ERROR_BUFFER_TOO_SMALL)
+        {
+            LogErrorOnFailure(err);
+            err = mWriter->Complete(StatusIB(err));
+        }
+        if (err != CHIP_ERROR_BUFFER_TOO_SMALL)
+        {
+            return err;
+        }
+
+        // retry once. Failure to flush is permanent.
+        ReturnErrorOnFailure(mWriter->FlushPendingResponses());
+        err = data.Encode(ResponseEncoder(ReplyData::GetCommandId()));
+
+        // If encoding fails, we will end up sending an error back to the other side
+        // the caller
+        LogErrorOnFailure(err);
+        if (err == CHIP_NO_ERROR)
+        {
+            err = mWriter->Complete(StatusIB(err));
+        }
+        else
+        {
+            // Error in "complete" is not something we can really forward anymore since
+            // we already got an error in Encode ... just log this.
+            LogErrorOnFailure(mWriter->Complete(StatusIB(err)));
+        }
+
+        return err;
+    }
+
+private:
+    // Contract says that complete may only be called twice:
+    //   - initial complete
+    //   - again after a `Flush`
+    // The states here expect we are in:
+    //
+    //    +----------------------------Flush---------|
+    //    |                                          v
+    //  NEVER --Complete--> F_EXPECTED --Flush--> FLUSHED --Complete--> COMPLETE
+    //             |                                                    ^
+    //             +-------------(success or permanent error)-----------|
+    enum class CompleteState
+    {
+        kNeverCalled,
+        kFlushExpected,
+        kFlushed,
+        kComplete,
+    };
+
+    InvokeResponder * mWriter;
+    CompleteState mCompleteState = CompleteState::kNeverCalled;
+};
+
+enum ReplyAsyncFlags
+{
+    // Some commands that are expensive to process (e.g. crypto).
+    // Implementations may choose to send an ack on the message right away to
+    // avoid MRP retransmits.
+    kSlowCommandHandling = 0x0001,
+};
+
+class InvokeReply
+{
+public:
+    virtual ~InvokeReply() = default;
+
+    // reply with no data
+    CHIP_ERROR Reply(StatusIB status) { return this->Reply().Complete(status); }
+
+    // Enqueue the content of the reply at this point in time (rather than Async sending it).
+    //
+    // Implementations will often batch several replies into one packet for batch commands,
+    // so it will be implementation-specific on when the actual reply packet is
+    // sent.
+    virtual AutoCompleteInvokeResponder Reply() = 0;
+
+    // Reply "later" to the command. This allows async processing. A reply will be forced
+    // when the returned InvokeReply is destroyed.
+    //
+    // NOTE: Each InvokeReply is associated with a separate `CommandDataIB` within batch
+    //       commands. When replying asynchronously, each InvokeReply will set the response
+    //       data for the given commandpath/ref only.
+    //
+    // IF empty pointer is returned, insufficient memory to reply async is available and
+    // this should be handled (e.g. by returning an error to the handler/replying with
+    // an errorcode synchronously).
+    virtual std::unique_ptr<InvokeReply> ReplyAsync(BitFlags<ReplyAsyncFlags> flags) = 0;
+};
+
+} // namespace InteractionModel
+} // namespace app
+} // namespace chip
diff --git a/src/app/interaction-model/IterationTypes.h b/src/app/interaction-model/IterationTypes.h
new file mode 100644
index 0000000..32ad8bc
--- /dev/null
+++ b/src/app/interaction-model/IterationTypes.h
@@ -0,0 +1,94 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <cstdint>
+#include <optional>
+
+#include <app/ConcreteAttributePath.h>
+#include <app/ConcreteClusterPath.h>
+#include <lib/core/DataModelTypes.h>
+#include <lib/support/BitFlags.h>
+
+namespace chip {
+namespace app {
+namespace InteractionModel {
+
+enum class ClusterQualityFlags : uint32_t
+{
+    kDiagnosticsData = 0x0001, // `K` quality, may be filtered out in subscriptions
+};
+
+struct ClusterInfo
+{
+    DataVersion dataVersion; // current version of this cluster
+    BitFlags<ClusterQualityFlags> flags;
+};
+
+struct ClusterEntry
+{
+    ConcreteClusterPath path;
+    ClusterInfo info;
+};
+
+enum class AttributeQualityFlags : uint32_t
+{
+    kListAttribute  = 0x0001, // This attribute is a list attribute
+    kChangesOmitted = 0x0002, // `C` quality on attributes
+};
+
+struct AttributeInfo
+{
+    BitFlags<AttributeQualityFlags> flags;
+};
+
+struct AttributeEntry
+{
+    ConcreteAttributePath path;
+    AttributeInfo info;
+};
+
+/// Provides metadata information for a data model
+///
+/// The data model can be viewed as a tree of endpoint/cluster/attribute
+/// where each element can be iterated through independently
+///
+/// Iteration rules:
+///   - kInvalidEndpointId will be returned when iteration ends (or generally kInvalid* for paths)
+///   - Any internal iteration errors are just logged (callers do not handle iteration CHIP_ERROR)
+///   - Iteration order is NOT guaranteed, however uniqueness and completeness is (must iterate
+///     over all possible distinct values as long as no internal structural changes occur)
+class AttributeTreeIterator
+{
+public:
+    virtual ~AttributeTreeIterator() = default;
+
+    virtual EndpointId FirstEndpoint()                 = 0;
+    virtual EndpointId NextEndpoint(EndpointId before) = 0;
+
+    virtual ClusterEntry FirstCluster(EndpointId endpoint)                              = 0;
+    virtual ClusterEntry NextCluster(const ConcreteClusterPath & before)                = 0;
+    virtual std::optional<ClusterInfo> GetClusterInfo(const ConcreteClusterPath & path) = 0;
+
+    virtual AttributeEntry FirstAttribute(const ConcreteClusterPath & cluster)                = 0;
+    virtual AttributeEntry NextAttribute(const ConcreteAttributePath & before)                = 0;
+    virtual std::optional<AttributeInfo> GetAttributeInfo(const ConcreteAttributePath & path) = 0;
+};
+
+} // namespace InteractionModel
+} // namespace app
+} // namespace chip
diff --git a/src/app/interaction-model/Model.h b/src/app/interaction-model/Model.h
new file mode 100644
index 0000000..b3a127c
--- /dev/null
+++ b/src/app/interaction-model/Model.h
@@ -0,0 +1,123 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <lib/core/TLVReader.h>
+#include <lib/core/TLVWriter.h>
+
+#include <app/AttributeValueDecoder.h>
+#include <app/AttributeValueEncoder.h>
+
+#include <app/interaction-model/Actions.h>
+#include <app/interaction-model/InvokeResponder.h>
+#include <app/interaction-model/IterationTypes.h>
+#include <app/interaction-model/OperationTypes.h>
+
+namespace chip {
+namespace app {
+namespace InteractionModel {
+
+/// Represents operations against a matter-defined data model.
+///
+/// Class is SINGLE-THREADED:
+///   - operations are assumed to only be ever run in a single event-loop
+///     thread or equivalent
+///   - class is allowed to attempt to cache indexes/locations for faster
+///     lookups of things (e.g during iterations)
+class Model : public AttributeTreeIterator
+{
+public:
+    virtual ~Model() = default;
+
+    // `actions` pointers  will be guaranteed valid until Shutdown is called()
+    virtual CHIP_ERROR Startup(InteractionModelActions actions)
+    {
+        mActions = actions;
+        return CHIP_NO_ERROR;
+    }
+    virtual CHIP_ERROR Shutdown() = 0;
+
+    // During the transition phase, we expect a large subset of code to require access to
+    // event emitting, path marking and other operations
+    virtual InteractionModelActions CurrentActions() { return mActions; }
+
+    /// List reading has specific handling logic:
+    ///   `state` contains in/out data about the current list reading. MUST start with kInvalidListIndex on first call
+    ///
+    /// Return codes:
+    ///   CHIP_ERROR_MORE_LIST_DATA_AVAILABLE (NOTE: new error defined for this purpose)
+    ///      - partial data written to the destination
+    ///      - destination will contain AT LEAST one valid list entry fully serialized
+    ///      - destination will be fully valid (it will be rolled back on partial list writes)
+    ///   CHIP_IM_GLOBAL_STATUS(code):
+    ///      - error codes that are translatable in IM status codes (otherwise we expect Failure to be reported)
+    ///      - In particular, some handlers rely on special handling for:
+    ///        - `UnsupportedAccess`  - for ACL checks (e.g. wildcard expansion may choose to skip these)
+    ///      - to check for this, CHIP_ERROR provides:
+    ///        - ::IsPart(ChipError::SdkPart::kIMGlobalStatus) -> bool
+    ///        - ::GetSdkCode() -> uint8_t to translate to the actual code
+    virtual CHIP_ERROR ReadAttribute(const ReadAttributeRequest & request, ReadState & state, AttributeValueEncoder & encoder) = 0;
+
+    /// Requests a write of an attribute.
+    ///
+    /// When this is invoked, caller is expected to have already done some validations:
+    ///    - cluster `data version` has been checked for the incoming request if applicable
+    ///
+    /// List operation support:
+    ///    - the first list write will have `request.writeFlags.Has(WriteFlags::kListBegin)`
+    ///    - the last list write will have `request.writeFlags.Has(WriteFlags::kListEnd)`
+    ///    - the last list write MAY have empty data (no list items)
+    ///
+    /// When `request.writeFlags.Has(WriteFlags::kForceInternal)` the request is from an internal app update
+    /// and SHOULD bypass some internal checks (like timed enforcement, potentially read-only restrictions)
+    ///
+    /// Return codes
+    ///   CHIP_IM_GLOBAL_STATUS(code):
+    ///       - error codes that are translatable to specific IM codes
+    ///       - in particular, the following codes are interesting/expected
+    ///         - `UnsupportedWrite` for attempts to write read-only data
+    ///         - `UnsupportedAccess` for ACL failures
+    ///         - `NeedsTimedInteraction` for writes that are not timed however are required to be so
+    virtual CHIP_ERROR WriteAttribute(const WriteAttributeRequest & request, AttributeValueDecoder & decoder) = 0;
+
+    /// `responder` is used to send back the reply.
+    ///    - calling Reply() or ReplyAsync() will let the application control the reply
+    ///    - returning a CHIP_NO_ERROR without reply/reply_async implies a Status::Success reply without data
+    ///    - returning a CHIP_*_ERROR implies an error reply (error and data are mutually exclusive)
+    ///
+    /// See InvokeReply/AutoCompleteInvokeResponder for details on how to send back replies and expected
+    /// error handling. If you require knowledge if a response was successfully sent, use the underlying
+    /// `reply` object instead of returning an error codes from Invoke.
+    ///
+    /// Return codes
+    ///   CHIP_IM_GLOBAL_STATUS(code):
+    ///       - error codes that are translatable to specific IM codes
+    ///       - in particular, the following codes are interesting/expected
+    ///         - `UnsupportedEndpoint` for invalid endpoint
+    ///         - `UnsupportedCluster` for no such cluster on the endpoint
+    ///         - `UnsupportedCommand` for no such command in the cluster
+    ///         - `UnsupportedAccess` for permission errors (ACL or fabric scoped with invalid fabric)
+    ///         - `NeedsTimedInteraction` if the invoke requires timed interaction support
+    virtual CHIP_ERROR Invoke(const InvokeRequest & request, chip::TLV::TLVReader & input_arguments, InvokeReply & reply) = 0;
+
+private:
+    InteractionModelActions mActions = { nullptr };
+};
+
+} // namespace InteractionModel
+} // namespace app
+} // namespace chip
diff --git a/src/app/interaction-model/OperationTypes.h b/src/app/interaction-model/OperationTypes.h
new file mode 100644
index 0000000..115dc11
--- /dev/null
+++ b/src/app/interaction-model/OperationTypes.h
@@ -0,0 +1,94 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <access/SubjectDescriptor.h>
+#include <app/ConcreteAttributePath.h>
+#include <app/ConcreteCommandPath.h>
+#include <lib/support/BitFlags.h>
+
+#include <cstdint>
+#include <optional>
+
+namespace chip {
+namespace app {
+namespace InteractionModel {
+
+/// Contains common flags among all interaction model operations: read/write/invoke
+enum class OperationFlags : uint32_t
+{
+    kInternal = 0x0001, // Internal request for data changes (can bypass checks/ACL etc.)
+};
+
+/// This information is available for ALL interactions: read/write/invoke
+struct OperationRequest
+{
+    OperationFlags operationFlags;
+
+    /// Current authentication data EXCEPT for internal requests.
+    ///  - Non-internal requests MUST have this set.
+    ///  - operationFlags.Has(OperationFlags::kInternal) MUST NOT have this set
+    std::optional<chip::Access::SubjectDescriptor> subjectDescriptor;
+};
+
+enum class ReadFlags : uint32_t
+{
+    kFabricFiltered = 0x0001, // reading is performed fabric-filtered
+};
+
+struct ReadAttributeRequest : OperationRequest
+{
+    ConcreteAttributePath path;
+    std::optional<DataVersion> dataVersion;
+    BitFlags<ReadFlags> readFlags;
+};
+
+struct ReadState
+{
+    // When reading lists, reading will start at this index.
+    // As list data is read, this index is incremented
+    ListIndex listEncodeStart = kInvalidListIndex;
+};
+
+enum class WriteFlags : uint32_t
+{
+    kTimed     = 0x0001, // Received as a 2nd command after a timed invoke
+    kListBegin = 0x0002, // This is the FIRST list data element in a series of data
+    kListEnd   = 0x0004, // This is the LAST list element to write
+};
+
+struct WriteAttributeRequest : OperationRequest
+{
+    ConcreteDataAttributePath path; // NOTE: this also contains LIST operation options (i.e. "data" path type)
+    BitFlags<WriteFlags> writeFlags;
+};
+
+enum class InvokeFlags : uint32_t
+{
+    kTimed = 0x0001, // Received as a 2nd command after a timed invoke
+};
+
+struct InvokeRequest : OperationRequest
+{
+    ConcreteCommandPath path;
+    std::optional<GroupId> groupRequestId; // set if and only if this was a group request
+    BitFlags<InvokeFlags> invokeFlags;
+};
+
+} // namespace InteractionModel
+} // namespace app
+} // namespace chip
diff --git a/src/app/interaction-model/Paths.h b/src/app/interaction-model/Paths.h
new file mode 100644
index 0000000..2bf9f0c
--- /dev/null
+++ b/src/app/interaction-model/Paths.h
@@ -0,0 +1,48 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <app/AttributePathParams.h>
+
+namespace chip {
+namespace app {
+namespace InteractionModel {
+
+/// Handles path attributes for interaction models.
+///
+/// It allows a user of the class to mark specific paths
+/// as having changed. The intended use is for some listener to
+/// perform operations as a result of something having changed,
+/// usually by forwarding updates (e.g. in case of subscriptions
+/// that cover that path).
+///
+/// Methods on this class MUCH be called from within the matter
+/// main loop as they will likely trigger interaction model
+/// internal updates and subscription event updates.
+class Paths
+{
+public:
+    virtual ~Paths() = 0;
+
+    /// Mark some specific attributes dirty.
+    /// Wildcards are supported.
+    virtual void MarkDirty(const AttributePathParams & path) = 0;
+};
+
+} // namespace InteractionModel
+} // namespace app
+} // namespace chip
diff --git a/src/app/interaction-model/RequestContext.h b/src/app/interaction-model/RequestContext.h
new file mode 100644
index 0000000..74fa9af
--- /dev/null
+++ b/src/app/interaction-model/RequestContext.h
@@ -0,0 +1,43 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <messaging/ExchangeContext.h>
+
+namespace chip {
+namespace app {
+namespace InteractionModel {
+
+// Context for a currently executing request
+class RequestContext
+{
+public:
+    virtual ~RequestContext() = default;
+
+    /// Valid ONLY during synchronous handling of a Read/Write/Invoke
+    ///
+    /// Used sparingly, however some operations will require these. An example
+    /// usage is "Operational Credentials aborting communications on removed fabrics"
+    ///
+    /// Callers MUST check for null here (e.g. unit tests mocks may set this to
+    /// nullptr due to object complexity)
+    virtual Messaging::ExchangeContext * CurrentExchange() = 0;
+};
+
+} // namespace InteractionModel
+} // namespace app
+} // namespace chip
diff --git a/src/app/interaction-model/tests/BUILD.gn b/src/app/interaction-model/tests/BUILD.gn
new file mode 100644
index 0000000..c7d36b4
--- /dev/null
+++ b/src/app/interaction-model/tests/BUILD.gn
@@ -0,0 +1,23 @@
+# Copyright (c) 2024 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import("//build_overrides/chip.gni")
+import("${chip_root}/build/chip/chip_test_suite.gni")
+
+chip_test_suite("tests") {
+  test_sources = [ "TestEventEmitting.cpp" ]
+
+  cflags = [ "-Wconversion" ]
+
+  public_deps = [ "${chip_root}/src/app/interaction-model" ]
+}
diff --git a/src/app/interaction-model/tests/TestEventEmitting.cpp b/src/app/interaction-model/tests/TestEventEmitting.cpp
new file mode 100644
index 0000000..cb49dc2
--- /dev/null
+++ b/src/app/interaction-model/tests/TestEventEmitting.cpp
@@ -0,0 +1,164 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include <app-common/zap-generated/cluster-objects.h>
+#include <app/data-model/Decode.h>
+#include <app/interaction-model/Events.h>
+#include <lib/support/CodeUtils.h>
+
+#include <gtest/gtest.h>
+
+namespace {
+
+using namespace chip;
+using namespace chip::app;
+using namespace chip::app::InteractionModel;
+
+using StartUpEventType              = chip::app::Clusters::BasicInformation::Events::StartUp::Type;
+using AccessControlEntryChangedType = chip::app::Clusters::AccessControl::Events::AccessControlEntryChanged::Type;
+
+constexpr uint32_t kFakeSoftwareVersion = 0x1234abcd;
+
+/// Keeps the "last event" in-memory to allow tests to validate
+/// that event writing and encoding worked.
+class LogOnlyEvents : public Events
+{
+public:
+    CHIP_ERROR GenerateEvent(EventLoggingDelegate * eventContentWriter, const EventOptions & options,
+                             EventNumber & generatedEventNumber) override
+    {
+        TLV::TLVWriter writer;
+        TLV::TLVType outerType;
+        writer.Init(mLastEventEncodeBuffer);
+
+        ReturnErrorOnFailure(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, outerType));
+        ReturnErrorOnFailure(eventContentWriter->WriteEvent(writer));
+        ReturnErrorOnFailure(writer.EndContainer(outerType));
+        ReturnErrorOnFailure(writer.Finalize());
+        mLastEncodedSpan = ByteSpan(mLastEventEncodeBuffer, writer.GetLengthWritten());
+
+        mLastOptions         = options;
+        generatedEventNumber = ++mCurrentEventNumber;
+
+        return CHIP_NO_ERROR;
+    }
+
+    EventNumber CurrentEventNumber() const { return mCurrentEventNumber; }
+    const EventOptions & LastOptions() const { return mLastOptions; }
+    ByteSpan LastWrittenEvent() const { return mLastEncodedSpan; }
+
+    // This relies on the default encoding of events which uses
+    // DataModel::Encode on a EventDataIB::Tag::kData
+    template <typename T>
+    CHIP_ERROR DecodeLastEvent(T & dest)
+    {
+        // attempt to decode the last encoded event
+        TLV::TLVReader reader;
+        TLV::TLVType outerType;
+
+        reader.Init(LastWrittenEvent());
+
+        ReturnErrorOnFailure(reader.Next());
+        ReturnErrorOnFailure(reader.EnterContainer(outerType));
+
+        ReturnErrorOnFailure(reader.Next()); // MUST be positioned on the first element
+        ReturnErrorOnFailure(DataModel::Decode(reader, dest));
+
+        ReturnErrorOnFailure(reader.ExitContainer(outerType));
+
+        return CHIP_NO_ERROR;
+    }
+
+private:
+    EventNumber mCurrentEventNumber = 0;
+    EventOptions mLastOptions;
+    uint8_t mLastEventEncodeBuffer[128];
+    ByteSpan mLastEncodedSpan;
+};
+
+} // namespace
+
+TEST(TestInteractionModelEventEmitting, TestBasicType)
+{
+    LogOnlyEvents logOnlyEvents;
+    Events * events = &logOnlyEvents;
+
+    StartUpEventType event{ kFakeSoftwareVersion };
+
+    EventNumber n1 = events->GenerateEvent(event, 0 /* EndpointId */);
+    ASSERT_EQ(n1, logOnlyEvents.CurrentEventNumber());
+    ASSERT_EQ(logOnlyEvents.LastOptions().mPath,
+              ConcreteEventPath(0 /* endpointId */, StartUpEventType::GetClusterId(), StartUpEventType::GetEventId()));
+
+    chip::app::Clusters::BasicInformation::Events::StartUp::DecodableType decoded_event;
+    CHIP_ERROR err = logOnlyEvents.DecodeLastEvent(decoded_event);
+
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(EventLogging, "Decoding failed: %" CHIP_ERROR_FORMAT, err.Format());
+    }
+    ASSERT_EQ(err, CHIP_NO_ERROR);
+    ASSERT_EQ(decoded_event.softwareVersion, kFakeSoftwareVersion);
+
+    EventNumber n2 = events->GenerateEvent(event, /* endpointId = */ 1);
+    ASSERT_EQ(n2, logOnlyEvents.CurrentEventNumber());
+    ASSERT_NE(n1, logOnlyEvents.CurrentEventNumber());
+
+    ASSERT_EQ(logOnlyEvents.LastOptions().mPath,
+              ConcreteEventPath(1 /* endpointId */, StartUpEventType::GetClusterId(), StartUpEventType::GetEventId()));
+}
+
+TEST(TestInteractionModelEventEmitting, TestFabricScoped)
+{
+    constexpr NodeId kTestNodeId           = 0x12ab;
+    constexpr uint16_t kTestPasscode       = 12345;
+    constexpr FabricIndex kTestFabricIndex = kMinValidFabricIndex + 10;
+    static_assert(kTestFabricIndex != kUndefinedFabricIndex);
+
+    LogOnlyEvents logOnlyEvents;
+    Events * events = &logOnlyEvents;
+
+    AccessControlEntryChangedType event;
+    event.adminNodeID     = chip::app::DataModel::MakeNullable(kTestNodeId);
+    event.adminPasscodeID = chip::app::DataModel::MakeNullable(kTestPasscode);
+
+    EventNumber n1 = events->GenerateEvent(event, 0 /* EndpointId */);
+    // encoding without a fabric ID MUST fail for fabric events
+    ASSERT_EQ(n1, kInvalidEventId);
+
+    event.fabricIndex = kTestFabricIndex;
+    n1                = events->GenerateEvent(event, /* endpointId = */ 0);
+
+    ASSERT_NE(n1, kInvalidEventId);
+    ASSERT_EQ(n1, logOnlyEvents.CurrentEventNumber());
+    ASSERT_EQ(logOnlyEvents.LastOptions().mPath,
+              ConcreteEventPath(0 /* endpointId */, AccessControlEntryChangedType::GetClusterId(),
+                                AccessControlEntryChangedType::GetEventId()));
+
+    chip::app::Clusters::AccessControl::Events::AccessControlEntryChanged::DecodableType decoded_event;
+    CHIP_ERROR err = logOnlyEvents.DecodeLastEvent(decoded_event);
+
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(EventLogging, "Decoding failed: %" CHIP_ERROR_FORMAT, err.Format());
+    }
+    ASSERT_EQ(err, CHIP_NO_ERROR);
+    ASSERT_EQ(decoded_event.adminNodeID.ValueOr(0), kTestNodeId);
+    ASSERT_EQ(decoded_event.adminPasscodeID.ValueOr(0), kTestPasscode);
+    ASSERT_EQ(decoded_event.fabricIndex, kTestFabricIndex);
+}