workflow builder deprecated field stop use in wf_mcp
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user