189 lines
7.1 KiB
Python
189 lines
7.1 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
"""
|
|
Phase-5 Schicht-4 integration test (T11): the typed-bindings pipeline must
|
|
produce identical action-call parameters whether a workflow stores
|
|
``featureInstanceId`` as a legacy raw UUID or as a typed
|
|
``FeatureInstanceRef`` envelope.
|
|
|
|
The pipeline under test::
|
|
|
|
saved graph
|
|
-> materializeFeatureInstanceRefs (Phase-5, this test)
|
|
-> materializeConnectionRefs (existing pick-not-push helper)
|
|
-> resolveParameterReferences (typed bindings + envelope unwrap)
|
|
-> action params (what the action implementation would receive)
|
|
|
|
This is the integration counterpart to the focused unit tests in
|
|
``tests/unit/workflows/test_featureInstanceRefMigration.py``.
|
|
|
|
Plan: ``wiki/c-work/1-plan/2026-04-typed-action-architecture.md``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from typing import Any, Dict
|
|
|
|
import pytest
|
|
|
|
from modules.workflows.automation2.featureInstanceRefMigration import (
|
|
materializeFeatureInstanceRefs,
|
|
)
|
|
from modules.workflows.automation2.graphUtils import resolveParameterReferences
|
|
from modules.workflows.automation2.pickNotPushMigration import materializeConnectionRefs
|
|
|
|
|
|
_TRUSTEE_INSTANCE_UUID = "f1e2d3c4-b5a6-7890-1234-567890abcdef"
|
|
|
|
|
|
def _resolveActionParams(graph: Dict[str, Any], nodeId: str) -> Dict[str, Any]:
|
|
"""Apply the full Schicht-4 pipeline and return the resolved action params
|
|
that ``ActionNodeExecutor`` would forward to ``ActionExecutor.executeAction``."""
|
|
g = materializeFeatureInstanceRefs(graph)
|
|
g = materializeConnectionRefs(g)
|
|
targetNode = next(n for n in g["nodes"] if n["id"] == nodeId)
|
|
rawParams = dict(targetNode.get("parameters") or {})
|
|
return resolveParameterReferences(rawParams, nodeOutputs={})
|
|
|
|
|
|
def _legacyTrusteeGraph() -> Dict[str, Any]:
|
|
"""Trustee Spesenbelege-shape graph with raw UUIDs (pre-migration)."""
|
|
return {
|
|
"nodes": [
|
|
{"id": "n1", "type": "trigger.manual", "parameters": {}},
|
|
{
|
|
"id": "n5",
|
|
"type": "trustee.extractFromFiles",
|
|
"parameters": {
|
|
"featureInstanceId": _TRUSTEE_INSTANCE_UUID,
|
|
"prompt": "extract expenses",
|
|
},
|
|
},
|
|
{
|
|
"id": "n6",
|
|
"type": "trustee.processDocuments",
|
|
"parameters": {
|
|
"featureInstanceId": _TRUSTEE_INSTANCE_UUID,
|
|
"documentList": {
|
|
"type": "ref",
|
|
"nodeId": "n5",
|
|
"path": ["documents"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"id": "n7",
|
|
"type": "trustee.syncToAccounting",
|
|
"parameters": {
|
|
"featureInstanceId": _TRUSTEE_INSTANCE_UUID,
|
|
"documentList": {
|
|
"type": "ref",
|
|
"nodeId": "n6",
|
|
"path": ["documents"],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
"connections": [
|
|
{"source": "n1", "target": "n5"},
|
|
{"source": "n5", "target": "n6"},
|
|
{"source": "n6", "target": "n7"},
|
|
],
|
|
}
|
|
|
|
|
|
def _migratedTrusteeGraph() -> Dict[str, Any]:
|
|
"""The same graph but already in the migrated (typed envelope) shape."""
|
|
g = _legacyTrusteeGraph()
|
|
envelope = {
|
|
"$type": "FeatureInstanceRef",
|
|
"id": _TRUSTEE_INSTANCE_UUID,
|
|
"featureCode": "trustee",
|
|
}
|
|
for node in g["nodes"]:
|
|
if node.get("type", "").startswith("trustee."):
|
|
node["parameters"]["featureInstanceId"] = copy.deepcopy(envelope)
|
|
return g
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Round-trip: legacy + migrated graphs produce identical action params
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTrusteeBindingsPipeline:
|
|
@pytest.mark.parametrize("nodeId", ["n5", "n6", "n7"])
|
|
def test_legacyAndMigratedGraphsResolveToSameFeatureInstanceId(self, nodeId):
|
|
legacyParams = _resolveActionParams(_legacyTrusteeGraph(), nodeId)
|
|
migratedParams = _resolveActionParams(_migratedTrusteeGraph(), nodeId)
|
|
assert legacyParams["featureInstanceId"] == _TRUSTEE_INSTANCE_UUID
|
|
assert migratedParams["featureInstanceId"] == _TRUSTEE_INSTANCE_UUID
|
|
assert legacyParams == migratedParams
|
|
|
|
def test_legacyGraphIsConvertedToTypedEnvelopeInPlaceOfRawUuid(self):
|
|
legacy = _legacyTrusteeGraph()
|
|
migrated = materializeFeatureInstanceRefs(legacy)
|
|
for node in migrated["nodes"]:
|
|
if not node.get("type", "").startswith("trustee."):
|
|
continue
|
|
param = node["parameters"]["featureInstanceId"]
|
|
assert isinstance(param, dict), f"node {node['id']} not migrated"
|
|
assert param["$type"] == "FeatureInstanceRef"
|
|
assert param["id"] == _TRUSTEE_INSTANCE_UUID
|
|
assert param["featureCode"] == "trustee"
|
|
|
|
def test_migrationIsIdempotentAcrossPipeline(self):
|
|
once = materializeFeatureInstanceRefs(_legacyTrusteeGraph())
|
|
twice = materializeFeatureInstanceRefs(once)
|
|
assert once == twice
|
|
|
|
def test_otherParamsArePreservedAcrossMigration(self):
|
|
legacy = _legacyTrusteeGraph()
|
|
migrated = materializeFeatureInstanceRefs(legacy)
|
|
n5 = next(n for n in migrated["nodes"] if n["id"] == "n5")
|
|
assert n5["parameters"]["prompt"] == "extract expenses"
|
|
n6 = next(n for n in migrated["nodes"] if n["id"] == "n6")
|
|
# documentList DataRef must survive untouched (only the
|
|
# featureInstanceId key is rewritten).
|
|
assert n6["parameters"]["documentList"] == {
|
|
"type": "ref",
|
|
"nodeId": "n5",
|
|
"path": ["documents"],
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cross-feature: same migration handles redmine / clickup / sharepoint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCrossFeatureMigration:
|
|
@pytest.mark.parametrize(
|
|
"nodeType,expectedCode",
|
|
[
|
|
("redmine.createIssue", "redmine"),
|
|
("clickup.createTask", "clickup"),
|
|
("sharepoint.listFiles", "sharepoint"),
|
|
],
|
|
)
|
|
def test_nonTrusteeNodesAreMigratedWithCorrectFeatureCode(
|
|
self, nodeType, expectedCode
|
|
):
|
|
graph = {
|
|
"nodes": [
|
|
{
|
|
"id": "n",
|
|
"type": nodeType,
|
|
"parameters": {"featureInstanceId": "uuid-x"},
|
|
}
|
|
]
|
|
}
|
|
out = materializeFeatureInstanceRefs(graph)
|
|
env = out["nodes"][0]["parameters"]["featureInstanceId"]
|
|
assert env == {
|
|
"$type": "FeatureInstanceRef",
|
|
"id": "uuid-x",
|
|
"featureCode": expectedCode,
|
|
}
|
|
# And the resolver still hands back the raw UUID for legacy actions.
|
|
resolved = resolveParameterReferences(env, nodeOutputs={})
|
|
assert resolved == "uuid-x"
|