From 6e6a4f943dd28216db5f50a7eaf5c89294502118 Mon Sep 17 00:00:00 2001 From: lda Date: Sat, 5 Sep 2026 00:10:10 +0700 Subject: [PATCH] fix: quote dotted aliases and offer root foreach in inventory --- src/wf_api/authoring_contracts.py | 4 +- tests/wf_api/test_authoring_contracts.py | 76 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/wf_api/authoring_contracts.py b/src/wf_api/authoring_contracts.py index 4694808b..3765f9ed 100644 --- a/src/wf_api/authoring_contracts.py +++ b/src/wf_api/authoring_contracts.py @@ -202,7 +202,9 @@ def context_path_options( reason = raw_reason if isinstance(raw_reason, str) else None option: AuthoringPathOptionPayload = { - "path": f"context.{name}", + # Field names are literal path segments: dotted foreach aliases + # must stay quoted as one segment, never split on ".". + "path": str(GraphSourcePath("context", (name,))), "label": name.replace("_", " ").replace("-", " ").title(), "origin": "runtime_context", "schema": deepcopy(dict(schema)), diff --git a/tests/wf_api/test_authoring_contracts.py b/tests/wf_api/test_authoring_contracts.py index 9ad23bf7..ef66b759 100644 --- a/tests/wf_api/test_authoring_contracts.py +++ b/tests/wf_api/test_authoring_contracts.py @@ -510,3 +510,79 @@ def test_structured_foreach_paths_appear_in_authoring_inventory() -> None: if option["path"].startswith("context.foreach."): assert option["origin"] == "runtime_context" assert option["uses"] == ["step_input"] + + +def _ref_inventory_workflow(*, alias: str = "order"): + from wf_core import END, Edge, ForeachNode, NodeUse, SchemaRef, Workflow + from wf_core.models.schemas import StateSchema + + return Workflow( + name="inventory_nested_ref", + input_schema=SchemaRef(type="object"), + state_schema=StateSchema.model_validate( + { + "type": "object", + "properties": { + "orders": { + "type": "array", + "items": {"$ref": "#/$defs/Order"}, + }, + }, + "$defs": { + "Order": { + "type": "object", + "properties": { + "sku": {"type": "string"}, + "detail": {"$ref": "#/$defs/Detail"}, + }, + }, + "Detail": { + "type": "object", + "properties": {"name": {"type": "string"}}, + }, + }, + } + ), + output_schema=SchemaRef(type="object"), + start="orders", + nodes=[ + ForeachNode.model_validate( + {"id": "orders", "type": "foreach", "over": "state.orders", "as": alias} + ), + NodeUse(id="body", type="node", node="noop"), + ], + edges=[ + Edge.model_validate({"from": "orders", "outcome": "loop", "to": "body"}), + Edge.model_validate({"from": "body", "outcome": "ok", "to": "orders"}), + Edge.model_validate({"from": "orders", "outcome": "done", "to": END}), + ], + ) + + +def test_nested_ref_item_children_appear_in_authoring_inventory() -> None: + from wf_api.authoring_contracts import context_path_options_for_node + + options = context_path_options_for_node(_ref_inventory_workflow(), "body") + paths = {option["path"] for option in options} + assert "context.foreach.orders.item.sku" in paths + assert "context.foreach.orders.item.detail.name" in paths + + +def test_dotted_alias_produces_quoted_authoring_path() -> None: + from wf_api.authoring_contracts import context_path_options_for_node + + options = context_path_options_for_node( + _ref_inventory_workflow(alias="item.alias"), "body" + ) + paths = {option["path"] for option in options} + assert 'context."item.alias"' in paths + assert "context.item.alias" not in paths + + +def test_root_inventory_offers_foreach_map() -> None: + from wf_api.authoring_contracts import context_path_options_for_node + + options = context_path_options_for_node(_ref_inventory_workflow(), "orders") + paths = {option["path"] for option in options} + assert "context.foreach" in paths + assert "context.prior_outcome" in paths