end to end fix by Validating the plan

This commit is contained in:
lda
2026-05-12 18:59:55 +07:00 Verified
parent f90367d471
commit 6b696ff56a
4 changed files with 84 additions and 9 deletions
+30
View File
@@ -2,6 +2,8 @@ from __future__ import annotations
from collections.abc import Mapping
from wf_core import Workflow
from .models import JsonObject, RequiredCapability, WorkflowArtifact
@@ -17,6 +19,7 @@ def create_workflow_artifact_from_plan(
created_from_catalog_version: str | None = None,
) -> WorkflowArtifact:
"""Create an immutable artifact from a declarative workflow plan."""
_validate_workflow_plan(plan)
return WorkflowArtifact(
id=artifact_id,
version=version,
@@ -36,3 +39,30 @@ def _required_object_field(plan: JsonObject, field_name: str) -> JsonObject:
if not isinstance(value, dict):
raise ValueError(f"workflow plan is missing object field {field_name!r}")
return value
def _validate_workflow_plan(plan: JsonObject) -> None:
try:
workflow = Workflow.model_validate(plan)
except Exception as exc:
raise ValueError(f"invalid workflow plan: {exc}") from exc
node_ids = {node.id for node in workflow.nodes}
if workflow.start not in node_ids:
raise ValueError(
f"invalid workflow plan: start node {workflow.start!r} does not exist"
)
# wf_core currently uses Workflow.start as the only entry-point source.
# START is exported for future LangGraph-style edges, but core validation
# does not accept START edges yet.
edge_sources = set(node_ids)
for edge in workflow.edges:
if edge.from_ not in edge_sources:
raise ValueError(
f"invalid workflow plan: edge source {edge.from_!r} does not exist"
)
if edge.to not in node_ids and edge.to != "__end__":
raise ValueError(
f"invalid workflow plan: edge destination {edge.to!r} does not exist"
)
+47 -2
View File
@@ -50,6 +50,43 @@ def test_create_workflow_artifact_from_plan_rejects_missing_boundary_schema() ->
raise AssertionError("expected missing output_schema to be rejected")
def test_create_workflow_artifact_from_plan_rejects_invalid_workflow_shape() -> None:
plan = _plan()
plan["state_schema"] = {"fields": {"echoed": {"schema": {"type": "string"}}}}
try:
create_workflow_artifact_from_plan(
artifact_id="echo",
version=1,
title="Echo",
plan=plan,
outcomes=("done",),
)
except ValueError as exc:
assert "state_schema" in str(exc)
assert "type" in str(exc)
else:
raise AssertionError("expected invalid state schema to be rejected")
def test_create_workflow_artifact_from_plan_rejects_missing_start_node() -> None:
plan = _plan()
plan["start"] = "missing"
try:
create_workflow_artifact_from_plan(
artifact_id="echo",
version=1,
title="Echo",
plan=plan,
outcomes=("done",),
)
except ValueError as exc:
assert "start node 'missing' does not exist" in str(exc)
else:
raise AssertionError("expected missing start node to be rejected")
def _plan() -> dict[str, object]:
return {
"name": "echo",
@@ -65,6 +102,14 @@ def _plan() -> dict[str, object]:
"required": ["echoed"],
},
"start": "echo",
"nodes": [],
"edges": [],
"nodes": [
{
"id": "echo",
"type": "node",
"node": "demo.echo_tool",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
}
],
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}],
}
+3 -1
View File
@@ -360,7 +360,9 @@ def test_broker_saves_and_lists_workflow_deployments() -> None:
def test_broker_runs_non_interrupting_workflow_deployment() -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "broker_run_artifacts")
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "broker_run_artifacts"
)
artifact_store.save_artifact(_echo_artifact())
artifact_store.save_deployment(
WorkflowDeployment(
+4 -6
View File
@@ -14,17 +14,15 @@
}
},
{
"id": "serena.default",
"server": "serena",
"id": "playwright.default",
"server": "playwright",
"account": "default",
"enabled": true,
"metadata": {
"transport": "stdio",
"command": "serena",
"command": "pnpx",
"args": [
"start-mcp-server",
"--enable-web-dashboard=true",
"--project-from-cwd"
"@playwright/mcp@latest"
],
"env": {}
}