fix: validate incoming draft route outcomes

This commit is contained in:
lda
2026-07-20 14:51:00 +07:00 Verified
parent 3119276014
commit dda013dd12
2 changed files with 94 additions and 2 deletions
+14 -2
View File
@@ -4,6 +4,8 @@ from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Any
from pydantic import TypeAdapter
from wf_artifacts.draft_workspaces.models import (
WorkflowDraftWorkspace,
summarize_draft_workspace,
@@ -185,8 +187,18 @@ class WorkflowDraftAuthoringApi:
f"{sorted(unknown_outcomes)!r}"
)
if incoming is not None and incoming.step_id not in steps:
raise ValueError(f"unknown incoming source step {incoming.step_id!r}")
if incoming is not None:
if incoming.step_id not in steps:
raise ValueError(f"unknown incoming source step {incoming.step_id!r}")
source_step = TypeAdapter(DraftStep).validate_python(
steps[incoming.step_id]
)
source_outcomes = self._draft_step_route_outcomes(source_step)
if source_outcomes is None or incoming.outcome not in source_outcomes:
raise ValueError(
f"unknown incoming route outcome {incoming.outcome!r} for "
f"source step {incoming.step_id!r}"
)
patch: list[dict[str, Any]] = [
{
+80
View File
@@ -961,6 +961,86 @@ async def test_add_step_adds_missing_incoming_route_parent_atomically(
assert workspace["draft"]["routes"]["echo"] == {"ok": "new_step"}
@pytest.mark.asyncio
async def test_add_step_distinguishes_missing_and_explicit_empty_routes(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_empty_routes")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
step_adapter = TypeAdapter(DraftStep)
step_without_routes = step_adapter.validate_python(
{"use": "demo.personal.echo_tool", "input": [], "output": []}
)
first = await api.add_step(
workspace_id="draft_ws",
revision=1,
step_id="no_routes",
step=step_without_routes,
routes=None,
)
assert first["revision"] == 2
after_none = await draft_api.get_draft_workspace(
workspace_id="draft_ws", include_draft=True
)
assert "no_routes" not in after_none["draft"]["routes"]
step_with_empty_routes = step_adapter.validate_python(
{"use": "demo.personal.echo_tool", "input": [], "output": []}
)
second = await api.add_step(
workspace_id="draft_ws",
revision=2,
step_id="empty_routes",
step=step_with_empty_routes,
routes={},
)
assert second["revision"] == 3
after_empty = await draft_api.get_draft_workspace(
workspace_id="draft_ws", include_draft=True
)
assert after_empty["draft"]["routes"]["empty_routes"] == {}
@pytest.mark.asyncio
async def test_add_step_rejects_unknown_incoming_outcome_without_mutation(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_bad_incoming")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
draft_store = authoring.drafts._draft_store()
assert isinstance(draft_store, FileDraftWorkspaceStore)
workspace_path = draft_store._workspace_path("draft_ws")
before_bytes = workspace_path.read_bytes()
before = await draft_api.get_draft_workspace(
workspace_id="draft_ws", include_draft=True
)
step = TypeAdapter(DraftStep).validate_python(
{"use": "demo.personal.echo_tool", "input": [], "output": []}
)
with pytest.raises(ValueError, match="unknown incoming route outcome"):
await api.add_step(
workspace_id="draft_ws",
revision=1,
step_id="new_step",
step=step,
incoming=RouteSource("echo", "missing"),
)
after = await draft_api.get_draft_workspace(
workspace_id="draft_ws", include_draft=True
)
assert workspace_path.read_bytes() == before_bytes
assert after["revision"] == before["revision"]
assert after["draft"] == before["draft"]
async def _assert_add_step_rejected_without_mutation(
api: WorkflowApi,
draft_api: WorkflowDraftApi,