diff --git a/pyproject.toml b/pyproject.toml index 82d3c108..fce3e853 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.53.0" +version = "0.54.0" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/extensibility/_ums_transport.py b/src/sap_cloud_sdk/extensibility/_ums_transport.py index 92ebf9c6..6c95db14 100644 --- a/src/sap_cloud_sdk/extensibility/_ums_transport.py +++ b/src/sap_cloud_sdk/extensibility/_ums_transport.py @@ -65,6 +65,7 @@ id title extensionVersion + isActive solutionId jouleStudioGsid capabilityImplementations { @@ -372,6 +373,10 @@ def _transform_ums_response( for edge in edges: node = edge.get("node", {}) + # Only include extensions that are explicitly active. + # isActive absent/None means the extension is not active. + if node.get("isActive") is not True: + continue nodes.append(node) title = node.get("title", "") if title: diff --git a/tests/extensibility/unit/_ums_test_helpers.py b/tests/extensibility/unit/_ums_test_helpers.py index 2a91e4a0..1a1ee4fd 100644 --- a/tests/extensibility/unit/_ums_test_helpers.py +++ b/tests/extensibility/unit/_ums_test_helpers.py @@ -40,6 +40,7 @@ "id": "ext-instance-1", "title": "ServiceNow Extension", "extensionVersion": "2.1.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -96,6 +97,7 @@ "id": "ext-instance-1", "title": "ServiceNow Extension", "extensionVersion": "1.0.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -122,6 +124,7 @@ "id": "ext-instance-2", "title": "Jira Extension", "extensionVersion": "3.2.1", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -182,6 +185,7 @@ "id": "ext-1", "title": "Minimal Extension", "extensionVersion": "0.1.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -217,6 +221,7 @@ "id": "ext-1", "title": "Empty Instruction Extension", "extensionVersion": "1.0.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -233,8 +238,7 @@ } } -UMS_RESPONSE_DIFFERENT_CAPABILITY = { - "data": { +UMS_RESPONSE_DIFFERENT_CAPABILITY = { "data": { "EXTHUB__ExtCapImplementationInstances": { "edges": [ { @@ -242,6 +246,7 @@ "id": "ext-1", "title": "Other Extension", "extensionVersion": "2.0.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "onboarding", @@ -286,6 +291,146 @@ } } +# A single extension explicitly deactivated (isActive: False). +UMS_RESPONSE_DEACTIVATED = { + "data": { + "EXTHUB__ExtCapImplementationInstances": { + "edges": [ + { + "node": { + "id": "ext-deactivated-1", + "title": "Deactivated Extension", + "extensionVersion": "1.0.0", + "isActive": False, + "capabilityImplementations": [ + { + "capabilityId": "default", + "instruction": {"text": "Should not appear."}, + "tools": { + "additions": [ + { + "type": "MCP", + "mcpConfig": { + "globalTenantId": "tenant-x", + "ordId": "sap.mcp:apiResource:deactivated:v1", + "toolNames": ["hidden_tool"], + }, + } + ] + }, + "hooks": [], + } + ], + } + } + ], + "pageInfo": {"hasNextPage": False, "cursor": None}, + } + } +} + +# One active extension and one deactivated extension in the same response. +UMS_RESPONSE_MIXED_ACTIVE = { + "data": { + "EXTHUB__ExtCapImplementationInstances": { + "edges": [ + { + "node": { + "id": "ext-active-1", + "title": "Active Extension", + "extensionVersion": "2.0.0", + "isActive": True, + "capabilityImplementations": [ + { + "capabilityId": "default", + "instruction": {"text": "Active instruction."}, + "tools": { + "additions": [ + { + "type": "MCP", + "mcpConfig": { + "globalTenantId": "tenant-a", + "ordId": "sap.mcp:apiResource:active:v1", + "toolNames": ["active_tool"], + }, + } + ] + }, + "hooks": [], + } + ], + } + }, + { + "node": { + "id": "ext-inactive-1", + "title": "Inactive Extension", + "extensionVersion": "1.0.0", + "isActive": False, + "capabilityImplementations": [ + { + "capabilityId": "default", + "instruction": {"text": "Should not appear."}, + "tools": { + "additions": [ + { + "type": "MCP", + "mcpConfig": { + "globalTenantId": "tenant-b", + "ordId": "sap.mcp:apiResource:inactive:v1", + "toolNames": ["inactive_tool"], + }, + } + ] + }, + "hooks": [], + } + ], + } + }, + ], + "pageInfo": {"hasNextPage": False, "cursor": None}, + } + } +} + +# Extension with no isActive field — deployed before activate/deactivate existed. +UMS_RESPONSE_LEGACY_NO_IS_ACTIVE = { + "data": { + "EXTHUB__ExtCapImplementationInstances": { + "edges": [ + { + "node": { + "id": "ext-legacy-1", + "title": "Legacy Extension", + "extensionVersion": "1.0.0", + "capabilityImplementations": [ + { + "capabilityId": "default", + "instruction": {"text": "Legacy instruction."}, + "tools": { + "additions": [ + { + "type": "MCP", + "mcpConfig": { + "globalTenantId": "tenant-leg", + "ordId": "sap.mcp:apiResource:legacy:v1", + "toolNames": ["legacy_tool"], + }, + } + ] + }, + "hooks": [], + } + ], + } + } + ], + "pageInfo": {"hasNextPage": False, "cursor": None}, + } + } +} + # --------------------------------------------------------------------------- # Helper functions diff --git a/tests/extensibility/unit/test_ums_pagination.py b/tests/extensibility/unit/test_ums_pagination.py index 35d06277..47b821de 100644 --- a/tests/extensibility/unit/test_ums_pagination.py +++ b/tests/extensibility/unit/test_ums_pagination.py @@ -72,6 +72,7 @@ def test_multiple_pages_accumulates_edges(self): "id": "ext-1", "title": "Extension A", "extensionVersion": "1.0.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -110,6 +111,7 @@ def test_multiple_pages_accumulates_edges(self): "id": "ext-2", "title": "Extension B", "extensionVersion": "2.0.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -179,6 +181,7 @@ def test_cursor_sent_on_subsequent_pages(self): "id": "ext-1", "title": "Ext", "extensionVersion": "1.0.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -269,6 +272,7 @@ def test_error_on_second_page_raises(self): "id": "ext-1", "title": "Ext", "extensionVersion": "1.0.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -311,6 +315,7 @@ def test_missing_page_info_stops_pagination(self): "id": "ext-1", "title": "Ext", "extensionVersion": "1.0.0", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", diff --git a/tests/extensibility/unit/test_ums_parsing.py b/tests/extensibility/unit/test_ums_parsing.py index 0cb2af6b..d7f77864 100644 --- a/tests/extensibility/unit/test_ums_parsing.py +++ b/tests/extensibility/unit/test_ums_parsing.py @@ -22,6 +22,9 @@ UMS_RESPONSE_NO_INSTRUCTION, UMS_RESPONSE_EMPTY_INSTRUCTION, UMS_RESPONSE_DIFFERENT_CAPABILITY, + UMS_RESPONSE_DEACTIVATED, + UMS_RESPONSE_MIXED_ACTIVE, + UMS_RESPONSE_LEGACY_NO_IS_ACTIVE, ) @@ -339,6 +342,7 @@ def test_hooks_with_unknown_type_skipped(self): "node": { "id": "ext-1", "title": "Test", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -381,6 +385,7 @@ def test_node_without_title(self): { "node": { "id": "ext-1", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -408,6 +413,7 @@ def test_hooks_null_in_response(self): "node": { "id": "ext-1", "title": "Null Hooks Extension", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -446,6 +452,7 @@ def test_tools_key_missing(self): "node": { "id": "ext-1", "title": "No tools", + "isActive": True, "capabilityImplementations": [ { "capabilityId": "default", @@ -459,3 +466,29 @@ def test_tools_key_missing(self): } result = _transform_ums_response(data, "default") assert result.mcp_servers == [] + + def test_deactivated_extension_excluded(self): + """isActive: False node must be filtered out entirely.""" + result = _transform_ums_response(UMS_RESPONSE_DEACTIVATED["data"], "default") + assert result.extension_names == [] + assert result.mcp_servers == [] + assert result.instruction is None + assert result.hooks == [] + + def test_mixed_active_inactive(self): + """Active node included; deactivated node excluded from the same response.""" + result = _transform_ums_response(UMS_RESPONSE_MIXED_ACTIVE["data"], "default") + assert result.extension_names == ["Active Extension"] + assert len(result.mcp_servers) == 1 + assert result.mcp_servers[0].ord_id == "sap.mcp:apiResource:active:v1" + assert result.instruction == "Active instruction." + + def test_legacy_extension_without_is_active_excluded(self): + """isActive absent (null) means not active — extension is excluded.""" + result = _transform_ums_response( + UMS_RESPONSE_LEGACY_NO_IS_ACTIVE["data"], "default" + ) + assert result.extension_names == [] + assert result.mcp_servers == [] + assert result.instruction is None + assert result.hooks == []