sched: extract shared input traversal behind SourceResolver seam (T03)
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user