Files
lda-wf/tests/core/test_input_sources.py
T

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,
walk_expression_paths,
)
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,
)
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)