gateway/tests/unit/workflows/test_automation2_graphUtils.py
2026-04-29 21:27:08 +02:00

177 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""
Unit tests for automation2 graphUtils - resolveParameterReferences (ref/value format).
"""
import pytest
from modules.workflows.automation2.graphUtils import resolveParameterReferences
class TestResolveParameterReferences:
"""Test structured ref/value resolution."""
def test_ref_simple(self):
node_outputs = {
"n1": {"payload": {"country": "CH"}},
}
value = {"type": "ref", "nodeId": "n1", "path": ["payload", "country"]}
assert resolveParameterReferences(value, node_outputs) == "CH"
def test_ref_root(self):
node_outputs = {"n1": {"a": 1, "b": 2}}
value = {"type": "ref", "nodeId": "n1", "path": []}
assert resolveParameterReferences(value, node_outputs) == {"a": 1, "b": 2}
def test_ref_nested(self):
node_outputs = {"form_1": {"customer": {"country": "DE", "name": "Test"}}}
value = {"type": "ref", "nodeId": "form_1", "path": ["customer", "country"]}
assert resolveParameterReferences(value, node_outputs) == "DE"
def test_ref_array_index(self):
node_outputs = {"n1": {"items": ["a", "b", "c"]}}
value = {"type": "ref", "nodeId": "n1", "path": ["items", 1]}
assert resolveParameterReferences(value, node_outputs) == "b"
def test_ref_missing_node(self):
# Current runtime semantics: an unresolved ref (nodeId not in
# node_outputs) collapses to None rather than the original
# placeholder dict. The workflow engine relies on this — downstream
# nodes treat missing refs as "no value yet" rather than "literal
# placeholder" — so we lock the contract here.
node_outputs = {}
value = {"type": "ref", "nodeId": "missing", "path": ["x"]}
assert resolveParameterReferences(value, node_outputs) is None
def test_value_wrapper(self):
value = {"type": "value", "value": "static text"}
assert resolveParameterReferences(value, {}) == "static text"
def test_value_nested_ref(self):
node_outputs = {"n1": {"x": 42}}
value = {"type": "value", "value": {"type": "ref", "nodeId": "n1", "path": ["x"]}}
assert resolveParameterReferences(value, node_outputs) == 42
def test_dict_mixed_ref_value(self):
node_outputs = {"n1": {"result": "hello"}}
value = {
"prompt": {"type": "ref", "nodeId": "n1", "path": ["result"]},
"suffix": {"type": "value", "value": " world"},
}
result = resolveParameterReferences(value, node_outputs)
assert result == {"prompt": "hello", "suffix": " world"}
def test_legacy_string_template(self):
node_outputs = {"n1": {"country": "CH"}}
value = "Land: {{n1.country}}"
assert resolveParameterReferences(value, node_outputs) == "Land: CH"
def test_legacy_string_template_loop_current_item_nested(self):
"""Same shape as executionEngine sets on loop node id during body iteration."""
node_outputs = {
"loop93": {
"currentItem": {"subject": "Hello", "body": {"content": "World"}},
"currentIndex": 0,
},
}
value = "Subj: {{loop93.currentItem.subject}} Body: {{loop93.currentItem.body.content}}"
assert resolveParameterReferences(value, node_outputs) == "Subj: Hello Body: World"
class TestWildcardIteration:
"""Phase-4 typed Bindings-Resolver: ``*`` segment iterates over a list.
Path semantics:
["docs", "*", "name"] ⇒ map "name" over each item in docs
["docs", "*"] ⇒ the docs list itself (after passing through *)
Drops items whose remainder resolves to ``None`` (missing field).
"""
def test_wildcard_maps_over_list_to_field(self):
node_outputs = {
"src": {
"documents": [
{"name": "a.pdf", "size": 10},
{"name": "b.pdf", "size": 20},
],
}
}
value = {
"type": "ref",
"nodeId": "src",
"path": ["documents", "*", "name"],
}
assert resolveParameterReferences(value, node_outputs) == ["a.pdf", "b.pdf"]
def test_wildcard_terminal_returns_list_copy(self):
node_outputs = {"src": {"items": ["x", "y", "z"]}}
value = {"type": "ref", "nodeId": "src", "path": ["items", "*"]}
assert resolveParameterReferences(value, node_outputs) == ["x", "y", "z"]
def test_wildcard_drops_missing_fields(self):
node_outputs = {
"src": {
"rows": [
{"name": "a"},
{"otherField": 1},
{"name": "c"},
]
}
}
value = {"type": "ref", "nodeId": "src", "path": ["rows", "*", "name"]}
assert resolveParameterReferences(value, node_outputs) == ["a", "c"]
def test_wildcard_on_non_list_returns_none(self):
node_outputs = {"src": {"docs": {"not": "a list"}}}
value = {"type": "ref", "nodeId": "src", "path": ["docs", "*", "name"]}
assert resolveParameterReferences(value, node_outputs) is None
def test_wildcard_nested(self):
node_outputs = {
"src": {
"groups": [
{"items": [{"v": 1}, {"v": 2}]},
{"items": [{"v": 3}]},
]
}
}
value = {
"type": "ref",
"nodeId": "src",
"path": ["groups", "*", "items", "*", "v"],
}
assert resolveParameterReferences(value, node_outputs) == [[1, 2], [3]]
def test_wildcard_inside_transit_envelope(self):
node_outputs = {
"src": {
"_transit": True,
"data": {"documents": [{"name": "p.pdf"}, {"name": "q.pdf"}]},
}
}
value = {
"type": "ref",
"nodeId": "src",
"path": ["documents", "*", "name"],
}
assert resolveParameterReferences(value, node_outputs) == ["p.pdf", "q.pdf"]
class TestPathContainsWildcard:
"""``_pathContainsWildcard`` lets the engine decide between a scalar bind
and an iteration target (e.g. wrap a Loop container around the consumer).
"""
def test_detects_wildcard(self):
from modules.workflows.automation2.graphUtils import _pathContainsWildcard
assert _pathContainsWildcard(["docs", "*", "name"]) is True
assert _pathContainsWildcard(["*"]) is True
def test_no_wildcard(self):
from modules.workflows.automation2.graphUtils import _pathContainsWildcard
assert _pathContainsWildcard(["docs", 0, "name"]) is False
assert _pathContainsWildcard([]) is False
def test_literal_star_in_int_segment_does_not_match(self):
from modules.workflows.automation2.graphUtils import _pathContainsWildcard
assert _pathContainsWildcard([1, 2, 3]) is False