test org 4

This commit is contained in:
lda
2026-05-29 23:50:06 +07:00 Verified
parent 6f9ca4df0a
commit 68c12e80b5
9 changed files with 2022 additions and 1915 deletions
File diff suppressed because it is too large Load Diff
+391
View File
@@ -0,0 +1,391 @@
from __future__ import annotations
import asyncio
from typing import Any
from pydantic import BaseModel
from wf_artifacts import (
FileWorkflowArtifactStore,
RequiredCapability,
WorkflowArtifact,
WorkflowDeployment,
)
from wf_authoring import node, reducer
from wf_mcp.broker import WfMcpService
from wf_mcp.models import AuthRecord, ConnectionConfig, RawWorkflowPlan
from wf_mcp.sdk import ToolCallResult
from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import TraceRange, WorkflowSurfaceHandlers
from wf_mcp.workflow_surface.models import CreateMinimalDraftWorkspaceRequest
from wf_core.models.steps import InputPathBinding, OutputBinding
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
from wf_mcp.capabilities import DiscoveredTool
from wf_platform import (
CapabilityBuckets,
CapabilitySource,
SourcePermissions,
SourceVisibility,
)
from ..test_support import echo_tool, input_binding, local_temp_root, output_binding
class AmountInput(BaseModel):
amount: int
class AmountOutput(BaseModel):
amount: int
class ChangedEchoInput(BaseModel):
message: str
class ChangedEchoOutput(BaseModel):
echoed: str
@node()
async def amount_tool(payload: AmountInput) -> AmountOutput:
return AmountOutput(amount=payload.amount)
@node(name="echo_tool")
def changed_echo_tool(payload: ChangedEchoInput) -> ChangedEchoOutput:
return ChangedEchoOutput(echoed=payload.message)
@node(name="mcp_echo_tool", outcomes=("ok", "error"))
def mcp_echo_tool(payload: ChangedEchoInput) -> ChangedEchoOutput:
"""Test fixture that mirrors naive MCP wrappers with ok/error outcomes."""
return ChangedEchoOutput(echoed=payload.message)
@node(name="failing_tool")
def failing_tool(payload: ChangedEchoInput) -> ChangedEchoOutput:
raise RuntimeError("upstream exploded")
class ContentOnlyOutputAdapter:
"""MCP-like adapter whose tool exposes raw content blocks as output schema."""
async def list_tools(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
) -> list[DiscoveredTool]:
return [
DiscoveredTool(
name="echo",
title="Echo",
description="Echo a message as an MCP text content block.",
input_schema={
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
output_schema={
"type": "object",
"properties": {"content": {"type": "array"}},
"required": ["content"],
},
)
]
async def list_resources(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
) -> list[Any]:
return []
async def list_prompts(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
) -> list[Any]:
return []
async def get_connection_metadata(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
) -> dict[str, Any]:
return {"server": connection.server}
async def call_tool(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
tool_name: str,
payload: dict[str, Any],
) -> ToolCallResult:
message = payload.get("message", "")
return ToolCallResult(
outcome="ok",
output={"content": [{"type": "text", "text": f"Echo: {message}"}]},
)
async def read_resource(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
uri: str,
) -> dict[str, Any]:
raise KeyError(uri)
async def get_prompt(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
prompt_name: str,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
raise KeyError(prompt_name)
async def invoke_method(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
method: str,
params: dict[str, Any] | None = None,
) -> dict[str, Any]:
raise KeyError(method)
async def send_notification(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
method: str,
params: dict[str, Any] | None = None,
) -> None:
raise KeyError(method)
@reducer(name="custom.default.multiply")
def multiply(current: int | None, incoming: int) -> int:
return (current or 1) * incoming
def handlers(artifact_store: FileWorkflowArtifactStore) -> WorkflowSurfaceHandlers:
service = WfMcpService(
store=FileStore(artifact_store.root / "surface_mcp" / str(id(artifact_store))),
artifact_store=artifact_store,
)
return WorkflowSurfaceHandlers(service)
def artifact() -> WorkflowArtifact:
return WorkflowArtifact(
id="summarize_docs",
version=1,
title="Summarize Docs",
description="Summarize retrieved documentation.",
input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}},
outcomes=("done",),
plan={"name": "summarize_docs", "nodes": [], "edges": []},
required_capabilities={
"context7.query-docs": RequiredCapability(
ref="context7.query-docs",
kind="tool",
input_schema_hash="sha256:input",
output_schema_hash="sha256:output",
)
},
)
def echo_artifact() -> WorkflowArtifact:
plan: dict[str, Any] = {
"name": "echo",
"input_schema": {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
"state_schema": {"fields": {"echoed": {"type": "string"}}},
"output_schema": {
"type": "object",
"properties": {"echoed": {"type": "string"}},
"required": ["echoed"],
},
"start": "echo",
"nodes": [
{
"id": "echo",
"type": "node",
"node": "demo.personal.echo_tool",
"input": [input_binding("input.text", "text")],
"output": [output_binding("echoed", "state.echoed")],
}
],
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}],
}
return WorkflowArtifact(
id="echo",
version=1,
title="Echo",
input_schema=plan["input_schema"],
output_schema=plan["output_schema"],
outcomes=("completed",),
plan=plan,
required_capabilities={
"demo.echo_tool": RequiredCapability(
ref="demo.echo_tool",
kind="node_spec",
)
},
)
def echo_draft() -> dict[str, Any]:
return {
"name": "echo",
"input_schema": {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
"state_schema": {"fields": {"echoed": {"type": "string"}}},
"output_schema": {
"type": "object",
"properties": {"echoed": {"type": "string"}},
"required": ["echoed"],
},
"start": "echo",
"steps": {
"echo": {
"use": "demo.personal.echo_tool",
"input": [
{
"target": {"root": "local", "parts": ["text"]},
"path": {"root": "input", "parts": ["text"]},
}
],
"output": [
{
"source": {"root": "local", "parts": ["echoed"]},
"target": {"root": "state", "parts": ["echoed"]},
}
],
}
},
"routes": {"echo": {"ok": "__end__"}},
}
def logical_echo_artifact() -> WorkflowArtifact:
a = echo_artifact()
plan = dict(a.plan)
nodes = [dict(node) for node in plan["nodes"]]
nodes[0]["node"] = "demo.echo_tool"
plan["nodes"] = nodes
return a.model_copy(
update={
"id": "logical_echo",
"plan": plan,
}
)
def failing_artifact() -> WorkflowArtifact:
plan: dict[str, Any] = {
"name": "fail",
"input_schema": {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
"state_schema": {"fields": {"echoed": {"type": "string"}}},
"output_schema": {
"type": "object",
"properties": {"echoed": {"type": "string"}},
},
"start": "fail",
"nodes": [
{
"id": "fail",
"type": "node",
"node": "demo.personal.failing_tool",
"input": [input_binding("input.message", "message")],
"output": [output_binding("echoed", "state.echoed")],
}
],
"edges": [{"from": "fail", "outcome": "ok", "to": "__end__"}],
}
return WorkflowArtifact(
id="fail",
version=1,
title="Fail",
input_schema=plan["input_schema"],
output_schema=plan["output_schema"],
outcomes=("completed",),
plan=plan,
required_capabilities={
"demo.failing_tool": RequiredCapability(
ref="demo.failing_tool",
kind="node_spec",
)
},
)
def custom_reducer_artifact() -> WorkflowArtifact:
plan: dict[str, Any] = {
"name": "multiply",
"input_schema": {
"type": "object",
"properties": {
"total": {"type": "integer"},
"amount": {"type": "integer"},
},
"required": ["total", "amount"],
},
"state_schema": {
"type": "object",
"properties": {
"total": {
"type": "integer",
"reducer": "custom.multiply",
}
},
},
"output_schema": {
"type": "object",
"properties": {"total": {"type": "integer"}},
"required": ["total"],
},
"start": "amount",
"nodes": [
{
"id": "amount",
"type": "node",
"node": "demo.personal.amount_tool",
"input": [input_binding("input.amount", "amount")],
"output": [output_binding("amount", "state.total")],
}
],
"edges": [{"from": "amount", "outcome": "ok", "to": "__end__"}],
}
return WorkflowArtifact(
id="multiply",
version=1,
title="Multiply",
input_schema=plan["input_schema"],
output_schema=plan["output_schema"],
outcomes=("completed",),
plan=plan,
required_capabilities={
"demo.amount_tool": RequiredCapability(
ref="demo.amount_tool",
kind="node_spec",
),
"custom.multiply": RequiredCapability(
ref="custom.multiply",
kind="reducer",
),
},
)
@@ -0,0 +1,71 @@
from __future__ import annotations
import asyncio
from wf_artifacts import FileWorkflowArtifactStore
from ..test_support import local_temp_root
from .conftest import artifact, echo_artifact, handlers
def test_workflow_surface_lists_artifact_catalog_entries() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_artifacts")
artifact_store.save_artifact(artifact())
h = handlers(artifact_store)
payload = asyncio.run(h.list_artifacts())
nodes = payload["nodes"]
assert len(nodes) == 1
assert payload["total"] == 1
assert payload["next_cursor"] is None
assert nodes[0]["name"] == "workflow.summarize_docs.v1"
assert nodes[0]["artifact_id"] == "summarize_docs"
assert nodes[0]["version"] == 1
assert nodes[0]["kind"] == "workflow"
assert nodes[0]["required_sources"] == ["context7"]
assert "plan" not in nodes[0]
def test_workflow_surface_pages_and_filters_artifact_catalog_entries() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_artifact_pages"
)
artifact_store.save_artifact(artifact())
artifact_store.save_artifact(
artifact().model_copy(
update={
"id": "echo_wrapper",
"version": 2,
"kind": "wrapper",
"title": "Echo Wrapper",
"description": "Reusable echo wrapper.",
}
)
)
artifact_store.save_artifact(
artifact().model_copy(
update={
"id": "browser_click",
"title": "Browser Click",
"description": "Open a page and wait for a click.",
}
)
)
h = handlers(artifact_store)
first_page = asyncio.run(h.list_artifacts(limit=2))
second_page = asyncio.run(
h.list_artifacts(cursor=first_page["next_cursor"], limit=2)
)
wrappers = asyncio.run(h.list_artifacts(kind="wrapper", query="echo"))
assert first_page["total"] == 3
assert first_page["next_cursor"] == "2"
assert len(first_page["nodes"]) == 2
assert len(second_page["nodes"]) == 1
assert second_page["next_cursor"] is None
assert wrappers["total"] == 1
assert wrappers["nodes"][0]["artifact_id"] == "echo_wrapper"
assert wrappers["nodes"][0]["kind"] == "wrapper"
assert "plan" not in wrappers["nodes"][0]
@@ -0,0 +1,216 @@
from __future__ import annotations
import asyncio
from wf_artifacts import FileWorkflowArtifactStore, WorkflowArtifact
from wf_mcp.broker import WfMcpService
from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
from ..test_support import echo_tool, local_temp_root
from .conftest import (
ContentOnlyOutputAdapter,
echo_artifact,
failing_tool,
handlers,
)
def test_workflow_surface_lists_planner_visible_capabilities() -> None:
h = handlers(FileWorkflowArtifactStore(local_temp_root() / "surface_caps"))
payload = asyncio.run(h.list_capabilities(limit=2))
names = [capability["name"] for capability in payload["capabilities"]]
first = payload["capabilities"][0]
assert len(names) == 2
assert payload["total"] >= 2
assert payload["next_cursor"] == "2"
assert "description" in first
assert "source_id" in first
assert first["kind"] == "node_spec"
assert "input_fields" in first
assert "output_fields" in first
assert "input_schema" not in first
assert "wf.admin.list_sources" not in names
def test_workflow_surface_filters_stdlib_capabilities_by_source() -> None:
h = handlers(
FileWorkflowArtifactStore(local_temp_root() / "surface_filtered_caps")
)
payload = asyncio.run(
h.list_capabilities(source_id="wf.std", query="truthy")
)
assert [capability["name"] for capability in payload["capabilities"]] == [
"wf.std.truthy"
]
assert payload["capabilities"][0]["source_id"] == "wf.std"
def test_workflow_surface_call_capability_returns_structured_error() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_call_capability_error_mcp"),
artifact_store=FileWorkflowArtifactStore(
local_temp_root() / "surface_call_capability_error"
),
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", failing_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.call_capability(
qualified_name="demo.personal.failing_tool",
payload={"message": "hello"},
)
)
assert payload["qualified_name"] == "demo.personal.failing_tool"
assert payload["source_id"] == "demo.personal"
assert payload["kind"] == "node_spec"
assert payload["outcome"] == "runtime_error"
assert payload["output"] is None
assert payload["diagnostics"][0]["code"] == "capability_call_failed"
assert payload["diagnostics"][0]["severity"] == "error"
assert "demo.personal.failing_tool" in payload["diagnostics"][0]["message"]
assert "upstream exploded" in payload["diagnostics"][0]["message"]
def test_workflow_surface_lists_saved_wrapper_capabilities() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_wrapper_caps"
)
artifact_store.save_artifact(
echo_artifact().model_copy(
update={
"id": "echo_wrapper",
"kind": "wrapper",
"description": "Reusable echo wrapper.",
}
)
)
artifact_store.save_artifact(echo_artifact())
h = handlers(artifact_store)
payload = asyncio.run(
h.list_capabilities(source_id="workflow", query="echo")
)
names = [capability["name"] for capability in payload["capabilities"]]
assert names == ["workflow.echo_wrapper.v1"]
assert payload["capabilities"][0]["source_id"] == "workflow"
assert payload["capabilities"][0]["kind"] == "wrapper_artifact"
assert payload["capabilities"][0]["artifact_id"] == "echo_wrapper"
assert payload["capabilities"][0]["version"] == 1
assert payload["capabilities"][0]["title"] == "Echo"
assert payload["capabilities"][0]["outcomes"] == ["completed"]
assert payload["capabilities"][0]["input_fields"] == ["text"]
assert payload["capabilities"][0]["output_fields"] == ["echoed"]
def test_workflow_surface_inspects_one_capability() -> None:
h = handlers(
FileWorkflowArtifactStore(local_temp_root() / "surface_inspect_cap")
)
payload = asyncio.run(
h.inspect_capability(qualified_name="wf.std.runtime_error")
)
assert payload["name"] == "wf.std.runtime_error"
assert payload["outcomes"] == ["ok"]
assert "input_schema" in payload
def test_workflow_surface_inspect_capability_includes_wrapper_hints() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_wrapper_hints_mcp"),
artifact_store=FileWorkflowArtifactStore(
local_temp_root() / "surface_wrapper_hints_artifacts"
),
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.inspect_capability(qualified_name="demo.personal.echo_tool")
)
hints = payload["wrapper_hints"]
assert hints["capability_name"] == "demo.personal.echo_tool"
assert hints["declared_outcomes"] == ["ok"]
assert hints["input_map"] == {"input.text": "text"}
assert hints["output_map"] == {"echoed": "state.echoed"}
assert hints["outcome_policy"] == "preserve_declared"
def test_workflow_surface_inspects_saved_wrapper_capability() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_inspect_wrapper_cap"
)
artifact_store.save_artifact(
echo_artifact().model_copy(update={"id": "echo_wrapper", "kind": "wrapper"})
)
h = handlers(artifact_store)
payload = asyncio.run(
h.inspect_capability(qualified_name="workflow.echo_wrapper.v1")
)
assert payload["name"] == "workflow.echo_wrapper.v1"
assert payload["source_id"] == "workflow"
assert payload["kind"] == "wrapper_artifact"
assert payload["artifact_id"] == "echo_wrapper"
assert payload["outcomes"] == ["completed"]
assert "input_schema" in payload
hints = payload["wrapper_hints"]
assert hints["capability_name"] == "workflow.echo_wrapper.v1"
assert hints["declared_outcomes"] == ["completed"]
assert hints["suggested_wrapper_outcomes"] == ["completed"]
assert hints["input_map"] == {"input.text": "text"}
assert hints["output_map"] == {"echoed": "state.echoed"}
def test_workflow_surface_does_not_auto_map_raw_mcp_content_blocks() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_content_only_content_hint"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_content_only_content_hint_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(
id="everything.default", server="everything", account="default"
)
)
service.register_adapter("everything", ContentOnlyOutputAdapter())
asyncio.run(service.refresh_connection_catalog("everything.default"))
h = WorkflowSurfaceHandlers(service)
inspected = asyncio.run(
h.inspect_capability(qualified_name="everything.default.echo")
)
created = asyncio.run(
h.create_draft_workspace_from_capability(
workspace_id="content_blocks",
capability_name="everything.default.echo",
name="content_blocks",
)
)
assert inspected["wrapper_hints"]["output_map"] == {}
assert created["wrapper_hints"]["output_map"] == {}
assert inspected["wrapper_hints"]["missing_decisions"][0]["kind"] == (
"review_nested_output"
)
assert "Raw MCP content blocks" in inspected["wrapper_hints"]["notes"][2]
@@ -0,0 +1,87 @@
from __future__ import annotations
import asyncio
from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment
from ..test_support import local_temp_root
from .conftest import artifact, echo_artifact, handlers
def test_workflow_surface_validates_deployment_dependencies() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_validate")
artifact_store.save_artifact(artifact())
artifact_store.save_deployment(
WorkflowDeployment(
id="summarize_docs.personal",
artifact_id="summarize_docs",
artifact_version=1,
bindings=[
{"logical_source": "context7", "concrete_source": "context7.personal"}
],
)
)
h = handlers(artifact_store)
payload = asyncio.run(
h.validate_deployment(deployment_id="summarize_docs.personal")
)
assert payload["status"] == "unrunnable"
assert payload["diagnostics"][0]["code"] == "source_missing"
def test_workflow_surface_records_artifact_and_deployment_save_events() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_events")
h = handlers(artifact_store)
artifact_payload = asyncio.run(
h.save_artifact(echo_artifact().model_dump(mode="json"))
)
deployment_payload = asyncio.run(
h.save_deployment(
WorkflowDeployment(
id="echo.personal",
artifact_id="echo",
artifact_version=1,
bindings=[
{"logical_source": "demo", "concrete_source": "demo.personal"}
],
).model_dump(mode="json")
)
)
events = h.service.list_events()
assert artifact_payload["saved"] is True
assert deployment_payload["saved"] is True
assert [event.kind for event in events] == [
"workflow_artifact_saved",
"workflow_deployment_saved",
]
assert events[0].capability_id == "workflow.echo.v1"
assert events[1].capability_id == "deployment.echo.personal"
def test_workflow_surface_lists_compact_deployment_summaries_and_inspects_detail() -> (
None
):
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_deployment_list"
)
h = handlers(artifact_store)
deployment = WorkflowDeployment(
id="echo.personal",
artifact_id="echo",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
artifact_store.save_deployment(deployment)
listed = asyncio.run(h.list_deployments())
inspected = asyncio.run(h.inspect_deployment(deployment_id="echo.personal"))
assert listed["deployments"][0]["id"] == "echo.personal"
assert listed["deployments"][0]["binding_count"] == 1
assert "bindings" not in listed["deployments"][0]
assert inspected["bindings"][0]["logical_source"] == "demo"
assert inspected["bindings"][0]["concrete_source"] == "demo.personal"
@@ -0,0 +1,673 @@
from __future__ import annotations
import asyncio
from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment
from wf_mcp.broker import WfMcpService
from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
from wf_mcp.workflow_surface.models import CreateMinimalDraftWorkspaceRequest
from wf_core.models.steps import InputPathBinding, OutputBinding
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
from ..test_support import echo_tool, local_temp_root
from .conftest import (
echo_artifact,
echo_draft,
handlers,
mcp_echo_tool,
)
def test_workflow_surface_validates_draft_without_saving() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_draft_validate"
)
h = handlers(artifact_store)
payload = asyncio.run(h.validate_draft(draft=echo_draft()))
assert payload["status"] == "valid"
assert payload["diagnostics"] == []
assert payload["compiled_plan"]["nodes"][0]["type"] == "node"
assert not artifact_store.list_artifacts()
def test_workflow_surface_rejects_unknown_draft_route_outcome_when_spec_is_known() -> (
None
):
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_draft_bad_outcome"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_draft_bad_outcome_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
draft = echo_draft()
draft["routes"]["echo"] = {"typo": "__end__"}
payload = asyncio.run(h.validate_draft(draft=draft))
assert payload["status"] == "invalid"
assert payload["diagnostics"][0]["path"] == "routes.echo.typo"
def test_workflow_surface_creates_artifact_from_draft_with_binding_suggestions() -> (
None
):
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_draft_create"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_draft_create_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
draft = echo_draft()
draft["steps"]["echo"]["use"] = "demo.personal.echo_tool"
payload = asyncio.run(
h.create_artifact_from_draft(
artifact_id="draft_echo",
version=1,
title="Draft Echo",
draft=draft,
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact = artifact_store.get_artifact("draft_echo", 1)
assert payload["saved"] is True
assert payload["required_logical_sources"] == ["demo", "wf.std"]
assert payload["suggested_bindings"]["wf.std"] == "wf.std"
assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool"
assert artifact.required_capability_map()["demo.echo_tool"].logical_source == "demo"
def test_workflow_surface_draft_artifact_requires_std_self_binding() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_draft_missing_std"
)
h = handlers(artifact_store)
asyncio.run(
h.create_artifact_from_draft(
artifact_id="draft_echo_missing_std",
version=1,
title="Draft Echo Missing Std",
draft=echo_draft(),
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact_store.save_deployment(
WorkflowDeployment(
id="draft_echo_missing_std.personal",
artifact_id="draft_echo_missing_std",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
payload = asyncio.run(
h.validate_deployment(deployment_id="draft_echo_missing_std.personal")
)
assert payload["status"] == "unrunnable"
assert payload["diagnostics"][0]["code"] == "binding_missing"
assert payload["diagnostics"][0]["logical_ref"] == "wf.std.replace"
def test_workflow_surface_patches_draft_without_saving() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_draft_patch"
)
h = handlers(artifact_store)
payload = asyncio.run(
h.patch_draft(
draft=echo_draft(),
patch=[
{
"op": "replace",
"path": "/steps/echo/input/0/target/parts/0",
"value": "message",
}
],
)
)
assert payload["status"] == "valid"
assert payload["draft"]["steps"]["echo"]["input"][0]["target"] == {
"root": "local",
"parts": ["message"],
}
assert not artifact_store.list_artifacts()
def test_workflow_surface_creates_and_gets_draft_workspace() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_workspace")
h = handlers(artifact_store)
created = asyncio.run(
h.create_draft_workspace(
workspace_id="echo_draft",
title="Echo Draft",
draft=echo_draft(),
)
)
fetched = asyncio.run(
h.get_draft_workspace(
workspace_id="echo_draft",
include_draft=True,
)
)
assert created["workspace_id"] == "echo_draft"
assert created["revision"] == 1
assert fetched["draft"]["steps"]["echo"]["use"] == "demo.personal.echo_tool"
def test_workflow_surface_lists_draft_workspaces() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_list"
)
h = handlers(artifact_store)
asyncio.run(
h.create_draft_workspace(
workspace_id="b_draft",
draft=echo_draft(),
title="B Draft",
)
)
asyncio.run(
h.create_draft_workspace(
workspace_id="a_draft",
draft=echo_draft(),
title="A Draft",
)
)
payload = asyncio.run(h.list_draft_workspaces())
assert [workspace["workspace_id"] for workspace in payload["workspaces"]] == [
"a_draft",
"b_draft",
]
assert payload["workspaces"][0]["title"] == "A Draft"
assert "draft" not in payload["workspaces"][0]
def test_workflow_surface_deletes_draft_workspace() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_delete"
)
h = handlers(artifact_store)
asyncio.run(
h.create_draft_workspace(
workspace_id="echo_draft",
draft=echo_draft(),
)
)
deleted = asyncio.run(h.delete_draft_workspace(workspace_id="echo_draft"))
deleted_again = asyncio.run(
h.delete_draft_workspace(workspace_id="echo_draft")
)
listed = asyncio.run(h.list_draft_workspaces())
assert deleted["deleted"] is True
assert deleted["status"] == "deleted"
assert deleted_again["deleted"] is False
assert deleted_again["status"] == "not_found"
assert listed["workspaces"] == []
def test_workflow_surface_patch_helpers_update_draft_workspace() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_patch_helpers"
)
h = handlers(artifact_store)
asyncio.run(
h.create_draft_workspace(
workspace_id="echo_draft",
draft=echo_draft(),
)
)
named = asyncio.run(
h.set_draft_name(
workspace_id="echo_draft",
revision=1,
name="echo_v2",
)
)
routed = asyncio.run(
h.set_draft_route(
workspace_id="echo_draft",
revision=2,
step_id="echo",
outcome="error",
target="__end__",
)
)
input_mapped = asyncio.run(
h.set_step_input_map(
workspace_id="echo_draft",
revision=3,
step_id="echo",
input_map={"input.text": "message"},
)
)
output_mapped = asyncio.run(
h.set_step_output_map(
workspace_id="echo_draft",
revision=4,
step_id="echo",
output_map={"echoed": "state.echoed"},
)
)
fetched = asyncio.run(
h.get_draft_workspace(workspace_id="echo_draft", include_draft=True)
)
assert named["revision"] == 2
assert routed["revision"] == 3
assert input_mapped["revision"] == 4
assert output_mapped["revision"] == 5
assert fetched["draft"]["name"] == "echo_v2"
assert fetched["draft"]["routes"]["echo"]["error"] == "__end__"
assert fetched["draft"]["steps"]["echo"]["input"] == [
{
"target": {"root": "local", "parts": ["message"]},
"path": {"root": "input", "parts": ["text"]},
}
]
assert fetched["draft"]["steps"]["echo"]["output"] == [
{
"source": {"root": "local", "parts": ["echoed"]},
"target": {"root": "state", "parts": ["echoed"]},
}
]
def test_workflow_surface_validates_draft_workspace_with_live_outcomes() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_validate"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_validate_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
draft = echo_draft()
draft["routes"]["echo"] = {"typo": "__end__"}
asyncio.run(
h.create_draft_workspace(
workspace_id="echo_draft",
draft=draft,
)
)
payload = asyncio.run(h.validate_draft_workspace(workspace_id="echo_draft"))
fetched = asyncio.run(h.get_draft_workspace(workspace_id="echo_draft"))
assert payload["revision"] == 1
assert payload["status"] == "invalid"
assert payload["diagnostics"][0]["code"] == "unknown_outcome"
assert fetched["status"] == "invalid"
def test_workflow_surface_patches_draft_workspace_by_revision() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_patch"
)
h = handlers(artifact_store)
asyncio.run(
h.create_draft_workspace(
workspace_id="echo_draft",
draft=echo_draft(),
)
)
patched = asyncio.run(
h.patch_draft_workspace(
workspace_id="echo_draft",
revision=1,
patch=[{"op": "replace", "path": "/name", "value": "echo_v2"}],
)
)
assert patched["revision"] == 2
assert patched["status"] == "valid"
def test_workflow_surface_creates_minimal_draft_workspace_with_error_route() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_minimal_workspace"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_minimal_workspace_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", mcp_echo_tool)
h = WorkflowSurfaceHandlers(service)
result = asyncio.run(
h.create_minimal_draft_workspace(
workspace_id="echo_draft_static_error",
name="echo",
capability_name="demo.personal.mcp_echo_tool",
input_schema={
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
state_schema={"fields": {"echoed": {"type": "string"}}},
output_schema={
"type": "object",
"properties": {"echoed": {"type": "string"}},
"required": ["echoed"],
},
input_map={"input.text": "text"},
output_map={"echoed": "state.echoed"},
)
)
assert service.draft_workspace_store is not None
workspace = service.draft_workspace_store.get_workspace("echo_draft_static_error")
assert result["workspace_id"] == "echo_draft_static_error"
assert workspace.draft["routes"]["call"]["ok"] == "__end__"
assert workspace.draft["routes"]["call"]["error"] == "tool_error"
assert workspace.draft["steps"]["tool_error"]["use"] == "wf.std.runtime_error"
assert workspace.draft["steps"]["tool_error"]["input"] == [
{
"target": {"root": "local", "parts": ["message"]},
"value": "Capability call failed",
}
]
def test_workflow_surface_minimal_draft_honors_explicit_error_message_source() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_minimal_explicit_error_mcp"),
artifact_store=FileWorkflowArtifactStore(
local_temp_root() / "surface_minimal_explicit_error"
),
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", mcp_echo_tool)
h = WorkflowSurfaceHandlers(service)
asyncio.run(
h.create_minimal_draft_workspace(
workspace_id="echo_draft_explicit_error",
name="echo",
capability_name="demo.personal.mcp_echo_tool",
input_schema={"type": "object"},
state_schema={"fields": {"error_message": {"type": "string"}}},
output_schema={"type": "object"},
input_map={"input.text": "text"},
output_map={"echoed": "state.echoed"},
error_message_source=GraphSourcePath.state("error_message"),
)
)
assert service.draft_workspace_store is not None
workspace = service.draft_workspace_store.get_workspace("echo_draft_explicit_error")
assert workspace.draft["steps"]["tool_error"]["input"] == [
{
"target": {"root": "local", "parts": ["message"]},
"path": {"root": "state", "parts": ["error_message"]},
}
]
def test_minimal_draft_request_accepts_structural_error_message_source() -> None:
request = CreateMinimalDraftWorkspaceRequest.model_validate(
{
"workspace_id": "echo_draft_structural_error",
"name": "echo",
"capability_name": "demo.personal.mcp_echo_tool",
"input_schema": {"type": "object"},
"state_schema": {"type": "object"},
"output_schema": {"type": "object"},
"error_message_source": {
"root": "state",
"parts": ["error_message"],
},
}
)
assert isinstance(request.error_message_source, GraphSourcePath)
assert request.error_message_source.root == "state"
assert request.error_message_source.parts == ("error_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"
),
)
h = WorkflowSurfaceHandlers(service)
result = asyncio.run(
h.create_minimal_draft_workspace(
workspace_id="echo_draft_canonical",
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_canonical")
assert result["workspace_id"] == "echo_draft_canonical"
assert workspace.draft["steps"]["call"]["input"] == [
{
"target": {"root": "local", "parts": ["text"]},
"path": {"root": "input", "parts": ["text"]},
}
]
assert workspace.draft["steps"]["call"]["output"] == [
{
"source": {"root": "local", "parts": ["echoed"]},
"target": {"root": "state", "parts": ["echoed"]},
}
]
def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_from_capability"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_from_capability_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
result = asyncio.run(
h.create_draft_workspace_from_capability(
workspace_id="echo_from_capability_canonical",
capability_name="demo.personal.echo_tool",
name="echo_from_capability",
)
)
assert service.draft_workspace_store is not None
workspace = service.draft_workspace_store.get_workspace(
"echo_from_capability_canonical"
)
assert result["workspace_id"] == "echo_from_capability_canonical"
assert result["wrapper_hints"]["input_map"] == {"input.text": "text"}
assert result["wrapper_hints"]["output_map"] == {"echoed": "state.echoed"}
assert workspace.draft["steps"]["call"]["use"] == "demo.personal.echo_tool"
assert workspace.draft["steps"]["call"]["input"] == [
{
"target": {"root": "local", "parts": ["text"]},
"path": {"root": "input", "parts": ["text"]},
}
]
assert workspace.draft["steps"]["call"]["output"] == [
{
"source": {"root": "local", "parts": ["echoed"]},
"target": {"root": "state", "parts": ["echoed"]},
}
]
def test_workflow_surface_creates_artifact_from_workspace() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_artifact"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_artifact_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
asyncio.run(
h.create_draft_workspace(
workspace_id="echo_draft",
draft=echo_draft(),
)
)
result = asyncio.run(
h.create_artifact_from_workspace(
workspace_id="echo_draft",
artifact_id="workspace_echo",
version=1,
title="Workspace Echo",
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact = artifact_store.get_artifact("workspace_echo", 1)
assert result["saved"] is True
assert artifact.id == "workspace_echo"
assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool"
required = artifact.required_capability_map()["demo.echo_tool"]
assert required.kind == "node_spec"
assert str(required.observed_concrete_source) == "demo.personal"
assert required.input_schema_snapshot is not None
assert required.output_schema_snapshot is not None
def test_workflow_surface_workspace_artifact_infers_raw_concrete_dependency() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_artifact_raw_dependency"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_artifact_raw_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
asyncio.run(
h.create_draft_workspace(
workspace_id="echo_draft",
draft=echo_draft(),
)
)
asyncio.run(
h.create_artifact_from_workspace(
workspace_id="echo_draft",
artifact_id="workspace_echo_raw_dependency",
version=1,
title="Workspace Echo Raw Dependency",
outcomes=("completed",),
)
)
artifact = artifact_store.get_artifact("workspace_echo_raw_dependency", 1)
required = artifact.required_capability_map()["demo.personal.echo_tool"]
assert required.kind == "node_spec"
assert required.input_schema_snapshot is not None
assert required.output_schema_snapshot is not None
def test_workflow_surface_creates_wrapper_from_workspace() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_wrapper"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_wrapper_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
asyncio.run(
h.create_draft_workspace(
workspace_id="echo_draft",
draft=echo_draft(),
)
)
result = asyncio.run(
h.create_wrapper_from_workspace(
workspace_id="echo_draft",
artifact_id="workspace_echo_wrapper",
version=1,
title="Workspace Echo Wrapper",
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact = artifact_store.get_artifact("workspace_echo_wrapper", 1)
assert result["saved"] is True
assert artifact.kind == "wrapper"
assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool"
+412
View File
@@ -0,0 +1,412 @@
from __future__ import annotations
import asyncio
from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment
from wf_authoring import node, reducer
from wf_mcp.broker import WfMcpService
from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import TraceRange, WorkflowSurfaceHandlers
from wf_platform import (
CapabilityBuckets,
CapabilitySource,
SourcePermissions,
SourceVisibility,
)
from ..test_support import echo_tool, input_binding, local_temp_root, output_binding
from .conftest import (
AmountInput,
AmountOutput,
amount_tool,
changed_echo_tool,
custom_reducer_artifact,
echo_artifact,
echo_draft,
failing_artifact,
failing_tool,
handlers,
logical_echo_artifact,
multiply,
)
def test_raw_workflow_plan_uses_core_step_and_edge_models() -> None:
from wf_mcp.models import RawWorkflowPlan
plan = RawWorkflowPlan.model_validate(echo_artifact().plan)
assert plan.nodes[0].type == "node"
assert plan.edges[0].outcome == "ok"
def test_workflow_surface_runs_non_interrupting_deployment() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_run")
artifact_store.save_artifact(echo_artifact())
artifact_store.save_deployment(
WorkflowDeployment(
id="echo.personal",
artifact_id="echo",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_run_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.run_deployment(
deployment_id="echo.personal",
workflow_input={"text": "hello"},
)
)
assert payload["status"] == "completed"
assert isinstance(payload["run_id"], str)
assert payload["output"]["echoed"] == "hello"
assert payload["diagnostics"] == []
assert payload["trace_count"] == 1
assert "trace" not in payload
inspected = asyncio.run(h.inspect_run(run_id=payload["run_id"]))
traced = asyncio.run(
h.read_run_trace(
run_id=payload["run_id"],
trace_range=TraceRange(start=0, limit=1),
)
)
assert inspected["run_id"] == payload["run_id"]
assert inspected["status"] == "completed"
assert inspected["trace_count"] == 1
assert "trace" not in inspected
assert traced["trace_count"] == 1
assert traced["trace_start"] == 0
assert traced["trace_limit"] == 1
assert traced["trace"][0]["node_id"] == "echo"
assert traced["trace_truncated"] is False
def test_workflow_surface_failed_deployment_exposes_error_on_run_and_inspect() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_failed_run_error"
)
artifact_store.save_artifact(failing_artifact())
artifact_store.save_deployment(
WorkflowDeployment(
id="fail.personal",
artifact_id="fail",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_failed_run_error_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", failing_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.run_deployment(
deployment_id="fail.personal",
workflow_input={"message": "hello"},
)
)
inspected = asyncio.run(h.inspect_run(run_id=payload["run_id"]))
assert payload["status"] == "failed"
assert "upstream exploded" in payload["error"]
assert payload["trace_count"] == 0
assert inspected["status"] == "failed"
assert inspected["error"] == payload["error"]
def test_workflow_surface_run_deployment_can_include_trace_detail() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_run_trace_detail"
)
artifact_store.save_artifact(echo_artifact())
artifact_store.save_deployment(
WorkflowDeployment(
id="echo.personal",
artifact_id="echo",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_run_trace_detail_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.run_deployment(
deployment_id="echo.personal",
workflow_input={"text": "hello"},
trace_range=TraceRange(start=0, limit=10),
)
)
assert payload["status"] == "completed"
assert payload["trace_count"] == 1
assert payload["trace_start"] == 0
assert payload["trace_limit"] == 10
assert payload["trace_truncated"] is False
assert len(payload["trace"]) == 1
assert payload["trace"][0]["node_id"] == "echo"
assert payload["trace"][0]["outcome"] == "ok"
def test_workflow_surface_run_deployment_can_read_empty_trace_range() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_run_trace_empty_range"
)
artifact_store.save_artifact(echo_artifact())
artifact_store.save_deployment(
WorkflowDeployment(
id="echo.personal",
artifact_id="echo",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_run_trace_empty_range_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.run_deployment(
deployment_id="echo.personal",
workflow_input={"text": "hello"},
trace_range=TraceRange(start=5, limit=10),
)
)
assert payload["trace_count"] == 1
assert payload["trace_start"] == 5
assert payload["trace_limit"] == 10
assert payload["trace"] == []
assert payload["trace_truncated"] is False
def test_workflow_surface_runs_deployment_with_bound_node_spec_dependency() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_bound_node")
artifact_store.save_artifact(logical_echo_artifact())
artifact_store.save_deployment(
WorkflowDeployment(
id="echo.personal",
artifact_id="logical_echo",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_bound_node_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.run_deployment(
deployment_id="echo.personal",
workflow_input={"text": "hello"},
)
)
assert payload["status"] == "completed"
assert payload["output"]["echoed"] == "hello"
assert payload["diagnostics"] == []
def test_workflow_surface_runs_artifact_created_from_concrete_node_ref() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_created_bound_node"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_created_bound_node_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
asyncio.run(
h.create_artifact_from_plan(
artifact_id="created_echo",
version=1,
title="Created Echo",
plan=echo_artifact().plan,
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact_store.save_deployment(
WorkflowDeployment(
id="created_echo.personal",
artifact_id="created_echo",
artifact_version=1,
bindings={
"demo": "demo.personal",
"wf.std": "wf.std",
},
)
)
payload = asyncio.run(
h.run_deployment(
deployment_id="created_echo.personal",
workflow_input={"text": "hello"},
)
)
artifact = artifact_store.get_artifact("created_echo", 1)
assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool"
assert artifact.required_capability_map()["demo.echo_tool"].logical_source == "demo"
assert payload["status"] == "completed"
assert payload["output"]["echoed"] == "hello"
assert payload["diagnostics"] == []
def test_workflow_surface_detects_drift_from_saved_node_spec_snapshot() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_created_drift"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_created_drift_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
asyncio.run(
h.create_artifact_from_plan(
artifact_id="created_echo_drift",
version=1,
title="Created Echo Drift",
plan=echo_artifact().plan,
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact_store.save_deployment(
WorkflowDeployment(
id="created_echo_drift.personal",
artifact_id="created_echo_drift",
artifact_version=1,
bindings={
"demo": "demo.personal",
"wf.std": "wf.std",
},
)
)
required = artifact_store.get_artifact(
"created_echo_drift",
1,
).required_capability_map()["demo.echo_tool"]
assert required.input_schema_hash is not None
service.register_connection(
ConnectionConfig(id="demo.work", server="demo", account="work")
)
service.register_specs("demo.work", changed_echo_tool)
artifact_store.save_deployment(
WorkflowDeployment(
id="created_echo_drift.work",
artifact_id="created_echo_drift",
artifact_version=1,
bindings={
"demo": "demo.work",
"wf.std": "wf.std",
},
)
)
payload = asyncio.run(
h.validate_deployment(deployment_id="created_echo_drift.work")
)
assert payload["status"] == "unrunnable"
assert payload["diagnostics"][0]["code"] == "schema_changed"
def test_workflow_surface_runs_deployment_with_bound_reducer_dependency() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_reducer")
artifact_store.save_artifact(custom_reducer_artifact())
artifact_store.save_deployment(
WorkflowDeployment(
id="multiply.personal",
artifact_id="multiply",
artifact_version=1,
bindings={
"demo": "demo.personal",
"custom": "custom.default",
},
)
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_reducer_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", amount_tool)
service.register_capability_source(
CapabilitySource(
id="custom.default",
kind="system",
capabilities=CapabilityBuckets(
reducers={multiply.definition.spec.name: multiply.definition.spec},
reducer_definitions={
multiply.definition.spec.name: multiply.definition,
},
),
visibility=SourceVisibility(planner=True),
permissions=SourcePermissions(safe_for_workflow=True),
)
)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.run_deployment(
deployment_id="multiply.personal",
workflow_input={"total": 2, "amount": 3},
)
)
assert payload["status"] == "completed"
assert payload["output"]["total"] == 6
assert payload["diagnostics"] == []
@@ -0,0 +1,172 @@
from __future__ import annotations
import asyncio
from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment
from wf_mcp.broker import WfMcpService
from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
from ..test_support import echo_tool, local_temp_root
from .conftest import echo_artifact, logical_echo_artifact
def test_workflow_surface_creates_wrapper_artifact_from_plan() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_wrapper_plan"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_wrapper_plan_mcp"),
artifact_store=artifact_store,
)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.create_artifact_from_plan(
artifact_id="echo_wrapper",
version=1,
title="Echo Wrapper",
kind="wrapper",
plan=echo_artifact().plan,
outcomes=("completed",),
)
)
artifact = artifact_store.get_artifact("echo_wrapper", 1)
assert payload["saved"] is True
assert artifact.kind == "wrapper"
def test_workflow_surface_creates_artifact_with_logical_node_refs() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_logical_refs"
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_logical_refs_mcp"),
artifact_store=artifact_store,
)
h = WorkflowSurfaceHandlers(service)
plan = echo_artifact().plan
plan["nodes"][0]["node"] = "demo.personal.echo_tool"
asyncio.run(
h.create_artifact_from_plan(
artifact_id="echo_logical",
version=1,
title="Echo Logical",
plan=plan,
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact = artifact_store.get_artifact("echo_logical", 1)
assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool"
assert artifact.required_capability_map()["demo.echo_tool"].logical_source == "demo"
def test_workflow_surface_calls_saved_wrapper_artifact() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_wrapper_call"
)
wrapper = echo_artifact().model_copy(
update={"id": "echo_wrapper", "kind": "wrapper"}
)
artifact_store.save_artifact(wrapper)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_wrapper_call_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.call_capability(
qualified_name="workflow.echo_wrapper.v1",
payload={"text": "hello"},
)
)
assert payload["qualified_name"] == "workflow.echo_wrapper.v1"
assert payload["source_id"] == "workflow"
assert payload["kind"] == "wrapper_artifact"
assert payload["diagnostics"] == []
assert payload["outcome"] == "completed"
assert payload["output"]["echoed"] == "hello"
def test_workflow_surface_calls_live_node_spec_with_self_describing_response() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_live_capability_call"),
artifact_store=FileWorkflowArtifactStore(
local_temp_root() / "surface_live_capability_call_artifacts"
),
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.call_capability(
qualified_name="demo.personal.echo_tool",
payload={"text": "hello"},
)
)
assert payload["qualified_name"] == "demo.personal.echo_tool"
assert payload["source_id"] == "demo.personal"
assert payload["kind"] == "node_spec"
assert payload["diagnostics"] == []
assert payload["outcome"] == "ok"
assert payload["output"]["echoed"] == "hello"
def test_workflow_surface_calls_saved_wrapper_artifact_with_deployment_bindings() -> (
None
):
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_wrapper_bound_call"
)
wrapper = logical_echo_artifact().model_copy(
update={"id": "logical_echo_wrapper", "kind": "wrapper"}
)
artifact_store.save_artifact(wrapper)
artifact_store.save_deployment(
WorkflowDeployment(
id="logical_echo_wrapper.personal",
artifact_id="logical_echo_wrapper",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_wrapper_bound_call_mcp"),
artifact_store=artifact_store,
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
h = WorkflowSurfaceHandlers(service)
payload = asyncio.run(
h.call_capability(
qualified_name="workflow.logical_echo_wrapper.v1",
payload={"text": "hello"},
deployment_id="logical_echo_wrapper.personal",
)
)
assert payload["qualified_name"] == "workflow.logical_echo_wrapper.v1"
assert payload["source_id"] == "workflow"
assert payload["kind"] == "wrapper_artifact"
assert payload["deployment_id"] == "logical_echo_wrapper.personal"
assert payload["diagnostics"] == []
assert payload["outcome"] == "completed"
assert payload["output"]["echoed"] == "hello"