refactor: extract NextActions into reusable workflow-surface models
Move wrapper-draft next_actions from handler-local dict helpers into typed models in workflow_surface/next_actions.py. Adds NextActionTool enum, NextActionPatchExample, and NextActions.from_wrapper_hints() constructor. Replaces private _wrapper_draft_next_actions and _wrapper_draft_patch_examples helpers. Adds can_continue as additive advisory field. Keeps backward-compatible aliases in models.py.
This commit is contained in:
@@ -356,6 +356,11 @@ tool to call next, and concrete patch examples for common missing decisions.
|
|||||||
`next_actions.can_save_now` is not enforced. A caller can still save a low
|
`next_actions.can_save_now` is not enforced. A caller can still save a low
|
||||||
confidence draft, but the field exists to make that risk explicit.
|
confidence draft, but the field exists to make that risk explicit.
|
||||||
|
|
||||||
|
`next_actions` is advisory guidance, not validation authority. It gives MCP
|
||||||
|
clients a compact "what should I call next?" pointer, while diagnostics,
|
||||||
|
artifact validation, deployment validation, and runtime status remain the
|
||||||
|
source of truth.
|
||||||
|
|
||||||
## Relationship To Capability Sources
|
## Relationship To Capability Sources
|
||||||
|
|
||||||
Sources own capability kinds:
|
Sources own capability kinds:
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ from .constants import (
|
|||||||
RUNTIME_ERROR_CAPABILITY,
|
RUNTIME_ERROR_CAPABILITY,
|
||||||
)
|
)
|
||||||
from .models import TraceRange
|
from .models import TraceRange
|
||||||
|
from .next_actions import NextActions
|
||||||
from .refs import parse_workflow_surface_capability_id
|
from .refs import parse_workflow_surface_capability_id
|
||||||
from .saved_subgraphs import (
|
from .saved_subgraphs import (
|
||||||
SavedSubgraphTree,
|
SavedSubgraphTree,
|
||||||
@@ -836,11 +837,11 @@ class WorkflowSurfaceHandlers:
|
|||||||
return {
|
return {
|
||||||
**result,
|
**result,
|
||||||
"wrapper_hints": hints,
|
"wrapper_hints": hints,
|
||||||
"next_actions": _wrapper_draft_next_actions(
|
"next_actions": NextActions.from_wrapper_hints(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=int(result["revision"]),
|
revision=int(result["revision"]),
|
||||||
hints=hints,
|
hints=hints,
|
||||||
),
|
).model_dump(mode="json"),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def create_artifact_from_workspace(
|
async def create_artifact_from_workspace(
|
||||||
@@ -1486,98 +1487,6 @@ def _draft_name_from_capability(capability_name: str) -> str:
|
|||||||
return capability_name.replace(".", "_").replace("-", "_")
|
return capability_name.replace(".", "_").replace("-", "_")
|
||||||
|
|
||||||
|
|
||||||
def _wrapper_draft_next_actions(
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
hints: dict[str, Any],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Convert wrapper_hints into advisory next-tool guidance for MCP clients."""
|
|
||||||
confidence = str(hints.get("confidence", "low"))
|
|
||||||
missing_decisions = hints.get("missing_decisions")
|
|
||||||
notes = [str(note) for note in hints.get("notes", []) if isinstance(note, str)]
|
|
||||||
has_missing = isinstance(missing_decisions, list) and len(missing_decisions) > 0
|
|
||||||
can_save_now = confidence == "high" and not has_missing
|
|
||||||
if can_save_now:
|
|
||||||
return {
|
|
||||||
"can_save_now": True,
|
|
||||||
"recommended_next_tool": "wf.workflow.validate_draft_workspace",
|
|
||||||
"reason": "Wrapper hints are high confidence and have no missing decisions.",
|
|
||||||
"patch_examples": [],
|
|
||||||
"warnings": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"can_save_now": False,
|
|
||||||
"recommended_next_tool": "wf.workflow.patch_draft_workspace",
|
|
||||||
"reason": "Review missing wrapper decisions before saving.",
|
|
||||||
"patch_examples": _wrapper_draft_patch_examples(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
hints=hints,
|
|
||||||
),
|
|
||||||
"warnings": notes,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _wrapper_draft_patch_examples(
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
hints: dict[str, Any],
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return conservative JSON Patch examples without guessing semantics."""
|
|
||||||
examples: list[dict[str, Any]] = []
|
|
||||||
missing_decisions = hints.get("missing_decisions")
|
|
||||||
if not isinstance(missing_decisions, list):
|
|
||||||
return examples
|
|
||||||
decision_kinds = {
|
|
||||||
str(decision.get("kind"))
|
|
||||||
for decision in missing_decisions
|
|
||||||
if isinstance(decision, dict)
|
|
||||||
}
|
|
||||||
if {
|
|
||||||
"choose_output_fields",
|
|
||||||
"review_nested_output",
|
|
||||||
} & decision_kinds:
|
|
||||||
examples.append(
|
|
||||||
{
|
|
||||||
"description": (
|
|
||||||
"Replace output bindings after choosing which capability "
|
|
||||||
"outputs should be written to workflow state."
|
|
||||||
),
|
|
||||||
"tool": "wf.workflow.patch_draft_workspace",
|
|
||||||
"request": {
|
|
||||||
"workspace_id": workspace_id,
|
|
||||||
"revision": revision,
|
|
||||||
"patch": [
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/draft/steps/call/output",
|
|
||||||
"value": [],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if "confirm_boolean_outcomes" in decision_kinds:
|
|
||||||
examples.append(
|
|
||||||
{
|
|
||||||
"description": (
|
|
||||||
"Review boolean output candidates before adding routing; "
|
|
||||||
"do not route on boolean fields automatically."
|
|
||||||
),
|
|
||||||
"tool": "wf.workflow.patch_draft_workspace",
|
|
||||||
"request": {
|
|
||||||
"workspace_id": workspace_id,
|
|
||||||
"revision": revision,
|
|
||||||
"patch": [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return examples
|
|
||||||
|
|
||||||
|
|
||||||
def _source_id_for_capability(
|
def _source_id_for_capability(
|
||||||
sources: dict[str, CapabilitySource],
|
sources: dict[str, CapabilitySource],
|
||||||
qualified_name: str,
|
qualified_name: str,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import Annotated, Any, Literal
|
|||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from .next_actions import NextActionPatchExample, NextActions
|
||||||
from wf_artifacts import ArtifactKind
|
from wf_artifacts import ArtifactKind
|
||||||
from wf_artifacts.draft_workspaces.models import WORKSPACE_ID_PATTERN
|
from wf_artifacts.draft_workspaces.models import WORKSPACE_ID_PATTERN
|
||||||
from wf_core.models.steps import InputBinding, OutputBinding
|
from wf_core.models.steps import InputBinding, OutputBinding
|
||||||
@@ -353,40 +354,10 @@ class CreateDraftWorkspaceFromCapabilityRequest(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WrapperDraftPatchExample(BaseModel):
|
# Compatibility aliases for older imports. The JSON fields are now generic
|
||||||
"""Concrete patch-workspace example for a likely next authoring edit."""
|
# workflow-surface guidance, not wrapper-only policy.
|
||||||
|
WrapperDraftPatchExample = NextActionPatchExample
|
||||||
description: str = Field(description="Human-readable reason for this patch.")
|
WrapperDraftNextActions = NextActions
|
||||||
tool: str = Field(description="MCP tool to call for this example.")
|
|
||||||
request: dict[str, Any] = Field(
|
|
||||||
description="JSON request payload to pass to the tool."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WrapperDraftNextActions(BaseModel):
|
|
||||||
"""Advisory continuation hints after bootstrapping a wrapper draft."""
|
|
||||||
|
|
||||||
can_save_now: bool = Field(
|
|
||||||
description=(
|
|
||||||
"Advisory only. False means the scaffold likely needs review before "
|
|
||||||
"saving, but the server does not enforce this."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
recommended_next_tool: str = Field(
|
|
||||||
description=(
|
|
||||||
"Suggested next MCP tool, usually wf.workflow.validate_draft_workspace "
|
|
||||||
"or wf.workflow.patch_draft_workspace."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
reason: str = Field(description="Short explanation for the recommendation.")
|
|
||||||
patch_examples: list[WrapperDraftPatchExample] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
description="Concrete JSON Patch examples for common missing decisions.",
|
|
||||||
)
|
|
||||||
warnings: list[str] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
description="Non-blocking warnings copied from low-confidence wrapper hints.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateDraftWorkspaceFromCapabilityResult(DraftWorkspaceResult):
|
class CreateDraftWorkspaceFromCapabilityResult(DraftWorkspaceResult):
|
||||||
@@ -398,7 +369,7 @@ class CreateDraftWorkspaceFromCapabilityResult(DraftWorkspaceResult):
|
|||||||
"Use this to patch uncertain maps or schemas by revision."
|
"Use this to patch uncertain maps or schemas by revision."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
next_actions: WrapperDraftNextActions = Field(
|
next_actions: NextActions = Field(
|
||||||
description=(
|
description=(
|
||||||
"Advisory next step guidance derived from wrapper_hints. "
|
"Advisory next step guidance derived from wrapper_hints. "
|
||||||
"The server does not enforce can_save_now."
|
"The server does not enforce can_save_now."
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Any, Self
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from .wrapper_hints import WrapperAuthoringHints
|
||||||
|
|
||||||
|
|
||||||
|
class NextActionTool(StrEnum):
|
||||||
|
"""Stable MCP workflow tools that guidance may recommend."""
|
||||||
|
|
||||||
|
PATCH_DRAFT_WORKSPACE = "wf.workflow.patch_draft_workspace"
|
||||||
|
VALIDATE_DRAFT_WORKSPACE = "wf.workflow.validate_draft_workspace"
|
||||||
|
VALIDATE_DEPLOYMENT = "wf.workflow.validate_deployment"
|
||||||
|
RUN_DEPLOYMENT = "wf.workflow.run_deployment"
|
||||||
|
RESUME_RUN = "wf.workflow.resume_run"
|
||||||
|
READ_RUN_TRACE = "wf.workflow.read_run_trace"
|
||||||
|
|
||||||
|
|
||||||
|
class NextActionPatchExample(BaseModel):
|
||||||
|
"""Concrete example request for a recommended MCP workflow tool."""
|
||||||
|
|
||||||
|
description: str = Field(description="Human-readable reason for this example.")
|
||||||
|
tool: NextActionTool = Field(description="MCP workflow tool to call.")
|
||||||
|
request: dict[str, Any] = Field(
|
||||||
|
description="JSON request payload to pass to the tool."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NextActions(BaseModel):
|
||||||
|
"""Advisory continuation hints for MCP workflow clients.
|
||||||
|
|
||||||
|
This object is guidance, not authority. Validation diagnostics and runtime
|
||||||
|
status remain the source of truth; clients should treat this as a compact
|
||||||
|
answer to "what tool should I call next?"
|
||||||
|
"""
|
||||||
|
|
||||||
|
can_continue: bool = Field(
|
||||||
|
description=(
|
||||||
|
"Whether there is an obvious next workflow-surface tool call. "
|
||||||
|
"Advisory only."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
can_save_now: bool | None = Field(
|
||||||
|
default=None,
|
||||||
|
description=(
|
||||||
|
"Advisory wrapper-authoring signal. False means review is "
|
||||||
|
"recommended before saving; the server does not enforce this."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
recommended_next_tool: NextActionTool | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Suggested next MCP workflow tool, if one is obvious.",
|
||||||
|
)
|
||||||
|
reason: str = Field(description="Short explanation for the recommendation.")
|
||||||
|
patch_examples: list[NextActionPatchExample] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Concrete JSON Patch examples for common missing decisions.",
|
||||||
|
)
|
||||||
|
warnings: list[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Non-blocking warnings copied from low-confidence hints.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_wrapper_hints(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
hints: WrapperAuthoringHints | dict[str, Any],
|
||||||
|
) -> Self:
|
||||||
|
"""Create guidance after bootstrapping a wrapper draft workspace."""
|
||||||
|
payload = (
|
||||||
|
hints.model_dump(mode="json")
|
||||||
|
if isinstance(hints, WrapperAuthoringHints)
|
||||||
|
else hints
|
||||||
|
)
|
||||||
|
confidence = str(payload.get("confidence", "low"))
|
||||||
|
missing_decisions = payload.get("missing_decisions")
|
||||||
|
notes = [
|
||||||
|
str(note) for note in payload.get("notes", []) if isinstance(note, str)
|
||||||
|
]
|
||||||
|
has_missing = isinstance(missing_decisions, list) and len(missing_decisions) > 0
|
||||||
|
can_save_now = confidence == "high" and not has_missing
|
||||||
|
if can_save_now:
|
||||||
|
return cls(
|
||||||
|
can_continue=True,
|
||||||
|
can_save_now=True,
|
||||||
|
recommended_next_tool=NextActionTool.VALIDATE_DRAFT_WORKSPACE,
|
||||||
|
reason="Wrapper hints are high confidence and have no missing decisions.",
|
||||||
|
patch_examples=[],
|
||||||
|
warnings=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
can_continue=True,
|
||||||
|
can_save_now=False,
|
||||||
|
recommended_next_tool=NextActionTool.PATCH_DRAFT_WORKSPACE,
|
||||||
|
reason="Review missing wrapper decisions before saving.",
|
||||||
|
patch_examples=_wrapper_draft_patch_examples(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
hints=payload,
|
||||||
|
),
|
||||||
|
warnings=notes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _wrapper_draft_patch_examples(
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
hints: dict[str, Any],
|
||||||
|
) -> list[NextActionPatchExample]:
|
||||||
|
"""Return conservative JSON Patch examples without guessing semantics."""
|
||||||
|
examples: list[NextActionPatchExample] = []
|
||||||
|
missing_decisions = hints.get("missing_decisions")
|
||||||
|
if not isinstance(missing_decisions, list):
|
||||||
|
return examples
|
||||||
|
decision_kinds = {
|
||||||
|
str(decision.get("kind"))
|
||||||
|
for decision in missing_decisions
|
||||||
|
if isinstance(decision, dict)
|
||||||
|
}
|
||||||
|
if {"choose_output_fields", "review_nested_output"} & decision_kinds:
|
||||||
|
examples.append(
|
||||||
|
NextActionPatchExample(
|
||||||
|
description=(
|
||||||
|
"Replace output bindings after choosing which capability "
|
||||||
|
"outputs should be written to workflow state."
|
||||||
|
),
|
||||||
|
tool=NextActionTool.PATCH_DRAFT_WORKSPACE,
|
||||||
|
request={
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"patch": [
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/draft/steps/call/output",
|
||||||
|
"value": [],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if "confirm_boolean_outcomes" in decision_kinds:
|
||||||
|
examples.append(
|
||||||
|
NextActionPatchExample(
|
||||||
|
description=(
|
||||||
|
"Review boolean output candidates before adding routing; "
|
||||||
|
"do not route on boolean fields automatically."
|
||||||
|
),
|
||||||
|
tool=NextActionTool.PATCH_DRAFT_WORKSPACE,
|
||||||
|
request={
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"patch": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return examples
|
||||||
@@ -116,6 +116,11 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
next_actions_schema = from_capability_output["properties"]["next_actions"]
|
next_actions_schema = from_capability_output["properties"]["next_actions"]
|
||||||
assert "recommended_next_tool" in next_actions_schema["properties"]
|
assert "recommended_next_tool" in next_actions_schema["properties"]
|
||||||
assert "patch_examples" in next_actions_schema["properties"]
|
assert "patch_examples" in next_actions_schema["properties"]
|
||||||
|
assert "can_continue" in next_actions_schema["properties"]
|
||||||
|
assert (
|
||||||
|
"Advisory"
|
||||||
|
in next_actions_schema["properties"]["can_continue"]["description"]
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
"Advisory"
|
"Advisory"
|
||||||
in next_actions_schema["properties"]["can_save_now"]["description"]
|
in next_actions_schema["properties"]["can_save_now"]["description"]
|
||||||
|
|||||||
@@ -541,6 +541,7 @@ def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> Non
|
|||||||
assert result["wrapper_hints"]["input_map"] == {"input.text": "text"}
|
assert result["wrapper_hints"]["input_map"] == {"input.text": "text"}
|
||||||
assert result["wrapper_hints"]["output_map"] == {"echoed": "state.echoed"}
|
assert result["wrapper_hints"]["output_map"] == {"echoed": "state.echoed"}
|
||||||
next_actions = result["next_actions"]
|
next_actions = result["next_actions"]
|
||||||
|
assert next_actions["can_continue"] is True
|
||||||
assert next_actions["can_save_now"] is True
|
assert next_actions["can_save_now"] is True
|
||||||
assert (
|
assert (
|
||||||
next_actions["recommended_next_tool"] == "wf.workflow.validate_draft_workspace"
|
next_actions["recommended_next_tool"] == "wf.workflow.validate_draft_workspace"
|
||||||
@@ -705,6 +706,7 @@ def test_workflow_surface_low_confidence_draft_returns_patch_guidance() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
next_actions = result["next_actions"]
|
next_actions = result["next_actions"]
|
||||||
|
assert next_actions["can_continue"] is True
|
||||||
assert next_actions["can_save_now"] is False
|
assert next_actions["can_save_now"] is False
|
||||||
assert next_actions["recommended_next_tool"] == "wf.workflow.patch_draft_workspace"
|
assert next_actions["recommended_next_tool"] == "wf.workflow.patch_draft_workspace"
|
||||||
assert "missing wrapper decisions" in next_actions["reason"]
|
assert "missing wrapper decisions" in next_actions["reason"]
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from wf_mcp.workflow_surface.next_actions import NextActionTool, NextActions
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_actions_from_high_confidence_wrapper_hints_can_validate() -> None:
|
||||||
|
actions = NextActions.from_wrapper_hints(
|
||||||
|
workspace_id="echo_workspace",
|
||||||
|
revision=3,
|
||||||
|
hints={
|
||||||
|
"confidence": "high",
|
||||||
|
"missing_decisions": [],
|
||||||
|
"notes": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dumped = actions.model_dump(mode="json")
|
||||||
|
assert dumped["can_continue"] is True
|
||||||
|
assert dumped["can_save_now"] is True
|
||||||
|
assert dumped["recommended_next_tool"] == (
|
||||||
|
NextActionTool.VALIDATE_DRAFT_WORKSPACE.value
|
||||||
|
)
|
||||||
|
assert "high confidence" in dumped["reason"]
|
||||||
|
assert dumped["patch_examples"] == []
|
||||||
|
assert dumped["warnings"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_actions_from_low_confidence_wrapper_hints_can_patch() -> None:
|
||||||
|
actions = NextActions.from_wrapper_hints(
|
||||||
|
workspace_id="echo_workspace",
|
||||||
|
revision=4,
|
||||||
|
hints={
|
||||||
|
"confidence": "low",
|
||||||
|
"missing_decisions": [
|
||||||
|
{
|
||||||
|
"kind": "review_nested_output",
|
||||||
|
"message": "Review nested output fields before mapping.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "confirm_boolean_outcomes",
|
||||||
|
"message": "Boolean fields may be data, not outcomes.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"notes": ["Raw MCP tool output is not workflow-shaped."],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dumped = actions.model_dump(mode="json")
|
||||||
|
assert dumped["can_continue"] is True
|
||||||
|
assert dumped["can_save_now"] is False
|
||||||
|
assert dumped["recommended_next_tool"] == NextActionTool.PATCH_DRAFT_WORKSPACE.value
|
||||||
|
assert "missing wrapper decisions" in dumped["reason"]
|
||||||
|
assert dumped["warnings"][0] == "Raw MCP tool output is not workflow-shaped."
|
||||||
|
assert dumped["patch_examples"][0]["tool"] == (
|
||||||
|
NextActionTool.PATCH_DRAFT_WORKSPACE.value
|
||||||
|
)
|
||||||
|
assert dumped["patch_examples"][0]["request"]["workspace_id"] == "echo_workspace"
|
||||||
|
assert dumped["patch_examples"][0]["request"]["revision"] == 4
|
||||||
|
assert dumped["patch_examples"][0]["request"]["patch"][0]["path"] == (
|
||||||
|
"/draft/steps/call/output"
|
||||||
|
)
|
||||||
|
assert dumped["patch_examples"][1]["request"]["patch"] == []
|
||||||
Reference in New Issue
Block a user