Files
lda-wf/tests/wf_mcp/workflow_surface/test_drafts.py
T

853 lines
29 KiB
Python

from __future__ import annotations
import asyncio
from pathlib import Path
from typing import Any
import pytest
from pydantic import ValidationError
from wf_artifacts import (
FileDraftWorkspaceStore,
FileWorkflowArtifactStore,
WorkflowDeployment,
)
from wf_core.models.steps import InputPathBinding, InputValueBinding, OutputBinding
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
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 (
AddStepFromCapabilityRequest,
CreateMinimalDraftWorkspaceRequest,
SetStepInputBindingsRequest,
SetStepOutputBindingsRequest,
SetWorkflowOutputBindingsRequest,
UpdateCapabilityStepRequest,
)
from ..test_support import echo_tool
from .conftest import (
ContentOnlyOutputAdapter,
echo_draft,
handlers,
mcp_echo_tool,
)
def test_update_capability_step_request_preserves_field_presence_and_binding_type() -> (
None
):
request = UpdateCapabilityStepRequest.model_validate(
{
"workspace_id": "report",
"revision": 4,
"step_id": "publish",
"update": {
"desc": None,
"input": [
{"value": "markdown", "target": "request.format"},
],
},
}
)
assert request.update.model_fields_set == {"desc", "input"}
assert request.update.desc is None
assert request.update.input is not None
assert isinstance(request.update.input[0], InputValueBinding)
@pytest.mark.parametrize(
"update",
[
{},
{"input": None},
{"retry": -1},
{"timeout_seconds": 0},
{"unknown": "field"},
],
)
def test_update_capability_step_request_rejects_invalid_patch(
update: dict[str, Any],
) -> None:
with pytest.raises(ValidationError):
UpdateCapabilityStepRequest.model_validate(
{
"workspace_id": "report",
"revision": 4,
"step_id": "publish",
"update": update,
}
)
def test_add_step_from_capability_request_preserves_creation_parity() -> None:
request = AddStepFromCapabilityRequest.model_validate(
{
"workspace_id": "report",
"revision": 3,
"step_id": "publish",
"capability_name": "local.report.publish",
"input_bindings": [
{"path": "state.report.title", "target": "request.title"},
{"value": "markdown", "target": "request.format"},
],
"desc": "Publish report",
"retry": 0,
"timeout_seconds": 30,
}
)
assert request.input_map is None
assert request.input_bindings is not None
assert isinstance(request.input_bindings[0], InputPathBinding)
assert isinstance(request.input_bindings[1], InputValueBinding)
assert request.retry == 0
def test_add_step_from_capability_request_rejects_both_input_forms() -> None:
with pytest.raises(ValidationError, match="mutually exclusive"):
AddStepFromCapabilityRequest.model_validate(
{
"workspace_id": "report",
"revision": 3,
"step_id": "publish",
"capability_name": "local.report.publish",
"input_map": {"state.title": "request.title"},
"input_bindings": [
{"value": "markdown", "target": "request.format"},
],
}
)
def test_add_step_from_capability_request_rejects_explicit_null_input_form() -> None:
with pytest.raises(ValidationError, match="mutually exclusive"):
AddStepFromCapabilityRequest.model_validate(
{
"workspace_id": "report",
"revision": 3,
"step_id": "publish",
"capability_name": "local.report.publish",
"input_map": None,
"input_bindings": [
{"value": "markdown", "target": "request.format"},
],
}
)
def test_workflow_surface_rejects_unknown_draft_route_outcome_when_spec_is_known(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_draft_bad_outcome")
service = WfMcpService(
store=FileStore(tmp_path / "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"] == "edges[0].outcome"
def test_workflow_surface_creates_artifact_from_draft_with_binding_suggestions(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_draft_create")
service = WfMcpService(
store=FileStore(tmp_path / "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"]
assert payload["suggested_bindings"] == {}
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_with_platform_source_succeeds(
tmp_path: Path,
) -> None:
"""Platform sources like wf.std don't require explicit bindings."""
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_draft_platform")
h = handlers(artifact_store)
asyncio.run(
h.create_artifact_from_draft(
artifact_id="draft_echo_platform",
version=1,
title="Draft Echo Platform",
draft=echo_draft(),
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact_store.save_deployment(
WorkflowDeployment(
id="draft_echo_platform.personal",
artifact_id="draft_echo_platform",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
payload = asyncio.run(
h.validate_deployment(deployment_id="draft_echo_platform.personal")
)
# wf.std is a platform source, so no binding_missing diagnostic for it
# The only diagnostic should be source_missing for demo.personal (not registered)
assert payload["status"] == "unrunnable"
assert len(payload["diagnostics"]) == 1
assert payload["diagnostics"][0]["code"] == "source_missing"
assert payload["diagnostics"][0]["logical_ref"] == "demo.echo_tool"
def test_workflow_surface_validates_draft_workspace_with_live_outcomes(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_workspace_validate")
service = WfMcpService(
store=FileStore(tmp_path / "surface_workspace_validate_mcp"),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_workspace_validate_mcp"
),
)
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"] == "undeclared_edge_outcome"
assert fetched["status"] == "invalid"
def test_workflow_surface_creates_minimal_draft_workspace_with_error_route(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_minimal_workspace")
service = WfMcpService(
store=FileStore(tmp_path / "surface_minimal_workspace_mcp"),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_minimal_workspace_mcp"
),
)
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": "message",
"value": "Capability call failed",
}
]
def test_workflow_surface_minimal_draft_honors_explicit_error_message_source(
tmp_path: Path,
) -> None:
service = WfMcpService(
store=FileStore(tmp_path / "surface_minimal_explicit_error_mcp"),
artifact_store=FileWorkflowArtifactStore(
tmp_path / "surface_minimal_explicit_error"
),
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_minimal_explicit_error_mcp"
),
)
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": "message",
"path": "state.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_set_step_input_bindings_request_accepts_path_value_and_null() -> None:
request = SetStepInputBindingsRequest.model_validate(
{
"workspace_id": "concat_draft",
"revision": 1,
"step_id": "call",
"bindings": [
{
"target": {"root": "local", "parts": ["items"]},
"path": {"root": "input", "parts": ["items"]},
},
{
"target": {"root": "local", "parts": ["separator"]},
"value": None,
},
],
}
)
assert isinstance(request.bindings[0], InputPathBinding)
assert request.bindings[0].path == GraphSourcePath.input("items")
assert isinstance(request.bindings[1], InputValueBinding)
assert request.bindings[1].value is None
def test_set_step_output_bindings_request_preserves_ordered_source_fan_out() -> None:
request = SetStepOutputBindingsRequest.model_validate(
{
"workspace_id": "draft-output",
"revision": 4,
"step_id": "analyze",
"bindings": [
{"source": "report.title", "target": "state.report.title"},
{"source": "report.title", "target": "state.audit.title"},
],
}
)
assert [str(binding.source) for binding in request.bindings] == [
"report.title",
"report.title",
]
def test_set_step_output_bindings_request_rejects_malformed_canonical_record() -> None:
with pytest.raises(ValidationError):
SetStepOutputBindingsRequest.model_validate(
{
"workspace_id": "draft-output",
"revision": 4,
"step_id": "analyze",
"bindings": [{"source": "report.title"}],
}
)
def test_set_workflow_output_bindings_request_preserves_union_order() -> None:
request = SetWorkflowOutputBindingsRequest.model_validate(
{
"workspace_id": "draft-output",
"revision": 4,
"bindings": [
{"path": "state.report.title", "target": "report.title"},
{"value": "markdown", "target": "format"},
],
}
)
assert isinstance(request.bindings[0], InputPathBinding)
assert isinstance(request.bindings[1], InputValueBinding)
assert [binding.model_dump(mode="json") for binding in request.bindings] == [
{"path": "state.report.title", "target": "report.title"},
{"value": "markdown", "target": "format"},
]
def test_set_workflow_output_bindings_request_rejects_malformed_record() -> None:
with pytest.raises(ValidationError):
SetWorkflowOutputBindingsRequest.model_validate(
{
"workspace_id": "draft-output",
"revision": 4,
"bindings": [{"target": "report.title"}],
}
)
def test_workflow_surface_sets_ordered_canonical_step_output_bindings(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_output_bindings")
service = WfMcpService(
store=FileStore(tmp_path / "surface_output_bindings_mcp"),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_output_bindings_mcp"
),
)
h = WorkflowSurfaceHandlers(service)
created = asyncio.run(
h.create_minimal_draft_workspace(
workspace_id="echo_draft",
name="echo_draft",
capability_name="wf.std.constant",
input_schema={"type": "object"},
state_schema={"fields": {"report": {}, "audit": {}}},
output_schema={"type": "object"},
)
)
result = asyncio.run(
h.set_step_output_bindings(
workspace_id="echo_draft",
revision=created["revision"],
step_id="call",
bindings=[
OutputBinding(
source=LocalPath.parse("value"),
target=StatePath.parse("state.report"),
),
OutputBinding(
source=LocalPath.parse("value"),
target=StatePath.parse("state.audit"),
),
],
)
)
inspected = asyncio.run(
h.get_draft_workspace(workspace_id="echo_draft", include_draft=True)
)
assert result["revision"] == created["revision"] + 1
assert inspected["draft"]["steps"]["call"]["output"] == [
{"source": "value", "target": "state.report"},
{"source": "value", "target": "state.audit"},
]
def test_workflow_surface_sets_ordered_canonical_step_input_bindings(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_input_bindings")
service = WfMcpService(
store=FileStore(tmp_path / "surface_input_bindings_mcp"),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_input_bindings_mcp"
),
)
h = WorkflowSurfaceHandlers(service)
created = asyncio.run(
h.create_draft_workspace_from_capability(
workspace_id="concat_draft",
capability_name="wf.std.concat",
name="concat_draft",
)
)
result = asyncio.run(
h.set_step_input_bindings(
workspace_id="concat_draft",
revision=created["revision"],
step_id="call",
bindings=[
InputPathBinding(
target=LocalPath.of("items"),
path=GraphSourcePath.input("items"),
),
InputValueBinding(
target=LocalPath.of("separator"),
value="\n",
),
],
)
)
inspected = asyncio.run(
h.get_draft_workspace(workspace_id="concat_draft", include_draft=True)
)
assert result["revision"] == created["revision"] + 1
assert inspected["draft"]["steps"]["call"]["input"] == [
{"target": "items", "path": "input.items"},
{"target": "separator", "value": "\n"},
]
def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace(
tmp_path: Path,
) -> None:
service = WfMcpService(
store=FileStore(tmp_path / "surface_minimal_canonical_mcp"),
artifact_store=FileWorkflowArtifactStore(
tmp_path / "surface_minimal_canonical"
),
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_minimal_canonical_mcp"
),
)
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": "text",
"path": "input.text",
}
]
assert workspace.draft["steps"]["call"]["output"] == [
{
"source": "echoed",
"target": "state.echoed",
}
]
def test_workflow_surface_creates_draft_workspace_from_capability_hints(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(
tmp_path / "surface_workspace_from_capability"
)
service = WfMcpService(
store=FileStore(tmp_path / "surface_workspace_from_capability_mcp"),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_workspace_from_capability_mcp"
),
)
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"}
next_actions = result["next_actions"]
assert next_actions["can_continue"] is True
assert next_actions["can_save_now"] is True
assert (
next_actions["recommended_next_tool"] == "wf.workflow.validate_draft_workspace"
)
assert "high confidence" in next_actions["reason"]
assert next_actions["patch_examples"] == []
assert next_actions["warnings"] == []
assert workspace.draft["steps"]["call"]["use"] == "demo.personal.echo_tool"
assert workspace.draft["steps"]["call"]["input"] == [
{
"target": "text",
"path": "input.text",
}
]
assert workspace.draft["steps"]["call"]["output"] == [
{
"source": "echoed",
"target": "state.echoed",
}
]
def test_workflow_surface_creates_artifact_from_workspace(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_workspace_artifact")
service = WfMcpService(
store=FileStore(tmp_path / "surface_workspace_artifact_mcp"),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_workspace_artifact_mcp"
),
)
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(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(
tmp_path / "surface_workspace_artifact_raw_dependency"
)
service = WfMcpService(
store=FileStore(tmp_path / "surface_workspace_artifact_raw_mcp"),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_workspace_artifact_raw_mcp"
),
)
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(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_workspace_wrapper")
service = WfMcpService(
store=FileStore(tmp_path / "surface_workspace_wrapper_mcp"),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_workspace_wrapper_mcp"
),
)
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"
def test_workflow_surface_low_confidence_draft_returns_patch_guidance(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(
tmp_path / "surface_workspace_low_confidence"
)
service = WfMcpService(
store=FileStore(tmp_path / "surface_workspace_low_confidence_mcp"),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(
tmp_path / "surface_workspace_low_confidence_mcp"
),
)
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)
result = asyncio.run(
h.create_draft_workspace_from_capability(
workspace_id="content_wrapper",
capability_name="everything.default.echo",
name="content_wrapper",
)
)
next_actions = result["next_actions"]
assert next_actions["can_continue"] is True
assert next_actions["can_save_now"] is False
assert next_actions["recommended_next_tool"] == "wf.workflow.patch_draft_workspace"
assert "missing wrapper decisions" in next_actions["reason"]
assert (
next_actions["patch_examples"][0]["tool"] == "wf.workflow.patch_draft_workspace"
)
assert (
next_actions["patch_examples"][0]["request"]["workspace_id"]
== "content_wrapper"
)
assert (
next_actions["patch_examples"][0]["request"]["revision"] == result["revision"]
)
assert next_actions["warnings"]