# 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[]`` 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[]``; 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[]: " + ", ".join(offenders) )