"""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)