109 lines
3.6 KiB
Python
109 lines
3.6 KiB
Python
"""Source-resolver seam: one traversal, distinct graph/schedule models (T03)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from wf_core.errors import WorkflowExecutionError
|
|
from wf_core.models.input_bindings import (
|
|
ObjectExpression,
|
|
PathExpression,
|
|
validate_input_expression_limits,
|
|
)
|
|
from wf_core.runtime.input_bindings import (
|
|
resolve_input_expression,
|
|
resolve_input_expression_with_resolver,
|
|
)
|
|
from wf_core.runtime.input_sources import (
|
|
GraphSourceResolver,
|
|
MappingSourceResolver,
|
|
resolve_composed_expression,
|
|
walk_expression_paths,
|
|
)
|
|
|
|
|
|
def test_graph_resolver_matches_legacy_resolution() -> None:
|
|
expr = ObjectExpression.model_validate(
|
|
{
|
|
"kind": "object",
|
|
"fields": {
|
|
"team": {"kind": "literal", "value": "eng"},
|
|
"tags": {"kind": "array", "items": [{"kind": "literal", "value": "a"}]},
|
|
"req": {"kind": "path", "path": "input.request_id"},
|
|
},
|
|
}
|
|
)
|
|
kwargs = {
|
|
"state": {},
|
|
"workflow_input": {"request_id": "r1"},
|
|
"context": {},
|
|
"label": "probe",
|
|
"location": "$",
|
|
}
|
|
assert resolve_input_expression(expr, **kwargs) == {
|
|
"team": "eng",
|
|
"tags": ["a"],
|
|
"req": "r1",
|
|
}
|
|
resolver = GraphSourceResolver(
|
|
state={}, workflow_input={"request_id": "r1"}, context={}
|
|
)
|
|
assert resolve_composed_expression(
|
|
expr, resolver=resolver, label="probe", location="$"
|
|
) == {"team": "eng", "tags": ["a"], "req": "r1"}
|
|
assert resolve_input_expression_with_resolver(
|
|
expr, resolver=resolver, label="probe", location="$"
|
|
) == {"team": "eng", "tags": ["a"], "req": "r1"}
|
|
|
|
|
|
def test_graph_resolver_does_not_smuggle_occurrence_values() -> None:
|
|
# Occurrence values must not be faked through graph context: a path that
|
|
# looks like an occurrence root is rejected by GraphSourcePath itself,
|
|
# and a resolver carrying occurrence data under context is a contract
|
|
# violation, not a supported seam.
|
|
from wf_core.paths import GraphSourcePath
|
|
|
|
with pytest.raises(ValueError):
|
|
GraphSourcePath.parse("occurrence.scheduled_at")
|
|
resolver = GraphSourceResolver(state={}, workflow_input={}, context={})
|
|
expr = PathExpression.model_validate({"kind": "path", "path": "input.missing"})
|
|
with pytest.raises(WorkflowExecutionError):
|
|
resolve_composed_expression(expr, resolver=resolver, label="t", location="$")
|
|
|
|
|
|
def test_mapping_resolver_is_distinct_from_graph_resolver() -> None:
|
|
occ = MappingSourceResolver(
|
|
{"schedule_id": "s", "scheduled_at": "2026-09-08T13:00:00+00:00"}
|
|
)
|
|
assert occ.resolve_field("schedule_id") == "s"
|
|
with pytest.raises(WorkflowExecutionError, match="unknown occurrence field"):
|
|
occ.resolve_field("nope")
|
|
assert not hasattr(occ, "resolve_path")
|
|
|
|
|
|
def test_walk_order_matches_validation_suffixes() -> None:
|
|
expr = ObjectExpression.model_validate(
|
|
{
|
|
"kind": "object",
|
|
"fields": {
|
|
"a": {"kind": "path", "path": "input.x"},
|
|
"b": {
|
|
"kind": "array",
|
|
"items": [{"kind": "path", "path": "state.y"}],
|
|
},
|
|
},
|
|
}
|
|
)
|
|
assert [suffix for _, suffix in walk_expression_paths(expr)] == [
|
|
"fields.a.path",
|
|
"fields.b.items[0].path",
|
|
]
|
|
|
|
|
|
def test_budget_validator_is_shared_entry_point() -> None:
|
|
deep: dict = {"kind": "literal", "value": 0}
|
|
for _ in range(70):
|
|
deep = {"kind": "array", "items": [deep]}
|
|
with pytest.raises(ValueError, match="limit exceeded"):
|
|
validate_input_expression_limits(deep)
|