#!/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" 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