139 lines
5.4 KiB
Python
139 lines
5.4 KiB
Python
# 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")
|