second slice: drafts system

This commit is contained in:
lda
2026-06-02 00:15:03 +07:00 Verified
parent 6fcd6b7593
commit fb8baf02ea
14 changed files with 1303 additions and 259 deletions
+221
View File
@@ -0,0 +1,221 @@
from __future__ import annotations
import asyncio
from typing import Any
from wf_artifacts import FileWorkflowArtifactStore
from wf_api.drafts import WorkflowDraftApi
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.broker.service.workflow_operation_context import context_from_service
from tests.wf_mcp.test_support import echo_tool, local_temp_root
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 _draft_api(
artifact_store: FileWorkflowArtifactStore,
*,
register_echo: bool = False,
) -> tuple[WorkflowDraftApi, WfMcpService]:
service = WfMcpService(
store=FileStore(artifact_store.root / "drafts_mcp" / str(id(artifact_store))),
artifact_store=artifact_store,
)
if register_echo:
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
context = context_from_service(service)
return WorkflowDraftApi(context), service
def test_patch_draft_applies_json_patch() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "drafts_patch")
api, _service = _draft_api(artifact_store)
result = asyncio.run(
api.patch_draft(
draft=_echo_draft(),
patch=[
{
"op": "replace",
"path": "/steps/echo/input/0/target/parts/0",
"value": "message",
}
],
)
)
assert result["status"] == "valid"
assert result["draft"]["steps"]["echo"]["input"][0]["target"] == {
"root": "local",
"parts": ["message"],
}
def test_create_draft_workspace_creates_workspace() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "drafts_create_workspace"
)
api, _service = _draft_api(artifact_store)
result = asyncio.run(
api.create_draft_workspace(
workspace_id="echo_ws",
title="Echo Workspace",
draft=_echo_draft(),
)
)
assert result["workspace_id"] == "echo_ws"
assert result["revision"] == 1
def test_patch_draft_workspace_updates_revision() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "drafts_patch_workspace"
)
api, _service = _draft_api(artifact_store)
asyncio.run(
api.create_draft_workspace(
workspace_id="echo_ws",
draft=_echo_draft(),
)
)
patched = asyncio.run(
api.patch_draft_workspace(
workspace_id="echo_ws",
revision=1,
patch=[{"op": "replace", "path": "/name", "value": "echo_v2"}],
)
)
assert patched["revision"] == 2
assert patched["status"] == "valid"
def test_validate_draft_workspace_refreshes_status() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "drafts_validate_workspace"
)
api, service = _draft_api(artifact_store, register_echo=True)
draft = _echo_draft()
draft["routes"]["echo"] = {"typo": "__end__"}
asyncio.run(
api.create_draft_workspace(
workspace_id="echo_ws",
draft=draft,
)
)
payload = asyncio.run(api.validate_draft_workspace(workspace_id="echo_ws"))
fetched = asyncio.run(api.get_draft_workspace(workspace_id="echo_ws"))
assert payload["revision"] == 1
assert payload["status"] == "invalid"
assert payload["diagnostics"][0]["code"] == "unknown_outcome"
assert fetched["status"] == "invalid"
def test_create_minimal_draft_workspace_with_error_route() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "drafts_minimal_workspace"
)
api, _service = _draft_api(artifact_store, register_echo=True)
result = asyncio.run(
api.create_minimal_draft_workspace(
workspace_id="echo_minimal",
name="echo",
capability_name="demo.personal.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 result["workspace_id"] == "echo_minimal"
fetched = asyncio.run(
api.get_draft_workspace(workspace_id="echo_minimal", include_draft=True)
)
assert fetched["draft"]["routes"]["call"]["ok"] == "__end__"
assert fetched["draft"]["steps"]["call"]["use"] == "demo.personal.echo_tool"
def test_delegation_smoke_validate_draft_equivalence() -> None:
"""WorkflowSurfaceHandlers.validate_draft delegates to WorkflowDraftApi."""
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "drafts_delegation_smoke"
)
service = WfMcpService(
store=FileStore(artifact_store.root / "delegation_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)
context = context_from_service(service)
api = WorkflowDraftApi(context)
draft = _echo_draft()
handler_result = asyncio.run(h.validate_draft(draft=draft))
api_result = asyncio.run(api.validate_draft(draft=draft))
assert handler_result["status"] == api_result["status"]
assert handler_result["diagnostics"] == api_result["diagnostics"]
assert (
handler_result["compiled_plan"]["nodes"] == api_result["compiled_plan"]["nodes"]
)
+6 -2
View File
@@ -15,14 +15,18 @@ def test_wf_api_has_no_wf_mcp_imports() -> None:
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module is not None:
if node.module.startswith("wf_mcp") or node.module.startswith("wf_mcp."):
if node.module.startswith("wf_mcp") or node.module.startswith(
"wf_mcp."
):
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name.startswith("wf_mcp"):
violations.append(f"{module}:{node.lineno}: import {alias.name}")
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_api imports wf_mcp — this breaks the dependency direction rule:\n"
+10 -3
View File
@@ -10,7 +10,9 @@ from wf_mcp.broker.service.workflow_operation_context import context_from_servic
def test_wf_api_operation_context_imports_no_wf_mcp() -> None:
path = Path(__file__).resolve().parents[2] / "src" / "wf_api" / "operation_context.py"
path = (
Path(__file__).resolve().parents[2] / "src" / "wf_api" / "operation_context.py"
)
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
violations: list[str] = []
@@ -49,9 +51,14 @@ def test_context_from_service_exposes_existing_store_objects(tmp_path: Path) ->
assert isinstance(operation_context, WorkflowOperationContext)
assert operation_context.artifact_store is cli_context.service.artifact_store
assert operation_context.draft_workspace_store is cli_context.service.draft_workspace_store
assert (
operation_context.draft_workspace_store
is cli_context.service.draft_workspace_store
)
assert operation_context.run_store is cli_context.service.run_store
assert operation_context.capability_sources is cli_context.service.capability_sources
assert (
operation_context.capability_sources is cli_context.service.capability_sources
)
def test_context_from_service_delegates_specs_and_events(tmp_path: Path) -> None: