feat: expose capability step updates over rpc
This commit is contained in:
@@ -11,6 +11,7 @@ from .artifacts import WorkflowArtifactApi
|
|||||||
from .capabilities import WorkflowCapabilityApi
|
from .capabilities import WorkflowCapabilityApi
|
||||||
from .deployments import WorkflowDeploymentApi
|
from .deployments import WorkflowDeploymentApi
|
||||||
from .draft_authoring import RouteSource, WorkflowDraftAuthoringApi
|
from .draft_authoring import RouteSource, WorkflowDraftAuthoringApi
|
||||||
|
from .draft_updates import CapabilityStepUpdate
|
||||||
from .drafts import WorkflowDraftApi
|
from .drafts import WorkflowDraftApi
|
||||||
from .models import RawWorkflowPlan
|
from .models import RawWorkflowPlan
|
||||||
from .operation_context import WorkflowOperationContext
|
from .operation_context import WorkflowOperationContext
|
||||||
@@ -439,6 +440,21 @@ class WorkflowApi:
|
|||||||
bindings=bindings,
|
bindings=bindings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def update_capability_step(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
update: CapabilityStepUpdate,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self.draft_authoring.update_capability_step(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
step_id=step_id,
|
||||||
|
update=update,
|
||||||
|
)
|
||||||
|
|
||||||
async def set_step_output_map(
|
async def set_step_output_map(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -512,7 +528,11 @@ class WorkflowApi:
|
|||||||
route_from_outcome: str = "ok",
|
route_from_outcome: str = "ok",
|
||||||
routes: dict[str, str] | None = None,
|
routes: dict[str, str] | None = None,
|
||||||
input_map: dict[str, str] | None = None,
|
input_map: dict[str, str] | None = None,
|
||||||
|
input_bindings: Sequence[InputBinding] | None = None,
|
||||||
bind_outputs: dict[str, str] | None = None,
|
bind_outputs: dict[str, str] | None = None,
|
||||||
|
desc: str | None = None,
|
||||||
|
retry: int | None = None,
|
||||||
|
timeout_seconds: int | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.draft_authoring.add_step_from_capability(
|
return await self.draft_authoring.add_step_from_capability(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
@@ -523,7 +543,11 @@ class WorkflowApi:
|
|||||||
route_from_outcome=route_from_outcome,
|
route_from_outcome=route_from_outcome,
|
||||||
routes=routes,
|
routes=routes,
|
||||||
input_map=input_map,
|
input_map=input_map,
|
||||||
|
input_bindings=input_bindings,
|
||||||
bind_outputs=bind_outputs,
|
bind_outputs=bind_outputs,
|
||||||
|
desc=desc,
|
||||||
|
retry=retry,
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def add_step(
|
async def add_step(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from wf_artifacts.drafts.models import DraftStep
|
|||||||
from wf_core.models.steps import InputBinding, OutputBinding
|
from wf_core.models.steps import InputBinding, OutputBinding
|
||||||
|
|
||||||
from .draft_authoring import RouteSource
|
from .draft_authoring import RouteSource
|
||||||
|
from .draft_updates import CapabilityStepUpdate
|
||||||
from .runs import TraceRangeLike
|
from .runs import TraceRangeLike
|
||||||
|
|
||||||
|
|
||||||
@@ -156,6 +157,15 @@ class WorkflowDraftSurface(Protocol):
|
|||||||
bindings: Sequence[OutputBinding],
|
bindings: Sequence[OutputBinding],
|
||||||
) -> dict[str, Any]: ...
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
async def update_capability_step(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
update: CapabilityStepUpdate,
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
async def set_step_output_map(
|
async def set_step_output_map(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -204,7 +214,11 @@ class WorkflowDraftSurface(Protocol):
|
|||||||
route_from_outcome: str = "ok",
|
route_from_outcome: str = "ok",
|
||||||
routes: dict[str, str] | None = None,
|
routes: dict[str, str] | None = None,
|
||||||
input_map: dict[str, str] | None = None,
|
input_map: dict[str, str] | None = None,
|
||||||
|
input_bindings: Sequence[InputBinding] | None = None,
|
||||||
bind_outputs: dict[str, str] | None = None,
|
bind_outputs: dict[str, str] | None = None,
|
||||||
|
desc: str | None = None,
|
||||||
|
retry: int | None = None,
|
||||||
|
timeout_seconds: int | None = None,
|
||||||
) -> dict[str, Any]: ...
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
async def add_step(
|
async def add_step(
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from .models import (
|
|||||||
SetWorkflowOutputMapParams,
|
SetWorkflowOutputMapParams,
|
||||||
StartRunParams,
|
StartRunParams,
|
||||||
TraceRangeParams,
|
TraceRangeParams,
|
||||||
|
UpdateCapabilityStepParams,
|
||||||
ValidateDeploymentParams,
|
ValidateDeploymentParams,
|
||||||
ValidateDraftParams,
|
ValidateDraftParams,
|
||||||
ValidateDraftWorkspaceParams,
|
ValidateDraftWorkspaceParams,
|
||||||
@@ -104,6 +105,7 @@ __all__ = [
|
|||||||
"SetWorkflowOutputMapParams",
|
"SetWorkflowOutputMapParams",
|
||||||
"StartRunParams",
|
"StartRunParams",
|
||||||
"TraceRangeParams",
|
"TraceRangeParams",
|
||||||
|
"UpdateCapabilityStepParams",
|
||||||
"ValidateDeploymentParams",
|
"ValidateDeploymentParams",
|
||||||
"ValidateDraftParams",
|
"ValidateDraftParams",
|
||||||
"ValidateDraftWorkspaceParams",
|
"ValidateDraftWorkspaceParams",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from wf_api import CapabilityStepUpdate
|
||||||
from wf_api.surface import RouteSource
|
from wf_api.surface import RouteSource
|
||||||
from wf_artifacts.drafts.models import DraftStep
|
from wf_artifacts.drafts.models import DraftStep
|
||||||
from wf_core.models.steps import InputBinding, OutputBinding
|
from wf_core.models.steps import InputBinding, OutputBinding
|
||||||
@@ -219,6 +220,24 @@ class RpcDraftClientMixin:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def update_capability_step(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
update: CapabilityStepUpdate,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.update_capability_step",
|
||||||
|
{
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"step_id": step_id,
|
||||||
|
"update": update.model_dump(mode="json", exclude_unset=True),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def set_step_output_map(
|
async def set_step_output_map(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
@@ -304,21 +323,39 @@ class RpcDraftClientMixin:
|
|||||||
route_from_outcome: str = "ok",
|
route_from_outcome: str = "ok",
|
||||||
routes: dict[str, str] | None = None,
|
routes: dict[str, str] | None = None,
|
||||||
input_map: dict[str, str] | None = None,
|
input_map: dict[str, str] | None = None,
|
||||||
|
input_bindings: Sequence[InputBinding] | None = None,
|
||||||
bind_outputs: dict[str, str] | None = None,
|
bind_outputs: dict[str, str] | None = None,
|
||||||
|
desc: str | None = None,
|
||||||
|
retry: int | None = None,
|
||||||
|
timeout_seconds: int | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
if input_map is not None and input_bindings is not None:
|
||||||
|
raise ValueError("input_map and input_bindings are mutually exclusive")
|
||||||
|
params: dict[str, object] = {
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"step_id": step_id,
|
||||||
|
"capability_name": capability_name,
|
||||||
|
"route_from_step": route_from_step,
|
||||||
|
"route_from_outcome": route_from_outcome,
|
||||||
|
"routes": routes,
|
||||||
|
"bind_outputs": bind_outputs or {},
|
||||||
|
}
|
||||||
|
if input_map is not None:
|
||||||
|
params["input_map"] = input_map
|
||||||
|
if input_bindings is not None:
|
||||||
|
params["input_bindings"] = [
|
||||||
|
binding.model_dump(mode="json") for binding in input_bindings
|
||||||
|
]
|
||||||
|
if desc is not None:
|
||||||
|
params["desc"] = desc
|
||||||
|
if retry is not None:
|
||||||
|
params["retry"] = retry
|
||||||
|
if timeout_seconds is not None:
|
||||||
|
params["timeout_seconds"] = timeout_seconds
|
||||||
return await self._call(
|
return await self._call(
|
||||||
"workflow.draft_workspaces.add_step_from_capability",
|
"workflow.draft_workspaces.add_step_from_capability",
|
||||||
{
|
params,
|
||||||
"workspace_id": workspace_id,
|
|
||||||
"revision": revision,
|
|
||||||
"step_id": step_id,
|
|
||||||
"capability_name": capability_name,
|
|
||||||
"route_from_step": route_from_step,
|
|
||||||
"route_from_outcome": route_from_outcome,
|
|
||||||
"routes": routes,
|
|
||||||
"input_map": input_map or {},
|
|
||||||
"bind_outputs": bind_outputs or {},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def add_step(
|
async def add_step(
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from ..models import (
|
|||||||
SetStepOutputMapParams,
|
SetStepOutputMapParams,
|
||||||
SetWorkflowOutputBindingsParams,
|
SetWorkflowOutputBindingsParams,
|
||||||
SetWorkflowOutputMapParams,
|
SetWorkflowOutputMapParams,
|
||||||
|
UpdateCapabilityStepParams,
|
||||||
ValidateDraftParams,
|
ValidateDraftParams,
|
||||||
ValidateDraftWorkspaceParams,
|
ValidateDraftWorkspaceParams,
|
||||||
)
|
)
|
||||||
@@ -246,6 +247,23 @@ def register_methods(
|
|||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
raise_workflow_rpc_error(exc)
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
|
@entrypoint.method(
|
||||||
|
name="workflow.draft_workspaces.update_capability_step",
|
||||||
|
errors=[WorkflowRpcError],
|
||||||
|
)
|
||||||
|
async def workflow_draft_workspaces_update_capability_step(
|
||||||
|
params: UpdateCapabilityStepParams = RpcParams(),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await server.api.update_capability_step(
|
||||||
|
workspace_id=params.workspace_id,
|
||||||
|
revision=params.revision,
|
||||||
|
step_id=params.step_id,
|
||||||
|
update=params.update,
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
@entrypoint.method(
|
@entrypoint.method(
|
||||||
name="workflow.draft_workspaces.set_step_output_bindings",
|
name="workflow.draft_workspaces.set_step_output_bindings",
|
||||||
errors=[WorkflowRpcError],
|
errors=[WorkflowRpcError],
|
||||||
@@ -349,7 +367,11 @@ def register_methods(
|
|||||||
route_from_outcome=params.route_from_outcome,
|
route_from_outcome=params.route_from_outcome,
|
||||||
routes=params.routes,
|
routes=params.routes,
|
||||||
input_map=params.input_map,
|
input_map=params.input_map,
|
||||||
|
input_bindings=params.input_bindings,
|
||||||
bind_outputs=params.bind_outputs,
|
bind_outputs=params.bind_outputs,
|
||||||
|
desc=params.desc,
|
||||||
|
retry=params.retry,
|
||||||
|
timeout_seconds=params.timeout_seconds,
|
||||||
)
|
)
|
||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
raise_workflow_rpc_error(exc)
|
raise_workflow_rpc_error(exc)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import Any, Literal, Self
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||||
|
|
||||||
|
from wf_api import CapabilityStepUpdate
|
||||||
from wf_api.models import TraceRange
|
from wf_api.models import TraceRange
|
||||||
from wf_artifacts.drafts.models import DraftStep
|
from wf_artifacts.drafts.models import DraftStep
|
||||||
from wf_core.models.steps import InputBinding, OutputBinding
|
from wf_core.models.steps import InputBinding, OutputBinding
|
||||||
@@ -252,6 +253,13 @@ class BindDraftParams(RpcParamsModel):
|
|||||||
target_path: str = Field(min_length=1)
|
target_path: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateCapabilityStepParams(RpcParamsModel):
|
||||||
|
workspace_id: str = Field(min_length=1)
|
||||||
|
revision: int = Field(ge=1)
|
||||||
|
step_id: str = Field(min_length=1)
|
||||||
|
update: CapabilityStepUpdate
|
||||||
|
|
||||||
|
|
||||||
class AddStepFromCapabilityParams(RpcParamsModel):
|
class AddStepFromCapabilityParams(RpcParamsModel):
|
||||||
workspace_id: str = Field(min_length=1)
|
workspace_id: str = Field(min_length=1)
|
||||||
revision: int = Field(ge=1)
|
revision: int = Field(ge=1)
|
||||||
@@ -260,8 +268,18 @@ class AddStepFromCapabilityParams(RpcParamsModel):
|
|||||||
route_from_step: str | None = None
|
route_from_step: str | None = None
|
||||||
route_from_outcome: str = Field(default="ok", min_length=1)
|
route_from_outcome: str = Field(default="ok", min_length=1)
|
||||||
routes: dict[str, str] | None = None
|
routes: dict[str, str] | None = None
|
||||||
input_map: dict[str, str] = Field(default_factory=dict)
|
input_map: dict[str, str] | None = None
|
||||||
|
input_bindings: list[InputBinding] | None = None
|
||||||
bind_outputs: dict[str, str] = Field(default_factory=dict)
|
bind_outputs: dict[str, str] = Field(default_factory=dict)
|
||||||
|
desc: str | None = Field(default=None, min_length=1)
|
||||||
|
retry: int | None = Field(default=None, ge=0)
|
||||||
|
timeout_seconds: int | None = Field(default=None, gt=0)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_input_forms(self) -> Self:
|
||||||
|
if {"input_map", "input_bindings"} <= self.model_fields_set:
|
||||||
|
raise ValueError("input_map and input_bindings are mutually exclusive")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class BranchDraftParams(RpcParamsModel):
|
class BranchDraftParams(RpcParamsModel):
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ from wf_core import END
|
|||||||
from wf_server import build_local_static_workflow_server
|
from wf_server import build_local_static_workflow_server
|
||||||
from wf_server.config import build_workflow_server_from_workflow_config
|
from wf_server.config import build_workflow_server_from_workflow_config
|
||||||
from wf_transport_rpc_http.app import create_rpc_app
|
from wf_transport_rpc_http.app import create_rpc_app
|
||||||
from wf_transport_rpc_http.models import AddDraftStepParams
|
from wf_transport_rpc_http.models import (
|
||||||
|
AddDraftStepParams,
|
||||||
|
AddStepFromCapabilityParams,
|
||||||
|
UpdateCapabilityStepParams,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _rpc(
|
async def _rpc(
|
||||||
@@ -26,6 +30,87 @@ async def _rpc(
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_capability_step_params_preserve_nested_field_presence() -> None:
|
||||||
|
params = UpdateCapabilityStepParams.model_validate(
|
||||||
|
{
|
||||||
|
"workspace_id": "report",
|
||||||
|
"revision": 4,
|
||||||
|
"step_id": "publish",
|
||||||
|
"update": {"desc": None, "retry": 0},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert params.update.model_fields_set == {"desc", "retry"}
|
||||||
|
assert params.update.desc is None
|
||||||
|
assert params.update.retry == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"update",
|
||||||
|
[
|
||||||
|
{},
|
||||||
|
{"input": None},
|
||||||
|
{"retry": -1},
|
||||||
|
{"timeout_seconds": 0},
|
||||||
|
{"unknown": True},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_update_capability_step_params_reject_invalid_update(
|
||||||
|
update: dict[str, object],
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
UpdateCapabilityStepParams.model_validate(
|
||||||
|
{
|
||||||
|
"workspace_id": "report",
|
||||||
|
"revision": 4,
|
||||||
|
"step_id": "publish",
|
||||||
|
"update": update,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_step_from_capability_params_preserve_canonical_inputs() -> None:
|
||||||
|
params = AddStepFromCapabilityParams.model_validate(
|
||||||
|
{
|
||||||
|
"workspace_id": "report",
|
||||||
|
"revision": 1,
|
||||||
|
"step_id": "publish",
|
||||||
|
"capability_name": "demo.report",
|
||||||
|
"desc": "Publish report",
|
||||||
|
"retry": 0,
|
||||||
|
"timeout_seconds": 30,
|
||||||
|
"input_bindings": [
|
||||||
|
{"path": "state.report.title", "target": "request.title"},
|
||||||
|
{"value": "markdown", "target": "request.format"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert params.desc == "Publish report"
|
||||||
|
assert params.retry == 0
|
||||||
|
assert params.timeout_seconds == 30
|
||||||
|
assert [
|
||||||
|
binding.model_dump(mode="json") for binding in params.input_bindings or []
|
||||||
|
] == [
|
||||||
|
{"path": "state.report.title", "target": "request.title"},
|
||||||
|
{"value": "markdown", "target": "request.format"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_step_from_capability_params_reject_both_input_forms() -> None:
|
||||||
|
with pytest.raises(ValidationError, match="mutually exclusive"):
|
||||||
|
AddStepFromCapabilityParams.model_validate(
|
||||||
|
{
|
||||||
|
"workspace_id": "report",
|
||||||
|
"revision": 1,
|
||||||
|
"step_id": "publish",
|
||||||
|
"capability_name": "demo.report",
|
||||||
|
"input_map": {},
|
||||||
|
"input_bindings": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_health_and_capability_methods(tmp_path) -> None:
|
async def test_rpc_health_and_capability_methods(tmp_path) -> None:
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
app = create_rpc_app(server)
|
app = create_rpc_app(server)
|
||||||
@@ -1232,6 +1317,99 @@ async def test_rpc_draft_workspace_add_step_from_capability(tmp_path) -> None:
|
|||||||
assert result["status"] == "valid"
|
assert result["status"] == "valid"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_draft_workspace_updates_capability_step_atomically(tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.create_from_capability",
|
||||||
|
{
|
||||||
|
"workspace_id": "update_step_ws",
|
||||||
|
"capability_name": "wf.std.constant",
|
||||||
|
"name": "update_step",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
before = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.get",
|
||||||
|
{"workspace_id": "update_step_ws", "include_draft": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.update_capability_step",
|
||||||
|
{
|
||||||
|
"workspace_id": "update_step_ws",
|
||||||
|
"revision": 1,
|
||||||
|
"step_id": "call",
|
||||||
|
"update": {
|
||||||
|
"desc": "Return the prepared value",
|
||||||
|
"retry": 0,
|
||||||
|
"input": [{"value": "prepared", "target": "value"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
inspected = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.get",
|
||||||
|
{"workspace_id": "update_step_ws", "include_draft": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated["result"]["revision"] == 2
|
||||||
|
step = inspected["result"]["draft"]["steps"]["call"]
|
||||||
|
assert step["use"] == "wf.std.constant"
|
||||||
|
assert step["desc"] == "Return the prepared value"
|
||||||
|
assert step["retry"] == 0
|
||||||
|
assert step["input"] == [{"value": "prepared", "target": "value"}]
|
||||||
|
assert step["output"] == before["result"]["draft"]["steps"]["call"]["output"]
|
||||||
|
assert inspected["result"]["draft"]["routes"] == before["result"]["draft"]["routes"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_add_step_from_capability_accepts_canonical_inputs(tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.create_from_capability",
|
||||||
|
{
|
||||||
|
"workspace_id": "canonical_add_ws",
|
||||||
|
"capability_name": "wf.std.constant",
|
||||||
|
"name": "canonical_add",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
added = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.add_step_from_capability",
|
||||||
|
{
|
||||||
|
"workspace_id": "canonical_add_ws",
|
||||||
|
"revision": 1,
|
||||||
|
"step_id": "second",
|
||||||
|
"capability_name": "wf.std.constant",
|
||||||
|
"routes": {"ok": "__end__"},
|
||||||
|
"desc": "Return another prepared value",
|
||||||
|
"retry": 0,
|
||||||
|
"timeout_seconds": 30,
|
||||||
|
"input_bindings": [{"value": "second", "target": "value"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
inspected = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.get",
|
||||||
|
{"workspace_id": "canonical_add_ws", "include_draft": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert added["result"]["revision"] == 2
|
||||||
|
step = inspected["result"]["draft"]["steps"]["second"]
|
||||||
|
assert step["desc"] == "Return another prepared value"
|
||||||
|
assert step["retry"] == 0
|
||||||
|
assert step["timeout_seconds"] == 30
|
||||||
|
assert step["input"] == [{"value": "second", "target": "value"}]
|
||||||
|
|
||||||
|
|
||||||
def test_add_draft_step_params_preserve_typed_step_json() -> None:
|
def test_add_draft_step_params_preserve_typed_step_json() -> None:
|
||||||
foreach = AddDraftStepParams.model_validate(
|
foreach = AddDraftStepParams.model_validate(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import httpx
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
|
from wf_api import CapabilityStepUpdate
|
||||||
from wf_api.models import RawWorkflowPlan, TraceRange
|
from wf_api.models import RawWorkflowPlan, TraceRange
|
||||||
from wf_api.surface import RouteSource, WorkflowDraftSurface
|
from wf_api.surface import RouteSource, WorkflowDraftSurface
|
||||||
from wf_artifacts.drafts.models import (
|
from wf_artifacts.drafts.models import (
|
||||||
@@ -875,6 +876,61 @@ async def test_rpc_client_draft_workspace_add_step_from_capability(tmp_path) ->
|
|||||||
assert result["status"] == "valid"
|
assert result["status"] == "valid"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_client_serializes_capability_step_changes() -> None:
|
||||||
|
calls: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
class Client(RpcDraftClientMixin):
|
||||||
|
async def _call(self, method: str, params: dict[str, object]):
|
||||||
|
calls.append({"method": method, "params": params})
|
||||||
|
return {"revision": 2}
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
await client.update_capability_step(
|
||||||
|
workspace_id="ws",
|
||||||
|
revision=1,
|
||||||
|
step_id="publish",
|
||||||
|
update=CapabilityStepUpdate.model_validate({"desc": None, "retry": 0}),
|
||||||
|
)
|
||||||
|
await client.add_step_from_capability(
|
||||||
|
workspace_id="ws",
|
||||||
|
revision=2,
|
||||||
|
step_id="publish",
|
||||||
|
capability_name="demo.report",
|
||||||
|
routes={"ok": "__end__"},
|
||||||
|
desc="Publish report",
|
||||||
|
retry=0,
|
||||||
|
timeout_seconds=30,
|
||||||
|
input_bindings=[
|
||||||
|
InputPathBinding(
|
||||||
|
path=GraphSourcePath.state("report", "title"),
|
||||||
|
target=LocalPath.of("request", "title"),
|
||||||
|
),
|
||||||
|
InputValueBinding(
|
||||||
|
target=LocalPath.of("request", "format"),
|
||||||
|
value="markdown",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert calls[0] == {
|
||||||
|
"method": "workflow.draft_workspaces.update_capability_step",
|
||||||
|
"params": {
|
||||||
|
"workspace_id": "ws",
|
||||||
|
"revision": 1,
|
||||||
|
"step_id": "publish",
|
||||||
|
"update": {"desc": None, "retry": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert calls[1]["method"] == "workflow.draft_workspaces.add_step_from_capability"
|
||||||
|
assert calls[1]["params"]["input_bindings"] == [
|
||||||
|
{"path": "state.report.title", "target": "request.title"},
|
||||||
|
{"value": "markdown", "target": "request.format"},
|
||||||
|
]
|
||||||
|
assert calls[1]["params"]["desc"] == "Publish report"
|
||||||
|
assert calls[1]["params"]["retry"] == 0
|
||||||
|
assert calls[1]["params"]["timeout_seconds"] == 30
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_client_preserves_nested_local_path_strings() -> None:
|
async def test_rpc_client_preserves_nested_local_path_strings() -> None:
|
||||||
calls: list[dict[str, Any]] = []
|
calls: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user