fix: preserve omitted capability metadata
This commit is contained in:
@@ -584,22 +584,19 @@ class WorkflowDraftAuthoringApi:
|
|||||||
changes["input"] = update.input
|
changes["input"] = update.input
|
||||||
|
|
||||||
changed = current.model_copy(update=changes)
|
changed = current.model_copy(update=changes)
|
||||||
if projected is None:
|
# Mutate a raw copy so omitted fields preserve their exact stored
|
||||||
step_payload = dict(deepcopy(step))
|
# presence, including legacy explicit-null metadata.
|
||||||
for field in ("desc", "retry", "timeout_seconds"):
|
step_payload = dict(deepcopy(step))
|
||||||
if field not in update.model_fields_set:
|
for field in ("desc", "retry", "timeout_seconds"):
|
||||||
continue
|
if field not in update.model_fields_set:
|
||||||
value = getattr(update, field)
|
continue
|
||||||
if value is None:
|
value = getattr(update, field)
|
||||||
step_payload.pop(field, None)
|
if value is None:
|
||||||
else:
|
step_payload.pop(field, None)
|
||||||
step_payload[field] = value
|
else:
|
||||||
else:
|
step_payload[field] = value
|
||||||
step_payload = changed.model_dump(
|
if projected is not None:
|
||||||
mode="json",
|
step_payload["input"] = projected.payload
|
||||||
by_alias=True,
|
|
||||||
exclude_none=True,
|
|
||||||
)
|
|
||||||
input_schema = (
|
input_schema = (
|
||||||
projected.input_schema
|
projected.input_schema
|
||||||
if projected is not None
|
if projected is not None
|
||||||
@@ -631,7 +628,7 @@ class WorkflowDraftAuthoringApi:
|
|||||||
next_steps[step_id] = step_payload
|
next_steps[step_id] = step_payload
|
||||||
next_draft["input_schema"] = input_schema
|
next_draft["input_schema"] = input_schema
|
||||||
next_draft["state_schema"] = state_schema
|
next_draft["state_schema"] = state_schema
|
||||||
return await self.drafts._replace_validated_draft_document(
|
return await self.drafts.replace_validated_draft_document(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=revision,
|
revision=revision,
|
||||||
draft=next_draft,
|
draft=next_draft,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from wf_artifacts import (
|
|||||||
DraftWorkspaceStore,
|
DraftWorkspaceStore,
|
||||||
compile_workflow_draft,
|
compile_workflow_draft,
|
||||||
patch_workflow_draft,
|
patch_workflow_draft,
|
||||||
|
replace_validated_draft_document,
|
||||||
validate_workflow_draft,
|
validate_workflow_draft,
|
||||||
)
|
)
|
||||||
from wf_artifacts import (
|
from wf_artifacts import (
|
||||||
@@ -19,9 +20,6 @@ from wf_artifacts import (
|
|||||||
from wf_artifacts import (
|
from wf_artifacts import (
|
||||||
patch_draft_workspace as patch_draft_workspace_record,
|
patch_draft_workspace as patch_draft_workspace_record,
|
||||||
)
|
)
|
||||||
from wf_artifacts.draft_workspaces.api import (
|
|
||||||
_replace_validated_draft_document as replace_validated_draft_document_record,
|
|
||||||
)
|
|
||||||
from wf_core.models.schemas import NodeDef
|
from wf_core.models.schemas import NodeDef
|
||||||
from wf_core.models.steps import (
|
from wf_core.models.steps import (
|
||||||
InputBinding,
|
InputBinding,
|
||||||
@@ -292,7 +290,7 @@ class WorkflowDraftApi:
|
|||||||
node_defs_for_draft=self._node_defs_for_draft,
|
node_defs_for_draft=self._node_defs_for_draft,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _replace_validated_draft_document(
|
async def replace_validated_draft_document(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
@@ -300,7 +298,7 @@ class WorkflowDraftApi:
|
|||||||
draft: dict[str, Any],
|
draft: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Persist a focused, structurally validated edit without provider lookup."""
|
"""Persist a focused, structurally validated edit without provider lookup."""
|
||||||
return replace_validated_draft_document_record(
|
return replace_validated_draft_document(
|
||||||
self._draft_store(),
|
self._draft_store(),
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=revision,
|
revision=revision,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .draft_workspaces import (
|
|||||||
ensure_workspace_id,
|
ensure_workspace_id,
|
||||||
get_draft_workspace,
|
get_draft_workspace,
|
||||||
patch_draft_workspace,
|
patch_draft_workspace,
|
||||||
|
replace_validated_draft_document,
|
||||||
summarize_draft_workspace,
|
summarize_draft_workspace,
|
||||||
)
|
)
|
||||||
from .drafts import (
|
from .drafts import (
|
||||||
@@ -91,6 +92,7 @@ __all__ = [
|
|||||||
"logical_ref_for_concrete_ref",
|
"logical_ref_for_concrete_ref",
|
||||||
"normalize_plan_node_refs",
|
"normalize_plan_node_refs",
|
||||||
"patch_draft_workspace",
|
"patch_draft_workspace",
|
||||||
|
"replace_validated_draft_document",
|
||||||
"patch_workflow_draft",
|
"patch_workflow_draft",
|
||||||
"summarize_draft_workspace",
|
"summarize_draft_workspace",
|
||||||
"validate_deployment_dependencies",
|
"validate_deployment_dependencies",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from .api import (
|
|||||||
create_draft_workspace,
|
create_draft_workspace,
|
||||||
get_draft_workspace,
|
get_draft_workspace,
|
||||||
patch_draft_workspace,
|
patch_draft_workspace,
|
||||||
|
replace_validated_draft_document,
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
WorkflowDraftWorkspace,
|
WorkflowDraftWorkspace,
|
||||||
@@ -23,5 +24,6 @@ __all__ = [
|
|||||||
"ensure_workspace_id",
|
"ensure_workspace_id",
|
||||||
"get_draft_workspace",
|
"get_draft_workspace",
|
||||||
"patch_draft_workspace",
|
"patch_draft_workspace",
|
||||||
|
"replace_validated_draft_document",
|
||||||
"summarize_draft_workspace",
|
"summarize_draft_workspace",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def patch_draft_workspace(
|
|||||||
return summarize_draft_workspace(next_workspace)
|
return summarize_draft_workspace(next_workspace)
|
||||||
|
|
||||||
|
|
||||||
def _replace_validated_draft_document(
|
def replace_validated_draft_document(
|
||||||
store: DraftWorkspaceStore,
|
store: DraftWorkspaceStore,
|
||||||
*,
|
*,
|
||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
|
|||||||
@@ -605,7 +605,8 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
title="Update Capability Step",
|
title="Update Capability Step",
|
||||||
description=(
|
description=(
|
||||||
"Update capability-step metadata and optionally replace its complete "
|
"Update capability-step metadata and optionally replace its complete "
|
||||||
"canonical input bindings. Preserves use, routes, and outputs."
|
"canonical input bindings. Omitted metadata is preserved; explicit "
|
||||||
|
"null clears it. Preserves use, routes, and outputs."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def update_capability_step(
|
async def update_capability_step(
|
||||||
|
|||||||
@@ -187,6 +187,50 @@ async def test_update_capability_step_removes_stored_null_metadata(
|
|||||||
assert "retry" not in after["draft"]["steps"]["echo"]
|
assert "retry" not in after["draft"]["steps"]["echo"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_capability_step_input_preserves_omitted_null_metadata(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
draft_api, _service, authoring = _draft_api(
|
||||||
|
FileWorkflowArtifactStore(tmp_path / "update_capability_null_metadata"),
|
||||||
|
register_echo=True,
|
||||||
|
)
|
||||||
|
draft = _echo_draft()
|
||||||
|
draft["steps"]["echo"].update(
|
||||||
|
{
|
||||||
|
"desc": None,
|
||||||
|
"retry": None,
|
||||||
|
"timeout_seconds": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await draft_api.create_draft_workspace(workspace_id="echo", draft=draft)
|
||||||
|
|
||||||
|
result = await authoring.update_capability_step(
|
||||||
|
workspace_id="echo",
|
||||||
|
revision=1,
|
||||||
|
step_id="echo",
|
||||||
|
update=CapabilityStepUpdate(
|
||||||
|
input=[
|
||||||
|
InputValueBinding(
|
||||||
|
target=LocalPath.of("text"),
|
||||||
|
value="replacement",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
inspected = await draft_api.get_draft_workspace(
|
||||||
|
workspace_id="echo",
|
||||||
|
include_draft=True,
|
||||||
|
)
|
||||||
|
step = inspected["draft"]["steps"]["echo"]
|
||||||
|
|
||||||
|
assert result["revision"] == 2
|
||||||
|
assert step["desc"] is None
|
||||||
|
assert step["retry"] is None
|
||||||
|
assert step["timeout_seconds"] is None
|
||||||
|
assert step["input"] == [{"value": "replacement", "target": "text"}]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_capability_step_metadata_does_not_resolve_capability(
|
async def test_update_capability_step_metadata_does_not_resolve_capability(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
|||||||
Reference in New Issue
Block a user