# DISPOSABLE EXPRESSION-CONTRACT PROBE — NOT PRODUCTION CODE. # See README.md in this directory. Runs in the repo env: # uv run pytest -q probes/deployment_scheduling_verify/test_expression_contract_probe.py """Pin the CURRENT input-expression contract that the scheduling slice must reuse (spec: Input authoring and serialization). These tests document what exists today for the Phase 1 implementer: the closed 4-kind union, budget enforcement point, strict-JSON literals, target-conflict detection, closed GraphSourcePath roots, and the single hardcoded graph-context evaluator. A schedule occurrence source must plug into this traversal (T03/T04 of the implementation plan), not copy it. """ from __future__ import annotations import pytest from pydantic import ValidationError from wf_core.local_paths import has_overlapping_paths from wf_core.models.input_bindings import ( MAX_INPUT_EXPRESSION_DEPTH, MAX_INPUT_EXPRESSION_NODES, ArrayExpression, InputExpressionBinding, LiteralExpression, ObjectExpression, PathExpression, validate_input_expression_limits, ) from wf_core.models.json_values import validate_strict_json_value from wf_core.paths import GraphSourcePath from wf_core.runtime.input_bindings import ( resolve_input_expression, resolve_step_input_bindings, ) def test_expression_union_is_closed_to_four_kinds(): assert LiteralExpression(kind="literal", value=1).kind == "literal" assert PathExpression(kind="path", path="input.a").kind == "path" assert ArrayExpression(kind="array", items=[]).kind == "array" assert ObjectExpression(kind="object", fields={}).kind == "object" with pytest.raises(ValidationError): InputExpressionBinding( target="x", expression={"kind": "occurrence", "field": "scheduled_at"} ) # type: ignore[dict-item] print("OBSERVED occurrence kind rejected: union closed to 4 kinds") def test_budget_constants_and_validator_entry_point(): assert (MAX_INPUT_EXPRESSION_DEPTH, MAX_INPUT_EXPRESSION_NODES) == (64, 1024) deep: dict = {"kind": "literal", "value": 0} for _ in range(MAX_INPUT_EXPRESSION_DEPTH + 5): deep = {"kind": "array", "items": [deep]} with pytest.raises(ValueError, match="limit exceeded"): validate_input_expression_limits(deep) # type: ignore[arg-type] print("OBSERVED depth budget enforced by validate_input_expression_limits") def test_strict_json_rejects_non_finite_and_non_string_keys(): with pytest.raises(ValueError): validate_strict_json_value(float("inf")) with pytest.raises(ValueError): validate_strict_json_value({1: "x"}) assert validate_strict_json_value({"a": [1, None, "x"]}) == {"a": [1, None, "x"]} print("OBSERVED strict-JSON validator rejects inf and non-string keys") def test_target_conflicts_detected_on_local_paths(): assert has_overlapping_paths(["a.b", "a.b.c"]) assert not has_overlapping_paths(["a.b", "a.c"]) print("OBSERVED overlapping local-path targets detected") def test_graph_source_roots_closed_to_input_state_context(): assert GraphSourcePath.parse("input.a").root == "input" assert GraphSourcePath.parse("state.a").root == "state" assert GraphSourcePath.parse("context.a").root == "context" with pytest.raises(ValueError): GraphSourcePath.parse("occurrence.scheduled_at") print("OBSERVED occurrence root rejected: GraphSourcePath closed") def test_runtime_resolver_composes_literal_object_array_and_paths(): expr = ObjectExpression( kind="object", fields={ "team": LiteralExpression(kind="literal", value="eng"), "tags": ArrayExpression( kind="array", items=[LiteralExpression(kind="literal", value="a")], ), "req": PathExpression(kind="path", path="input.request_id"), }, ) resolved = resolve_input_expression( expr, state={}, workflow_input={"request_id": "r1"}, context={}, label="probe", location="$", ) assert resolved == {"team": "eng", "tags": ["a"], "req": "r1"} print(f"OBSERVED composed resolution -> {resolved}") def test_runtime_resolver_takes_only_graph_context_mappings(): # There is no source-resolver seam: the only injection point is the # concrete state/workflow_input/context mappings (faking occurrence # values through context is exactly what the spec forbids). import inspect sig = inspect.signature(resolve_input_expression) assert list(sig.parameters) == [ "expression", "state", "workflow_input", "context", "label", "location", ], f"no resolver parameter exists: {list(sig.parameters)}" assert "resolver" not in inspect.signature(resolve_step_input_bindings).parameters print("OBSERVED resolver signatures are concrete graph mappings; no seam") def test_node_input_binding_rejects_expression_kind_at_top_level(): # StepInputBinding allows expressions, but plain InputBinding # (deployment-level inputs) does not carry them — resolved data only. from pydantic import TypeAdapter from wf_core.models.input_bindings import InputBinding with pytest.raises(ValidationError): TypeAdapter(InputBinding).validate_python( {"target": "x", "expression": {"kind": "literal", "value": 1}} ) print("OBSERVED top-level InputBinding carries no expressions")