feat: describe structured foreach context
This commit is contained in:
@@ -260,12 +260,54 @@ def test_nested_foreach_replaces_inner_scope_and_restores_outer_scope() -> None:
|
||||
|
||||
inner = _field_map(workflow, "inner_body")
|
||||
after_inner = _field_map(workflow, "after_inner")
|
||||
assert "outer_item" not in inner
|
||||
assert inner["inner_item"].availability == "available"
|
||||
outer_item_schema = {"type": "string"}
|
||||
inner_item_schema = {"type": "integer"}
|
||||
|
||||
assert inner["outer_item"].schema == outer_item_schema
|
||||
assert inner["inner_item"].schema == inner_item_schema
|
||||
assert inner["loop_item"].schema == inner_item_schema
|
||||
foreach_schema = inner["foreach"].schema
|
||||
assert set(foreach_schema["properties"]) == {"outer", "inner"}
|
||||
assert foreach_schema["properties"]["outer"]["properties"]["item"] == (
|
||||
outer_item_schema
|
||||
)
|
||||
assert foreach_schema["properties"]["inner"]["properties"]["index"] == {
|
||||
"type": "integer"
|
||||
}
|
||||
assert after_inner["outer_item"].availability == "available"
|
||||
assert "inner_item" not in after_inner
|
||||
|
||||
|
||||
def test_inner_completion_schema_restores_outer_structured_entry() -> None:
|
||||
workflow = _workflow(
|
||||
start="outer",
|
||||
nodes=[
|
||||
_foreach("outer", alias="outer_item"),
|
||||
_foreach("inner", alias="inner_item", over="state.inner_items"),
|
||||
_node("inner_body"),
|
||||
_node("after_inner"),
|
||||
],
|
||||
edges=[
|
||||
{"from": "outer", "outcome": "loop", "to": "inner"},
|
||||
{"from": "inner", "outcome": "loop", "to": "inner_body"},
|
||||
{"from": "inner", "outcome": "done", "to": "after_inner"},
|
||||
{"from": "inner_body", "outcome": "ok", "to": "inner"},
|
||||
{"from": "after_inner", "outcome": "ok", "to": "outer"},
|
||||
{"from": "outer", "outcome": "done", "to": END},
|
||||
],
|
||||
state_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {"type": "array", "items": {"type": "string"}},
|
||||
"inner_items": {"type": "array", "items": {"type": "integer"}},
|
||||
},
|
||||
},
|
||||
)
|
||||
after_inner = _field_map(workflow, "after_inner")
|
||||
foreach_schema = after_inner["foreach"].schema
|
||||
assert set(foreach_schema["properties"]) == {"outer"}
|
||||
|
||||
|
||||
def test_nested_foreach_preserves_context_backed_item_schema() -> None:
|
||||
workflow = _workflow(
|
||||
start="outer",
|
||||
|
||||
@@ -441,3 +441,68 @@ def test_project_inventory_does_not_offer_context_for_workflow_output() -> None:
|
||||
|
||||
assert inventory["readable_sources"][0]["path"] == "context.item"
|
||||
assert inventory["readable_sources"][0]["uses"] == ["step_input"]
|
||||
|
||||
|
||||
def test_structured_foreach_paths_appear_in_authoring_inventory() -> None:
|
||||
from wf_api.authoring_contracts import context_path_options_for_node
|
||||
from wf_core import END, Edge, ForeachNode, NodeUse, SchemaRef, Workflow
|
||||
from wf_core.models.schemas import StateSchema
|
||||
|
||||
def _foreach(node_id: str, *, over: str, alias: str) -> ForeachNode:
|
||||
return ForeachNode.model_validate(
|
||||
{"id": node_id, "type": "foreach", "over": over, "as": alias}
|
||||
)
|
||||
|
||||
workflow = Workflow(
|
||||
name="inventory_structured",
|
||||
input_schema=SchemaRef(type="object"),
|
||||
state_schema=StateSchema.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"customers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
},
|
||||
},
|
||||
"orders": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {"sku": {"type": "string"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
output_schema=SchemaRef(type="object"),
|
||||
start="customers",
|
||||
nodes=[
|
||||
_foreach("customers", over="state.customers", alias="customer"),
|
||||
_foreach("orders", over="state.orders", alias="order"),
|
||||
NodeUse(id="inner_body", type="node", node="noop"),
|
||||
],
|
||||
edges=[
|
||||
Edge.model_validate(
|
||||
{"from": "customers", "outcome": "loop", "to": "orders"}
|
||||
),
|
||||
Edge.model_validate(
|
||||
{"from": "orders", "outcome": "loop", "to": "inner_body"}
|
||||
),
|
||||
Edge.model_validate({"from": "inner_body", "outcome": "ok", "to": "orders"}),
|
||||
Edge.model_validate({"from": "orders", "outcome": "done", "to": "customers"}),
|
||||
Edge.model_validate({"from": "customers", "outcome": "done", "to": END}),
|
||||
],
|
||||
)
|
||||
options = context_path_options_for_node(workflow, "inner_body")
|
||||
paths = {option["path"] for option in options}
|
||||
assert "context.foreach.customers.item" in paths
|
||||
assert "context.foreach.customers.index" in paths
|
||||
assert "context.foreach.orders.item" in paths
|
||||
assert "context.foreach.orders.index" in paths
|
||||
for option in options:
|
||||
if option["path"].startswith("context.foreach."):
|
||||
assert option["origin"] == "runtime_context"
|
||||
assert option["uses"] == ["step_input"]
|
||||
|
||||
Reference in New Issue
Block a user