gateway/tests/unit/graphicalEditor/test_featureInstanceRef_node_definitions.py
2026-04-26 08:31:35 +02:00

95 lines
4 KiB
Python

# Copyright (c) 2026 Patrick Motsch
# All rights reserved.
"""
Schicht-4 / Phase-5 follow-up: assert that all Trustee + Redmine node
definitions expose ``featureInstanceId`` as a typed
``FeatureInstanceRef[<code>]`` parameter rendered by the dedicated
``featureInstance`` frontend renderer.
Background
----------
The legacy adapter shape used ``type: "string"`` + ``frontendType: "hidden"``
for the mandate binding. That produced two visible defects:
- the Graph Editor banner reported ``Pflichtfeld ohne Quelle`` for an
invisible parameter (no UI surface, no resolution path), and
- the DataPicker could not type-filter compatible upstream candidates.
The Typed Action Architecture ships a dedicated catalog type
(``FeatureInstanceRef``) and a discriminator notation
``FeatureInstanceRef[<featureCode>]``; this test guards the migration so
nobody silently re-introduces the legacy shape. See
``wiki/c-work/2-build/2026-04-feature-instance-ref-adapter-migration.md``.
"""
from __future__ import annotations
import pytest
from modules.features.graphicalEditor.nodeDefinitions.redmine import REDMINE_NODES
from modules.features.graphicalEditor.nodeDefinitions.trustee import TRUSTEE_NODES
def _featureInstanceParam(node: dict) -> dict | None:
for param in node.get("parameters", []):
if param.get("name") == "featureInstanceId":
return param
return None
@pytest.mark.parametrize("node", TRUSTEE_NODES, ids=lambda n: n["id"])
def test_trusteeNodesUseTypedFeatureInstanceRef(node: dict) -> None:
"""Every Trustee node must bind its mandate via the typed catalog ref."""
param = _featureInstanceParam(node)
assert param is not None, f"{node['id']} is missing a featureInstanceId parameter"
assert param["type"] == "FeatureInstanceRef[trustee]", (
f"{node['id']}.featureInstanceId.type must be 'FeatureInstanceRef[trustee]', "
f"got {param['type']!r}"
)
assert param.get("frontendType") == "featureInstance", (
f"{node['id']}.featureInstanceId.frontendType must be 'featureInstance', "
f"got {param.get('frontendType')!r}"
)
assert param.get("required") is True
assert (param.get("frontendOptions") or {}).get("featureCode") == "trustee"
@pytest.mark.parametrize("node", REDMINE_NODES, ids=lambda n: n["id"])
def test_redmineNodesUseTypedFeatureInstanceRef(node: dict) -> None:
"""Every Redmine node must bind its mandate via the typed catalog ref."""
param = _featureInstanceParam(node)
assert param is not None, f"{node['id']} is missing a featureInstanceId parameter"
assert param["type"] == "FeatureInstanceRef[redmine]", (
f"{node['id']}.featureInstanceId.type must be 'FeatureInstanceRef[redmine]', "
f"got {param['type']!r}"
)
assert param.get("frontendType") == "featureInstance", (
f"{node['id']}.featureInstanceId.frontendType must be 'featureInstance', "
f"got {param.get('frontendType')!r}"
)
assert param.get("required") is True
assert (param.get("frontendOptions") or {}).get("featureCode") == "redmine"
@pytest.mark.parametrize(
"nodes",
[TRUSTEE_NODES, REDMINE_NODES],
ids=["trustee", "redmine"],
)
def test_noLegacyHiddenStringFeatureInstanceParam(nodes: list[dict]) -> None:
"""Regression guard: the legacy ``string + hidden`` shape must be gone.
A hidden+required parameter produces a phantom error in the editor
banner (`findRequiredErrors` filters them out as a safety net, but the
correct fix is to remove them at the source).
"""
offenders = []
for node in nodes:
param = _featureInstanceParam(node)
if param is None:
continue
legacyShape = param.get("type") == "string" and param.get("frontendType") == "hidden"
if legacyShape:
offenders.append(node["id"])
assert offenders == [], (
"These nodes still use the legacy 'string + hidden' featureInstanceId "
"shape; migrate them to FeatureInstanceRef[<featureCode>]: " + ", ".join(offenders)
)