from __future__ import annotations from wf_api.authoring_contracts import ( context_path_options, project_authoring_contract_inventory, project_authoring_step_contract, schema_path_options, ) from wf_api.models import AuthoringPathOptionPayload, AuthoringStepContractPayload def test_schema_path_options_is_parent_first_and_schema_derived() -> None: schema = { "type": "object", "properties": { "request": { "title": "Request", "description": "The incoming request.", "type": "object", "properties": { "id": {"type": "string"}, "metadata": { "type": "object", "additionalProperties": True, }, }, "required": ["id"], }, "items": { "type": "array", "items": { "type": "object", "properties": {"name": {"type": "string"}}, }, }, "headers": { "type": "object", "additionalProperties": {"type": "string"}, }, }, "required": ["request", "items"], } options = schema_path_options( schema, root="input", uses=["step_input", "workflow_output"], ) assert options == [ { "path": "input.request", "label": "Request", "origin": "workflow_input", "schema": { "title": "Request", "description": "The incoming request.", "type": "object", "properties": { "id": {"type": "string"}, "metadata": {"type": "object", "additionalProperties": True}, }, "required": ["id"], }, "required": True, "availability": "available", "uses": ["step_input", "workflow_output"], "description": "The incoming request.", }, { "path": "input.request.id", "label": "Id", "origin": "workflow_input", "schema": {"type": "string"}, "required": True, "availability": "available", "uses": ["step_input", "workflow_output"], }, { "path": "input.request.metadata", "label": "Metadata", "origin": "workflow_input", "schema": {"type": "object", "additionalProperties": True}, "required": False, "availability": "available", "uses": ["step_input", "workflow_output"], }, { "path": "input.items", "label": "Items", "origin": "workflow_input", "schema": { "type": "array", "items": { "type": "object", "properties": {"name": {"type": "string"}}, }, }, "required": True, "availability": "available", "uses": ["step_input", "workflow_output"], }, { "path": "input.headers", "label": "Headers", "origin": "workflow_input", "schema": { "type": "object", "additionalProperties": {"type": "string"}, }, "required": False, "availability": "available", "uses": ["step_input", "workflow_output"], }, ] assert not any("*" in option["path"] for option in options) assert not any(option["path"].startswith("input.metadata.") for option in options) assert options[0] == { "path": "input.request", "label": "Request", "origin": "workflow_input", "schema": { "title": "Request", "description": "The incoming request.", "type": "object", "properties": { "id": {"type": "string"}, "metadata": {"type": "object", "additionalProperties": True}, }, "required": ["id"], }, "required": True, "availability": "available", "uses": ["step_input", "workflow_output"], "description": "The incoming request.", } def test_schema_path_options_resolves_local_definition_metadata() -> None: options = schema_path_options( { "type": "object", "properties": {"snapshot": {"$ref": "#/$defs/Snapshot"}}, "required": ["snapshot"], "$defs": { "Snapshot": { "title": "Saved Snapshot", "type": "object", "properties": {"version": {"type": "integer"}}, "required": ["version"], } }, }, root="input", uses=["step_input"], ) assert options == [ { "path": "input.snapshot", "label": "Saved Snapshot", "origin": "workflow_input", "schema": { "$ref": "#/$defs/Snapshot", "$defs": { "Snapshot": { "title": "Saved Snapshot", "type": "object", "properties": {"version": {"type": "integer"}}, "required": ["version"], } }, }, "required": True, "availability": "available", "uses": ["step_input"], }, { "path": "input.snapshot.version", "label": "Version", "origin": "workflow_input", "schema": { "type": "integer", "$defs": { "Snapshot": { "title": "Saved Snapshot", "type": "object", "properties": {"version": {"type": "integer"}}, "required": ["version"], } }, }, "required": True, "availability": "available", "uses": ["step_input"], }, ] def test_schema_path_options_stops_expanding_recursive_local_definition() -> None: options = schema_path_options( { "type": "object", "properties": {"node": {"$ref": "#/$defs/Node"}}, "$defs": { "Node": { "type": "object", "properties": {"child": {"$ref": "#/$defs/Node"}}, } }, }, root="input", uses=["step_input"], ) assert [option["path"] for option in options] == [ "input.node", "input.node.child", ] def test_schema_path_options_bounds_deep_inline_objects() -> None: schema: dict[str, object] = {"type": "object", "properties": {}} current = schema for index in range(60): child: dict[str, object] = {"type": "object", "properties": {}} current["properties"] = {f"level_{index}": child} current = child options = schema_path_options(schema, root="input", uses=["step_input"]) assert len(options) == 32 def test_schema_path_options_returns_empty_schema_for_unconstrained_property() -> None: options = schema_path_options( { "type": "object", "properties": {"value": {}}, }, root="state", uses=["state_target"], ) assert options == [ { "path": "state.value", "label": "Value", "origin": "workflow_state", "schema": {}, "required": False, "availability": "available", "uses": ["state_target"], } ] def test_schema_path_options_copies_schema_fragments() -> None: schema = { "type": "object", "properties": { "request": { "type": "object", "properties": {"id": {"type": "string"}}, } }, } options = schema_path_options(schema, root="input", uses=["step_input"]) # Options are safe for UI consumers to annotate without mutating the source schema. options[0]["schema"]["title"] = "Edited through option" assert schema["properties"]["request"].get("title") is None def test_project_authoring_contract_inventory_composes_pure_inputs() -> None: context_entry: AuthoringPathOptionPayload = { "path": "context.loop_item", "label": "Loop Item", "origin": "runtime_context", "schema": {"type": "string"}, "required": False, "availability": "conditional", "uses": ["step_input"], "reason": "Only available inside the foreach body.", } step_input_target: AuthoringPathOptionPayload = { "path": "step.input.query", "label": "Query", "origin": "step_input", "schema": {"type": "string"}, "required": True, "availability": "available", "uses": ["step_input"], } step_output_source: AuthoringPathOptionPayload = { "path": "step.output.answer", "label": "Answer", "origin": "step_output", "schema": {"type": "string"}, "required": False, "availability": "available", "uses": ["step_output_source", "workflow_output"], } entry_step: AuthoringStepContractPayload = { "step_id": "fetch", "label": "Fetch", } inventory = project_authoring_contract_inventory( workspace_id="workspace-1", revision=4, selected_step_id="fetch", input_schema={ "type": "object", "properties": {"request": {"type": "string"}}, }, state_schema={ "type": "object", "properties": {"answer": {"type": "string"}}, }, output_schema={ "type": "object", "properties": {"result": {"type": "string"}}, }, context_entries=[context_entry], step_input_targets=[step_input_target], step_output_sources=[step_output_source], entry_steps=[entry_step], workflow_outcomes=["ok", "error"], warnings=["selected step has conditional context"], ) assert inventory["workspace_id"] == "workspace-1" assert inventory["revision"] == 4 assert inventory["selected_step_id"] == "fetch" assert inventory["readable_sources"] == [ { "path": "input.request", "label": "Request", "origin": "workflow_input", "schema": {"type": "string"}, "required": False, "availability": "available", "uses": ["step_input", "workflow_output"], }, context_entry, { "path": "state.answer", "label": "Answer", "origin": "workflow_state", "schema": {"type": "string"}, "required": False, "availability": "available", "uses": ["step_input", "workflow_output"], }, ] assert inventory["step_input_targets"] == [step_input_target] assert inventory["step_output_sources"] == [step_output_source] assert inventory["state_targets"][0]["path"] == "state.answer" assert inventory["workflow_output_targets"][0]["path"] == "output.result" assert inventory["entry_steps"] == [entry_step] assert inventory["workflow_outcomes"] == ["ok", "error"] assert inventory["warnings"] == ["selected step has conditional context"] def test_authoring_paths_exclude_incompatible_binding_roles() -> None: inventory = project_authoring_contract_inventory( workspace_id="workspace-1", revision=1, selected_step_id=None, input_schema={"type": "object"}, state_schema={ "type": "object", "properties": {"answer": {"type": "string"}}, }, output_schema={"type": "object"}, ) step_contract = project_authoring_step_contract( step_id="fetch", label="Fetch", description=None, input_schema={"type": "object"}, output_schema={ "type": "object", "properties": {"answer": {"type": "string"}}, }, outcomes=["ok"], ) state_source = inventory["readable_sources"][0] assert state_source["path"] == "state.answer" assert "step_output_source" not in state_source["uses"] output_sources = step_contract.get("output_sources") assert output_sources is not None assert output_sources[0]["path"] == "step_output.answer" assert "workflow_output" not in output_sources[0]["uses"] def test_context_path_options_are_step_input_only() -> None: options = context_path_options( [ { "name": "loop_item", "schema": {"type": "string"}, "description": "Current foreach item", "availability": "conditional", "reason": "Only available inside the foreach body.", } ] ) assert options[0]["path"] == "context.loop_item" assert options[0]["origin"] == "runtime_context" assert options[0]["uses"] == ["step_input"] assert options[0]["availability"] == "conditional" reason = options[0].get("reason") assert reason == "Only available inside the foreach body." def test_project_inventory_does_not_offer_context_for_workflow_output() -> None: context_entry: AuthoringPathOptionPayload = { "path": "context.item", "label": "Item", "origin": "runtime_context", "schema": {}, "required": False, "availability": "available", "uses": ["step_input", "workflow_output"], } inventory = project_authoring_contract_inventory( workspace_id="workspace-1", revision=1, selected_step_id=None, input_schema={"type": "object"}, state_schema={"type": "object"}, output_schema={"type": "object"}, context_entries=[context_entry], ) 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"] 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 def _composer_inventory_workflow(): from wf_core import END, Edge, ForeachNode, NodeUse, SchemaRef, Workflow from wf_core.models.schemas import StateSchema return Workflow( name="inventory_composer_refs", 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": { "anyOf": [ {"$ref": "#/$defs/Detail"}, {"type": "null"}, ] }, "nick": { "$ref": "#/$defs/Detail", "description": "Short display name", "properties": {"label": {"type": "string"}}, }, }, }, "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": "order", } ), 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_composer_ref_children_appear_in_authoring_inventory() -> None: from wf_api.authoring_contracts import context_path_options_for_node options = context_path_options_for_node(_composer_inventory_workflow(), "body") paths = {option["path"] for option in options} assert "context.foreach.orders.item.detail.name" in paths assert "context.foreach.orders.item.nick.name" in paths assert "context.foreach.orders.item.nick.label" in paths def _recursive_inventory_workflow(): from wf_core import END, Edge, ForeachNode, NodeUse, SchemaRef, Workflow from wf_core.models.schemas import StateSchema return Workflow( name="inventory_recursive_refs", input_schema=SchemaRef(type="object"), state_schema=StateSchema.model_validate( { "type": "object", "properties": { "cats": { "type": "array", "items": {"$ref": "#/$defs/Category"}, }, }, "$defs": { "Category": { "type": "object", "properties": { "name": {"type": "string"}, "parent": { "anyOf": [ {"$ref": "#/$defs/Category"}, {"type": "null"}, ] }, "children": { "type": "array", "items": {"$ref": "#/$defs/Category"}, }, }, }, }, } ), output_schema=SchemaRef(type="object"), start="cats", nodes=[ ForeachNode.model_validate( {"id": "cats", "type": "foreach", "over": "state.cats", "as": "cat"} ), NodeUse(id="body", type="node", node="noop"), ], edges=[ Edge.model_validate({"from": "cats", "outcome": "loop", "to": "body"}), Edge.model_validate({"from": "body", "outcome": "ok", "to": "cats"}), Edge.model_validate({"from": "cats", "outcome": "done", "to": END}), ], ) def test_recursive_ref_inventory_stays_bounded() -> None: from wf_api.authoring_contracts import context_path_options_for_node options = context_path_options_for_node(_recursive_inventory_workflow(), "body") paths = {option["path"] for option in options} assert "context.foreach.cats.item.parent.name" in paths assert "context.foreach.cats.item.children" in paths # The repeated reference stays selectable but is not expanded again. assert "context.foreach.cats.item.parent.parent" in paths assert "context.foreach.cats.item.parent.parent.name" not in paths