gateway/tests/unit/graphicalEditor/test_node_adapter.py
2026-04-25 01:13:01 +02:00

170 lines
6 KiB
Python

# 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"]