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 collections.abc import Mapping
|
||||||
|
|
||||||
|
from wf_core import Workflow
|
||||||
|
|
||||||
from .models import JsonObject, RequiredCapability, WorkflowArtifact
|
from .models import JsonObject, RequiredCapability, WorkflowArtifact
|
||||||
|
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ def create_workflow_artifact_from_plan(
|
|||||||
created_from_catalog_version: str | None = None,
|
created_from_catalog_version: str | None = None,
|
||||||
) -> WorkflowArtifact:
|
) -> WorkflowArtifact:
|
||||||
"""Create an immutable artifact from a declarative workflow plan."""
|
"""Create an immutable artifact from a declarative workflow plan."""
|
||||||
|
_validate_workflow_plan(plan)
|
||||||
return WorkflowArtifact(
|
return WorkflowArtifact(
|
||||||
id=artifact_id,
|
id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -36,3 +39,30 @@ def _required_object_field(plan: JsonObject, field_name: str) -> JsonObject:
|
|||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
raise ValueError(f"workflow plan is missing object field {field_name!r}")
|
raise ValueError(f"workflow plan is missing object field {field_name!r}")
|
||||||
return value
|
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")
|
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]:
|
def _plan() -> dict[str, object]:
|
||||||
return {
|
return {
|
||||||
"name": "echo",
|
"name": "echo",
|
||||||
@@ -65,6 +102,14 @@ def _plan() -> dict[str, object]:
|
|||||||
"required": ["echoed"],
|
"required": ["echoed"],
|
||||||
},
|
},
|
||||||
"start": "echo",
|
"start": "echo",
|
||||||
"nodes": [],
|
"nodes": [
|
||||||
"edges": [],
|
{
|
||||||
|
"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:
|
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_artifact(_echo_artifact())
|
||||||
artifact_store.save_deployment(
|
artifact_store.save_deployment(
|
||||||
WorkflowDeployment(
|
WorkflowDeployment(
|
||||||
|
|||||||
+4
-6
@@ -14,17 +14,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "serena.default",
|
"id": "playwright.default",
|
||||||
"server": "serena",
|
"server": "playwright",
|
||||||
"account": "default",
|
"account": "default",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"transport": "stdio",
|
"transport": "stdio",
|
||||||
"command": "serena",
|
"command": "pnpx",
|
||||||
"args": [
|
"args": [
|
||||||
"start-mcp-server",
|
"@playwright/mcp@latest"
|
||||||
"--enable-web-dashboard=true",
|
|
||||||
"--project-from-cwd"
|
|
||||||
],
|
],
|
||||||
"env": {}
|
"env": {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user