fmt
This commit is contained in:
@@ -126,16 +126,18 @@ def test_output_bindings_validate_exact_state_schema_before_mutation() -> None:
|
||||
|
||||
def test_output_bindings_validate_declared_parent_schema_before_mutation() -> None:
|
||||
workflow = _workflow_from_state_schema(
|
||||
StateSchema.model_validate({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"additionalProperties": False,
|
||||
}
|
||||
},
|
||||
})
|
||||
StateSchema.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"additionalProperties": False,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
state = {"person": {"name": "old"}}
|
||||
|
||||
@@ -204,9 +206,9 @@ def _workflow_with_node() -> Workflow:
|
||||
return Workflow(
|
||||
name="canonical_output",
|
||||
input_schema=SchemaRef(type="object", properties={}),
|
||||
state_schema=StateSchema.from_field_map({
|
||||
"person.name": StateField(type="string")
|
||||
}),
|
||||
state_schema=StateSchema.from_field_map(
|
||||
{"person.name": StateField(type="string")}
|
||||
),
|
||||
output_schema=SchemaRef(
|
||||
type="object", properties={"person": {"type": "object"}}
|
||||
),
|
||||
@@ -223,12 +225,16 @@ def _workflow_with_node() -> Workflow:
|
||||
],
|
||||
start="rename",
|
||||
nodes=[
|
||||
NodeUse.model_validate({
|
||||
"id": "rename",
|
||||
"type": "node",
|
||||
"node": "rename",
|
||||
"output": [{"source": "person.name", "target": "state.person.name"}],
|
||||
})
|
||||
NodeUse.model_validate(
|
||||
{
|
||||
"id": "rename",
|
||||
"type": "node",
|
||||
"node": "rename",
|
||||
"output": [
|
||||
{"source": "person.name", "target": "state.person.name"}
|
||||
],
|
||||
}
|
||||
)
|
||||
],
|
||||
edges=[Edge.model_validate({"from": "rename", "outcome": "ok", "to": END})],
|
||||
)
|
||||
|
||||
@@ -118,18 +118,15 @@ def test_canonical_binding_json_schema_describes_nested_fields():
|
||||
input_value = defs["InputValueBinding"]
|
||||
output = defs["OutputBinding"]
|
||||
|
||||
assert "whole node input payload" in input_path["properties"]["target"][
|
||||
"description"
|
||||
]
|
||||
assert "input, state, or context" in input_path["properties"]["path"][
|
||||
"description"
|
||||
]
|
||||
assert "Literal JSON-compatible value" in input_value["properties"]["value"][
|
||||
"description"
|
||||
]
|
||||
assert "whole node output payload" in output["properties"]["source"][
|
||||
"description"
|
||||
]
|
||||
assert (
|
||||
"whole node input payload" in input_path["properties"]["target"]["description"]
|
||||
)
|
||||
assert "input, state, or context" in input_path["properties"]["path"]["description"]
|
||||
assert (
|
||||
"Literal JSON-compatible value"
|
||||
in input_value["properties"]["value"]["description"]
|
||||
)
|
||||
assert "whole node output payload" in output["properties"]["source"]["description"]
|
||||
assert "Bare state is invalid" in output["properties"]["target"]["description"]
|
||||
|
||||
|
||||
|
||||
@@ -178,10 +178,12 @@ def _workflow(
|
||||
|
||||
return Workflow(
|
||||
name="mapping_validation",
|
||||
input_schema=SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {"person": {"type": "object"}},
|
||||
}),
|
||||
input_schema=SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"person": {"type": "object"}},
|
||||
}
|
||||
),
|
||||
state_schema=StateSchema.from_field_map(
|
||||
state_fields or {"person": StateField(type="object")}
|
||||
),
|
||||
@@ -189,14 +191,18 @@ def _workflow(
|
||||
node_defs=[
|
||||
NodeDef(
|
||||
name="tool",
|
||||
input_schema=SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {"user": {"type": "object"}},
|
||||
}),
|
||||
output_schema=SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {"user": {"type": "object"}},
|
||||
}),
|
||||
input_schema=SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"user": {"type": "object"}},
|
||||
}
|
||||
),
|
||||
output_schema=SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"user": {"type": "object"}},
|
||||
}
|
||||
),
|
||||
outcomes=["ok"],
|
||||
)
|
||||
],
|
||||
|
||||
+131
-109
@@ -19,52 +19,54 @@ from wf_core import (
|
||||
|
||||
|
||||
def test_canonical_bindings_resolve_input_values_paths_and_explicit_null() -> None:
|
||||
workflow = Workflow.model_validate({
|
||||
"name": "canonical",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"message": {"type": "string"}},
|
||||
},
|
||||
"state_schema": {"fields": {"echoed": {"type": "string"}}},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
},
|
||||
"start": "echo",
|
||||
"node_defs": [
|
||||
{
|
||||
"name": "echo",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "string"},
|
||||
"mode": {"type": "string"},
|
||||
"maybe": {"type": "null"},
|
||||
workflow = Workflow.model_validate(
|
||||
{
|
||||
"name": "canonical",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"message": {"type": "string"}},
|
||||
},
|
||||
"state_schema": {"fields": {"echoed": {"type": "string"}}},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
},
|
||||
"start": "echo",
|
||||
"node_defs": [
|
||||
{
|
||||
"name": "echo",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "string"},
|
||||
"mode": {"type": "string"},
|
||||
"maybe": {"type": "null"},
|
||||
},
|
||||
"required": ["message", "mode", "maybe"],
|
||||
},
|
||||
"required": ["message", "mode", "maybe"],
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
},
|
||||
"outcomes": ["ok"],
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "echo",
|
||||
"type": "node",
|
||||
"node": "echo",
|
||||
"input": [
|
||||
{"target": "message", "path": "input.message"},
|
||||
{"target": "mode", "value": "fast"},
|
||||
{"target": "maybe", "value": None},
|
||||
],
|
||||
"output": [{"source": "echoed", "target": "state.echoed"}],
|
||||
}
|
||||
],
|
||||
"edges": [{"from": "echo", "outcome": "ok", "to": END}],
|
||||
})
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
},
|
||||
"outcomes": ["ok"],
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "echo",
|
||||
"type": "node",
|
||||
"node": "echo",
|
||||
"input": [
|
||||
{"target": "message", "path": "input.message"},
|
||||
{"target": "mode", "value": "fast"},
|
||||
{"target": "maybe", "value": None},
|
||||
],
|
||||
"output": [{"source": "echoed", "target": "state.echoed"}],
|
||||
}
|
||||
],
|
||||
"edges": [{"from": "echo", "outcome": "ok", "to": END}],
|
||||
}
|
||||
)
|
||||
run = execute_workflow(
|
||||
workflow,
|
||||
{"message": "hi"},
|
||||
@@ -128,10 +130,12 @@ def test_missing_nested_node_output_path_fails() -> None:
|
||||
def test_root_node_local_paths_map_whole_input_and_output_payloads() -> None:
|
||||
workflow = Workflow(
|
||||
name="root_mapping",
|
||||
input_schema=SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {"rates": {"type": "object"}},
|
||||
}),
|
||||
input_schema=SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"rates": {"type": "object"}},
|
||||
}
|
||||
),
|
||||
state_schema=StateSchema.from_field_map({"rates": StateField(type="object")}),
|
||||
output_schema=SchemaRef(type="object", properties={}),
|
||||
node_defs=[
|
||||
@@ -152,13 +156,15 @@ def test_root_node_local_paths_map_whole_input_and_output_payloads() -> None:
|
||||
nodes=[
|
||||
cast(
|
||||
Any,
|
||||
NodeUse.model_validate({
|
||||
"id": "force",
|
||||
"type": "node",
|
||||
"node": "force_rates",
|
||||
"in_map": {"input.rates": "."},
|
||||
"out_map": {".": "state.rates"},
|
||||
}),
|
||||
NodeUse.model_validate(
|
||||
{
|
||||
"id": "force",
|
||||
"type": "node",
|
||||
"node": "force_rates",
|
||||
"in_map": {"input.rates": "."},
|
||||
"out_map": {".": "state.rates"},
|
||||
}
|
||||
),
|
||||
)
|
||||
],
|
||||
edges=[Edge.model_validate({"from": "force", "outcome": "ok", "to": END})],
|
||||
@@ -190,16 +196,20 @@ def test_static_input_values_are_merged_into_node_local_input() -> None:
|
||||
node_defs=[
|
||||
NodeDef(
|
||||
name="constant",
|
||||
input_schema=SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {"value": {"type": "string"}},
|
||||
"required": ["value"],
|
||||
}),
|
||||
output_schema=SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {"value": {"type": "string"}},
|
||||
"required": ["value"],
|
||||
}),
|
||||
input_schema=SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"value": {"type": "string"}},
|
||||
"required": ["value"],
|
||||
}
|
||||
),
|
||||
output_schema=SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"value": {"type": "string"}},
|
||||
"required": ["value"],
|
||||
}
|
||||
),
|
||||
outcomes=["ok"],
|
||||
)
|
||||
],
|
||||
@@ -207,13 +217,15 @@ def test_static_input_values_are_merged_into_node_local_input() -> None:
|
||||
nodes=[
|
||||
cast(
|
||||
Any,
|
||||
NodeUse.model_validate({
|
||||
"id": "constant",
|
||||
"type": "node",
|
||||
"node": "constant",
|
||||
"input_values": {"value": "CLICKED"},
|
||||
"out_map": {"value": "state.message"},
|
||||
}),
|
||||
NodeUse.model_validate(
|
||||
{
|
||||
"id": "constant",
|
||||
"type": "node",
|
||||
"node": "constant",
|
||||
"input_values": {"value": "CLICKED"},
|
||||
"out_map": {"value": "state.message"},
|
||||
}
|
||||
),
|
||||
)
|
||||
],
|
||||
edges=[Edge.model_validate({"from": "constant", "outcome": "ok", "to": END})],
|
||||
@@ -232,32 +244,40 @@ def test_static_input_values_are_merged_into_node_local_input() -> None:
|
||||
def _nested_mapping_workflow() -> Workflow:
|
||||
return Workflow(
|
||||
name="nested_mapping",
|
||||
input_schema=SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person": {"type": "object"},
|
||||
"digital": {"type": "object"},
|
||||
},
|
||||
}),
|
||||
state_schema=StateSchema.from_field_map({
|
||||
"person": StateField(type="object"),
|
||||
"experience": StateField(type="object"),
|
||||
}),
|
||||
input_schema=SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person": {"type": "object"},
|
||||
"digital": {"type": "object"},
|
||||
},
|
||||
}
|
||||
),
|
||||
state_schema=StateSchema.from_field_map(
|
||||
{
|
||||
"person": StateField(type="object"),
|
||||
"experience": StateField(type="object"),
|
||||
}
|
||||
),
|
||||
output_schema=SchemaRef(type="object", properties={}),
|
||||
node_defs=[
|
||||
NodeDef(
|
||||
name="big_tool",
|
||||
input_schema=SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {"user": {"type": "object"}},
|
||||
}),
|
||||
output_schema=SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {"type": "object"},
|
||||
"job": {"type": "object"},
|
||||
},
|
||||
}),
|
||||
input_schema=SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"user": {"type": "object"}},
|
||||
}
|
||||
),
|
||||
output_schema=SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {"type": "object"},
|
||||
"job": {"type": "object"},
|
||||
},
|
||||
}
|
||||
),
|
||||
outcomes=["ok"],
|
||||
)
|
||||
],
|
||||
@@ -265,20 +285,22 @@ def _nested_mapping_workflow() -> Workflow:
|
||||
nodes=[
|
||||
cast(
|
||||
Any,
|
||||
NodeUse.model_validate({
|
||||
"id": "big",
|
||||
"type": "node",
|
||||
"node": "big_tool",
|
||||
"in_map": {
|
||||
"input.person.name": "user.name",
|
||||
"input.digital.email": "user.email",
|
||||
},
|
||||
"out_map": {
|
||||
"user.age": "state.person.age",
|
||||
"user.gender": "state.person.gender",
|
||||
"job.years": "state.experience.years",
|
||||
},
|
||||
}),
|
||||
NodeUse.model_validate(
|
||||
{
|
||||
"id": "big",
|
||||
"type": "node",
|
||||
"node": "big_tool",
|
||||
"in_map": {
|
||||
"input.person.name": "user.name",
|
||||
"input.digital.email": "user.email",
|
||||
},
|
||||
"out_map": {
|
||||
"user.age": "state.person.age",
|
||||
"user.gender": "state.person.gender",
|
||||
"job.years": "state.experience.years",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
],
|
||||
edges=[Edge.model_validate({"from": "big", "outcome": "ok", "to": END})],
|
||||
|
||||
@@ -10,14 +10,16 @@ from wf_core.runtime.ops.schemas import validate_payload_against_schema
|
||||
|
||||
|
||||
def test_schema_validation_rejects_wrong_property_type() -> None:
|
||||
schema = SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"count": {"type": "integer"},
|
||||
},
|
||||
"required": ["name", "count"],
|
||||
})
|
||||
schema = SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"count": {"type": "integer"},
|
||||
},
|
||||
"required": ["name", "count"],
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(WorkflowExecutionError, match=r"count.*not of type 'integer'"):
|
||||
validate_payload_against_schema(
|
||||
@@ -28,17 +30,19 @@ def test_schema_validation_rejects_wrong_property_type() -> None:
|
||||
|
||||
|
||||
def test_schema_validation_rejects_nested_missing_required_field() -> None:
|
||||
schema = SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"properties": {"email": {"type": "string"}},
|
||||
"required": ["email"],
|
||||
}
|
||||
},
|
||||
"required": ["profile"],
|
||||
})
|
||||
schema = SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"properties": {"email": {"type": "string"}},
|
||||
"required": ["email"],
|
||||
}
|
||||
},
|
||||
"required": ["profile"],
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(WorkflowExecutionError, match=r"profile.*email.*required"):
|
||||
validate_payload_against_schema(
|
||||
@@ -49,31 +53,35 @@ def test_schema_validation_rejects_nested_missing_required_field() -> None:
|
||||
|
||||
|
||||
def test_schema_validation_accepts_valid_payload() -> None:
|
||||
schema = SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["tags"],
|
||||
})
|
||||
schema = SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["tags"],
|
||||
}
|
||||
)
|
||||
|
||||
validate_payload_against_schema(schema, {"tags": ["a", "b"]}, "node input")
|
||||
|
||||
|
||||
def test_schema_ref_accepts_and_preserves_schema_with_defs_and_ref() -> None:
|
||||
schema = SchemaRef.model_validate({
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$defs": {
|
||||
"tag": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"properties": {"tag": {"$ref": "#/$defs/tag"}},
|
||||
"required": ["tag"],
|
||||
})
|
||||
schema = SchemaRef.model_validate(
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$defs": {
|
||||
"tag": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"properties": {"tag": {"$ref": "#/$defs/tag"}},
|
||||
"required": ["tag"],
|
||||
}
|
||||
)
|
||||
|
||||
dumped = schema.model_dump(mode="json")
|
||||
|
||||
@@ -89,11 +97,13 @@ def test_schema_ref_rejects_invalid_json_schema_shape() -> None:
|
||||
|
||||
|
||||
def test_schema_ref_defaults_to_draft_2020_12_without_schema_keyword() -> None:
|
||||
schema = SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {"count": {"type": "integer"}},
|
||||
"required": ["count"],
|
||||
})
|
||||
schema = SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"count": {"type": "integer"}},
|
||||
"required": ["count"],
|
||||
}
|
||||
)
|
||||
|
||||
dumped = schema.model_dump(mode="json")
|
||||
|
||||
@@ -103,11 +113,13 @@ def test_schema_ref_defaults_to_draft_2020_12_without_schema_keyword() -> None:
|
||||
|
||||
|
||||
def test_schema_ref_preserves_extra_json_schema_keywords() -> None:
|
||||
schema = SchemaRef.model_validate({
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"additionalProperties": False,
|
||||
})
|
||||
schema = SchemaRef.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"additionalProperties": False,
|
||||
}
|
||||
)
|
||||
|
||||
dumped = schema.model_dump(mode="json")
|
||||
|
||||
@@ -125,10 +137,12 @@ def test_schema_ref_dump_omits_none_fields_and_stays_valid_json_schema() -> None
|
||||
|
||||
|
||||
def test_state_field_decl_dump_omits_nested_schema_none_fields() -> None:
|
||||
field = StateFieldDecl.model_validate({
|
||||
"path": "state.person",
|
||||
"schema": {"type": "object"},
|
||||
})
|
||||
field = StateFieldDecl.model_validate(
|
||||
{
|
||||
"path": "state.person",
|
||||
"schema": {"type": "object"},
|
||||
}
|
||||
)
|
||||
|
||||
dumped = field.model_dump(mode="json")
|
||||
|
||||
@@ -140,16 +154,18 @@ def test_state_field_decl_dump_omits_nested_schema_none_fields() -> None:
|
||||
def test_state_schema_dump_is_valid_json_schema_with_reducer_keyword() -> None:
|
||||
from wf_core import StateSchema
|
||||
|
||||
schema = StateSchema.model_validate({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"description": "Running count",
|
||||
"reducer": "wf.std.add",
|
||||
}
|
||||
},
|
||||
})
|
||||
schema = StateSchema.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"description": "Running count",
|
||||
"reducer": "wf.std.add",
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
dumped = schema.model_dump(mode="json")
|
||||
assert dumped["type"] == "object"
|
||||
@@ -161,22 +177,24 @@ def test_state_schema_dump_is_valid_json_schema_with_reducer_keyword() -> None:
|
||||
def test_state_field_validation_schema_preserves_root_defs_for_local_refs() -> None:
|
||||
from wf_core import StateSchema
|
||||
|
||||
schema = StateSchema.model_validate({
|
||||
"type": "object",
|
||||
"$defs": {
|
||||
"PoolByCategory": {
|
||||
"type": "object",
|
||||
"properties": {"category": {"type": "string"}},
|
||||
"required": ["category"],
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"current_pools": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/PoolByCategory"},
|
||||
}
|
||||
},
|
||||
})
|
||||
schema = StateSchema.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"$defs": {
|
||||
"PoolByCategory": {
|
||||
"type": "object",
|
||||
"properties": {"category": {"type": "string"}},
|
||||
"required": ["category"],
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"current_pools": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/PoolByCategory"},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
field_schema = schema.field_map()["current_pools"].validation_schema
|
||||
|
||||
|
||||
Reference in New Issue
Block a user