# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Tests for the Schicht-3 NodeAdapter projection (Phase 3). Covers the pure projection helpers in nodeAdapter.py: - identifying method-bound vs framework-primitive nodes - extracting bindsAction - building UserParamMapping from legacy parameter dicts - converting inputPorts dict-of-dicts into per-port accepts lists - end-to-end legacy-node → NodeAdapter projection These tests do NOT touch live methods; they verify the projection logic in isolation so it is robust before the adapterValidator composes with it. """ from __future__ import annotations import pytest from modules.features.graphicalEditor.nodeAdapter import ( NodeAdapter, UserParamMapping, _adapterFromLegacyNode, _bindsActionFromLegacy, _extractVisibleWhen, _isMethodBoundNode, _projectAllAdapters, _projectInputAccepts, _userParamFromLegacyParam, ) def _legacyMethodNode(**overrides): base = { "id": "trustee.processDocuments", "category": "trustee", "label": "Verarbeiten", "description": "...", "parameters": [ {"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef", "description": "Eingabe"}, {"name": "featureInstanceId", "type": "FeatureInstanceRef", "required": True, "frontendType": "hidden", "description": "Trustee-Instanz"}, ], "inputs": 1, "outputs": 1, "inputPorts": {0: {"accepts": ["DocumentList", "Transit"]}}, "outputPorts": {0: {"schema": "TrusteeProcessResult"}}, "meta": {"icon": "mdi-x", "color": "#000", "usesAi": False}, "_method": "trustee", "_action": "processDocuments", } base.update(overrides) return base def _primitiveNode(**overrides): base = { "id": "flow.loop", "category": "flow", "label": "Schleife", "parameters": [{"name": "items", "type": "string", "required": True}], "inputs": 1, "outputs": 1, "inputPorts": {0: {"accepts": ["Transit"]}}, "executor": "flow", } base.update(overrides) return base class TestIsMethodBound: def test_methodBoundIsTrue(self): assert _isMethodBoundNode(_legacyMethodNode()) is True def test_primitiveIsFalse(self): assert _isMethodBoundNode(_primitiveNode()) is False @pytest.mark.parametrize("partial", [ {"_method": "trustee"}, # missing _action {"_action": "processDocuments"}, # missing _method {}, ]) def test_partialBindingIsFalse(self, partial): node = _primitiveNode(**partial) assert _isMethodBoundNode(node) is False class TestBindsActionFromLegacy: def test_returnsCanonicalFqn(self): assert _bindsActionFromLegacy(_legacyMethodNode()) == "trustee.processDocuments" def test_returnsNoneForPrimitive(self): assert _bindsActionFromLegacy(_primitiveNode()) is None class TestUserParamFromLegacy: def test_carriesEditorOverridesOnly(self): legacy = {"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef", "description": "Eingabe", "default": []} mapping = _userParamFromLegacyParam(legacy) assert isinstance(mapping, UserParamMapping) assert mapping.actionArg == "documentList" assert mapping.uiHint == "dataRef" assert mapping.description == "Eingabe" assert mapping.defaultValue == [] assert mapping.frontendOptions is None def test_extractsConditionalVisibility(self): legacy = { "name": "filterJson", "type": "string", "frontendType": "textarea", "frontendOptions": {"dependsOn": "mode", "showWhen": ["raw", "aggregate"]}, } mapping = _userParamFromLegacyParam(legacy) assert mapping.visibleWhen == {"actionArg": "mode", "in": ["raw", "aggregate"]} class TestExtractVisibleWhen: def test_returnsNoneForMissingHint(self): assert _extractVisibleWhen(None) is None assert _extractVisibleWhen({}) is None assert _extractVisibleWhen({"dependsOn": "x"}) is None def test_normalizesScalarShowWhen(self): out = _extractVisibleWhen({"dependsOn": "entity", "showWhen": "tenant"}) assert out == {"actionArg": "entity", "in": ["tenant"]} class TestProjectInputAccepts: def test_perPortAcceptsList(self): node = _legacyMethodNode() assert _projectInputAccepts(node) == [["DocumentList", "Transit"]] def test_emptyForZeroInputs(self): node = _legacyMethodNode(inputs=0, inputPorts={}) assert _projectInputAccepts(node) == [] def test_handlesStringKeys(self): node = _legacyMethodNode(inputPorts={"0": {"accepts": ["Transit"]}}) assert _projectInputAccepts(node) == [["Transit"]] def test_missingPortReturnsEmptyList(self): node = _legacyMethodNode(inputs=2, inputPorts={0: {"accepts": ["Transit"]}}) assert _projectInputAccepts(node) == [["Transit"], []] class TestAdapterFromLegacyNode: def test_buildsAdapter(self): adapter = _adapterFromLegacyNode(_legacyMethodNode()) assert isinstance(adapter, NodeAdapter) assert adapter.nodeId == "trustee.processDocuments" assert adapter.bindsAction == "trustee.processDocuments" assert adapter.category == "trustee" assert len(adapter.userParams) == 2 assert adapter.userParams[0].actionArg == "documentList" assert adapter.inputAccepts == [["DocumentList", "Transit"]] assert adapter.contextParams == {} assert adapter.meta.get("icon") == "mdi-x" def test_returnsNoneForPrimitive(self): assert _adapterFromLegacyNode(_primitiveNode()) is None class TestProjectAllAdapters: def test_skipsPrimitives(self): nodes = [_legacyMethodNode(), _primitiveNode()] out = _projectAllAdapters(nodes) assert list(out.keys()) == ["trustee.processDocuments"]