docs: specify deployment scheduling and verify implementation plan
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# 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")
|
||||
Reference in New Issue
Block a user