Repository: OpenAssetIO/OpenAssetIO (language: python)

Issue:
Ensure base capabilities satisfied when initializing a manager
## What

Provide early checks to determine if the resulting manager is missing any required capabilities early on in the bootstrap process.

- `kEnityReferenceIdentification`
- `kManagementPolicy`

## Why

In order to facilitate multi-language plugins, some required methods are not enforced at the implementation level (as they may be provide by a corresponding plugin in another language). This means there are no build-time guards in place to protect against programming errors in an implementation.

## Acceptance Criteria

- `Manager.initialize` validates that the resulting `ManagerInterface` instance has the `kEntityReferenceIdentification` and `kManagementPolicy` capabilities _after_ initializing the underlying interface.
  - If either is missing, a `ConfigurationError` is raised with a message denoting that the requested implementation does not support the required capabilities, and which capabilities are missing.


Discussion / hints:

Fix patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index df901096b..8ca40eb43 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -78,6 +78,11 @@ v1.0.0-alpha.xx
   implementation.
   [#163](https://github.com/OpenAssetIO/OpenAssetIO/issues/163)
 
+- The middleware will validate that the manager's implementation provides
+  the `kEntityReferenceIdentification` and `kManagementPolicyQuery`
+  capabilities upon initialization.
+  [#1148](https://github.com/OpenAssetIO/OpenAssetIO/issues/1148)
+
 ### New Features
 
 - Added new exception types, allowing all OpenAssetIO exceptions to
diff --git a/src/openassetio-core/src/hostApi/Manager.cpp b/src/openassetio-core/src/hostApi/Manager.cpp
index f4f6f1a83..6be0971d4 100644
--- a/src/openassetio-core/src/hostApi/Manager.cpp
+++ b/src/openassetio-core/src/hostApi/Manager.cpp
@@ -1,7 +1,9 @@
 // SPDX-License-Identifier: Apache-2.0
 // Copyright 2013-2023 The Foundry Visionmongers Ltd
+#include <array>
 #include <stdexcept>
 #include <utility>
+#include <vector>
 
 #include <fmt/format.h>
 
@@ -23,6 +25,41 @@
 namespace openassetio {
 inline namespace OPENASSETIO_CORE_ABI_VERSION {
 namespace {
+
+/**
+ * Validate the supplied ManagerInterface supports all required
+ * capabilities, or throw a ConfigurationException.
+ *
+ * These capabilities are required at runtime, but not enforced at
+ * compile time, as multi-language implementations may only provide the
+ * implementation in one of the component languages.
+ */
+void verifyRequiredCapabilities(const managerApi::ManagerInterfacePtr &interface) {
+  using managerApi::ManagerInterface;
+
+  static constexpr std::array kRequiredCapabilities = {
+      ManagerInterface::Capability::kEntityReferenceIdentification,
+      ManagerInterface::Capability::kManagementPolicyQueries};
+
+  std::vector<std::string> missingCapabilities;
+  for (const ManagerInterface::Capability capability : kRequiredCapabilities) {
+    if (!interface->hasCapability(capability)) {
+      missingCapabilities.emplace_back(
+          ManagerInterface::kCapabilityNames[static_cast<size_t>(capability)]);
+    }
+  }
+
+  if (missingCapabilities.empty()) {
+    return;
+  }
+
+  std::string msg =
+      fmt::format("Manager implementation for '{}' does not support the required capabilities: {}",
+                  interface->identifier(), fmt::join(missingCapabilities, ", "));
+
+  throw errors::ConfigurationException(msg);
+}
+
 /**
  * Extract the entity reference prefix from a manager plugin's info
  * dictionary, if available.
@@ -82,6 +119,12 @@ InfoDictionary Manager::settings() { return managerInterface_->settings(hostSess
 void Manager::initialize(InfoDictionary managerSettings) {
   managerInterface_->initialize(std::move(managerSettings), hostSession_);
 
+  // Verify the manager has required capabilities. This must only be
+  // done after initialization, to ensure we can support proxy interface
+  // implementations that need initializing to configure the proxied
+  // implementation
+  verifyRequiredCapabilities(managerInterface_);
+
   entityReferencePrefix_ =
       entityReferencePrefixFromInfo(hostSession_->logger(), managerInterface_->info());
 }


Test patch:
diff --git a/src/openassetio-python/tests/package/hostApi/test_manager.py b/src/openassetio-python/tests/package/hostApi/test_manager.py
index d722f9573..b43af7e70 100644
--- a/src/openassetio-python/tests/package/hostApi/test_manager.py
+++ b/src/openassetio-python/tests/package/hostApi/test_manager.py
@@ -37,6 +37,7 @@
     BatchElementException,
     BatchElementError,
     InputValidationException,
+    ConfigurationException,
 )
 from openassetio.hostApi import Manager, EntityReferencePager
 from openassetio.managerApi import EntityReferencePagerInterface, ManagerInterface
@@ -240,6 +241,56 @@ def test_when_entity_ref_prefix_type_invalid_then_debug_log_printed(
         )
 
 
+class Test_Manager_initialize_capablility_check:
+    def test_has_capability_called_after_initialize(
+        self, manager, a_host_session, mock_manager_interface
+    ):
+        mock_manager_interface.mock.hasCapability.return_value = True
+
+        manager.initialize({})
+
+        assert mock_manager_interface.mock.method_calls[0:3] == [
+            mock.call.initialize({}, a_host_session),
+            mock.call.hasCapability(ManagerInterface.Capability.kEntityReferenceIdentification),
+            mock.call.hasCapability(ManagerInterface.Capability.kManagementPolicyQueries),
+        ]
+
+    def test_when_manager_has_capabilities_then_no_error(self, manager, mock_manager_interface):
+        mock_manager_interface.mock.hasCapability.return_value = True
+        manager.initialize({})
+
+    @pytest.mark.parametrize(
+        "missing_capabilities",
+        [
+            [ManagerInterface.Capability.kEntityReferenceIdentification],
+            [ManagerInterface.Capability.kManagementPolicyQueries],
+            [
+                ManagerInterface.Capability.kEntityReferenceIdentification,
+                ManagerInterface.Capability.kManagementPolicyQueries,
+            ],
+        ],
+    )
+    def test_when_manager_does_not_have_capability_then_ConfigurationException_raised(
+        self, manager, mock_manager_interface, missing_capabilities
+    ):
+        def mock_has_capability(capability):
+            return capability not in missing_capabilities
+
+        mock_manager_interface.mock.hasCapability.side_effect = mock_has_capability
+        mock_manager_interface.mock.identifier.return_value = "expected.identifier"
+
+        capability_names = ", ".join(
+            [ManagerInterface.kCapabilityNames[c] for c in missing_capabilities]
+        )
+        expected_msg = (
+            f"Manager implementation for '{manager.identifier()}' does not "
+            + f"support the required capabilities: {capability_names}"
+        )
+
+        with pytest.raises(ConfigurationException, match=expected_msg):
+            manager.initialize({})
+
+
 manager_capabilities = [
     (Manager.Capability.kStatefulContexts, ManagerInterface.Capability.kStatefulContexts),
     (Manager.Capability.kCustomTerminology, ManagerInterface.Capability.kCustomTerminology),