95 lines
4 KiB
Python
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)
|
|
)
|