workflow builder deprecated field stop use in wf_mcp

This commit is contained in:
lda
2026-05-21 05:32:40 +07:00 Verified
parent d089e28c15
commit 8df4c94c4e
13 changed files with 463 additions and 193 deletions
+145 -103
View File
@@ -10,15 +10,17 @@ from wf_core.models.steps import InputValueBinding
def test_adapter_lowers_keyed_use_steps_and_routes_through_builder() -> None:
draft = WorkflowDraft.model_validate({
"name": "echo",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "echo",
"steps": {"echo": {"use": "demo.echo"}},
"routes": {"echo": {"ok": "__end__"}},
})
draft = WorkflowDraft.model_validate(
{
"name": "echo",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "echo",
"steps": {"echo": {"use": "demo.echo"}},
"routes": {"echo": {"ok": "__end__"}},
}
)
workflow = build_workflow_from_draft(draft)
@@ -31,22 +33,56 @@ def test_adapter_lowers_keyed_use_steps_and_routes_through_builder() -> None:
assert workflow.edges[0].to == "__end__"
def test_adapter_lowers_use_steps_to_canonical_bindings() -> None:
draft = WorkflowDraft.model_validate(
{
"name": "echo",
"input_schema": {},
"state_schema": {"fields": {"echoed": {"type": "string"}}},
"output_schema": {},
"start": "echo",
"steps": {
"echo": {
"use": "demo.echo",
"in": {"input.text": "text"},
"out": {"echoed": "state.echoed"},
}
},
"routes": {"echo": {"ok": "__end__"}},
}
)
workflow = build_workflow_from_draft(draft)
node = workflow.nodes[0]
assert isinstance(node, NodeUse)
dumped = node.model_dump(mode="json")
assert "in_map" not in dumped
assert "out_map" not in dumped
assert dumped["input"][0]["target"] == {"root": "local", "parts": ["text"]}
assert dumped["input"][0]["path"] == {"root": "input", "parts": ["text"]}
assert dumped["output"][0]["source"] == {"root": "local", "parts": ["echoed"]}
assert dumped["output"][0]["target"] == {"root": "state", "parts": ["echoed"]}
def test_adapter_lowers_static_inputs_for_constant_like_steps() -> None:
draft = WorkflowDraft.model_validate({
"name": "constant",
"input_schema": {},
"state_schema": {"fields": {"message": {"type": "string"}}},
"output_schema": {},
"start": "constant",
"steps": {
"constant": {
"use": "wf.std.constant",
"with": {"value": "CLICKED"},
"out": {"value": "state.message"},
}
},
"routes": {"constant": {"ok": "__end__"}},
})
draft = WorkflowDraft.model_validate(
{
"name": "constant",
"input_schema": {},
"state_schema": {"fields": {"message": {"type": "string"}}},
"output_schema": {},
"start": "constant",
"steps": {
"constant": {
"use": "wf.std.constant",
"with": {"value": "CLICKED"},
"out": {"value": "state.message"},
}
},
"routes": {"constant": {"ok": "__end__"}},
}
)
workflow = build_workflow_from_draft(draft)
node = workflow.nodes[0]
@@ -88,28 +124,30 @@ def test_invalid_literal_input_map_does_not_fall_through_to_join() -> None:
def test_adapter_lowers_when_step_through_builder() -> None:
draft = WorkflowDraft.model_validate({
"name": "when_example",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "decide",
"steps": {
"decide": {
"when": {
"if": {
"op": "ge",
"left": {"path": "state.count"},
"right": {"value": 1},
},
"then": "echo",
"otherwise": "__end__",
}
draft = WorkflowDraft.model_validate(
{
"name": "when_example",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "decide",
"steps": {
"decide": {
"when": {
"if": {
"op": "ge",
"left": {"path": "state.count"},
"right": {"value": 1},
},
"then": "echo",
"otherwise": "__end__",
}
},
"echo": {"use": "demo.echo"},
},
"echo": {"use": "demo.echo"},
},
"routes": {"echo": {"ok": "__end__"}},
})
"routes": {"echo": {"ok": "__end__"}},
}
)
workflow = build_workflow_from_draft(draft)
condition = workflow.nodes[0]
@@ -124,43 +162,45 @@ def test_adapter_lowers_when_step_through_builder() -> None:
def test_adapter_lowers_choose_step_through_builder() -> None:
draft = WorkflowDraft.model_validate({
"name": "choose_example",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "pick",
"steps": {
"pick": {
"choose": {
"clauses": [
{
"if": {
"op": "gt",
"left": {"path": "state.score"},
"right": {"value": 80},
draft = WorkflowDraft.model_validate(
{
"name": "choose_example",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "pick",
"steps": {
"pick": {
"choose": {
"clauses": [
{
"if": {
"op": "gt",
"left": {"path": "state.score"},
"right": {"value": 80},
},
"then": "high",
},
"then": "high",
},
{
"if": {
"op": "exists",
"path": "state.fallback",
{
"if": {
"op": "exists",
"path": "state.fallback",
},
"then": "fallback",
},
"then": "fallback",
},
],
"default": "__end__",
}
],
"default": "__end__",
}
},
"high": {"use": "demo.high"},
"fallback": {"use": "demo.fallback"},
},
"high": {"use": "demo.high"},
"fallback": {"use": "demo.fallback"},
},
"routes": {
"high": {"ok": "__end__"},
"fallback": {"ok": "__end__"},
},
})
"routes": {
"high": {"ok": "__end__"},
"fallback": {"ok": "__end__"},
},
}
)
workflow = build_workflow_from_draft(draft)
condition_ids = [
@@ -178,31 +218,33 @@ def test_adapter_lowers_choose_step_through_builder() -> None:
def test_adapter_lowers_match_step_through_builder() -> None:
draft = WorkflowDraft.model_validate({
"name": "match_example",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "match_status",
"steps": {
"match_status": {
"match": {
"value": "state.status",
"cases": [
{"equals": "ready", "then": "ready"},
{"equals": "waiting", "then": "waiting"},
],
"default": "__end__",
}
draft = WorkflowDraft.model_validate(
{
"name": "match_example",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "match_status",
"steps": {
"match_status": {
"match": {
"value": "state.status",
"cases": [
{"equals": "ready", "then": "ready"},
{"equals": "waiting", "then": "waiting"},
],
"default": "__end__",
}
},
"ready": {"use": "demo.ready"},
"waiting": {"use": "demo.waiting"},
},
"ready": {"use": "demo.ready"},
"waiting": {"use": "demo.waiting"},
},
"routes": {
"ready": {"ok": "__end__"},
"waiting": {"ok": "__end__"},
},
})
"routes": {
"ready": {"ok": "__end__"},
"waiting": {"ok": "__end__"},
},
}
)
workflow = build_workflow_from_draft(draft)
condition_ids = [
+5
View File
@@ -64,5 +64,10 @@ def test_source_inventory_exposes_serializable_reducer_details() -> None:
assert isinstance(detail, ReducerInventory)
assert detail.name == "wf.std.max"
assert str(detail.ref.source) == "wf.std"
assert detail.ref.name == "max"
assert detail.description == "Keep the greater value."
assert detail.config_schema == {"type": "object", "properties": {}}
assert source.as_inventory().model_dump(mode="json")["capabilities"][
"reducer_details"
][0]["ref"] == {"source": "wf.std", "capability_key": "max"}
+23 -16
View File
@@ -138,6 +138,9 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
minimal_request = minimal_workspace_input["properties"]["request"]
assert minimal_request["properties"]["workspace_id"]["pattern"]
assert "error_message_source" in minimal_request["properties"]
assert "input" in minimal_request["properties"]
assert "output" in minimal_request["properties"]
assert "input_map" in minimal_request["properties"]
assert (
minimal_request["properties"]["input_schema"]["description"]
== "Public input JSON Schema for the workflow or wrapper being "
@@ -149,6 +152,8 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
from_capability_request = from_capability_input["properties"]["request"]
assert "capability_name" in from_capability_request["properties"]
assert "input_schema" in from_capability_request["properties"]
assert "input" in from_capability_request["properties"]
assert "output" in from_capability_request["properties"]
assert "output_map" in from_capability_request["properties"]
from_capability_output = tools_by_name[
"wf.workflow.create_draft_workspace_from_capability"
@@ -526,22 +531,24 @@ def test_server_reload_syncs_service_connection_source_enabled_state() -> None:
tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text(
json.dumps({
"store_root": ".wf_mcp_store",
"connections": [
{
"id": "fixture.personal",
"server": "fixture",
"account": "personal",
"enabled": False,
"metadata": {
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
}
],
}),
json.dumps(
{
"store_root": ".wf_mcp_store",
"connections": [
{
"id": "fixture.personal",
"server": "fixture",
"account": "personal",
"enabled": False,
"metadata": {
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
}
],
}
),
encoding="utf-8",
)
config = load_broker_config(config_path)
+41
View File
@@ -16,6 +16,8 @@ from wf_mcp.broker import WfMcpService
from wf_mcp.models import ConnectionConfig, RawWorkflowPlan
from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
from wf_core.models.steps import InputPathBinding, OutputBinding
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
from wf_platform import (
CapabilityBuckets,
CapabilitySource,
@@ -680,6 +682,45 @@ def test_workflow_surface_creates_minimal_draft_workspace_with_error_route() ->
assert workspace.draft["steps"]["tool_error"]["in"] == {"state.echoed": "message"}
def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_minimal_canonical_mcp"),
artifact_store=FileWorkflowArtifactStore(
local_temp_root() / "surface_minimal_canonical"
),
)
handlers = WorkflowSurfaceHandlers(service)
result = asyncio.run(
handlers.create_minimal_draft_workspace(
workspace_id="echo_draft",
name="echo",
capability_name="demo.personal.echo_tool",
input_schema={"type": "object"},
state_schema={"fields": {"echoed": {"type": "string"}}},
output_schema={"type": "object"},
input=[
InputPathBinding(
target=LocalPath(("text",)),
path=GraphSourcePath("input", ("text",)),
)
],
output=[
OutputBinding(
source=LocalPath(("echoed",)),
target=StatePath(("echoed",)),
)
],
)
)
assert service.draft_workspace_store is not None
workspace = service.draft_workspace_store.get_workspace("echo_draft")
assert result["workspace_id"] == "echo_draft"
assert workspace.draft["steps"]["call"]["in"] == {"input.text": "text"}
assert workspace.draft["steps"]["call"]["out"] == {"echoed": "state.echoed"}
def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_from_capability"