fix: preserve omitted capability metadata

This commit is contained in:
lda
2026-07-27 02:30:08 +07:00 Verified
parent a0d76fdfb2
commit 1f023b4848
7 changed files with 68 additions and 24 deletions
+14 -17
View File
@@ -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,
+3 -5
View File
@@ -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,
+2
View File
@@ -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",
] ]
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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(
+44
View File
@@ -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,