end to end fix by Validating the plan
This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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__"}],
|
||||
}
|
||||
|
||||
@@ -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
@@ -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": {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user