feat: validate workflow plans without saving

This commit is contained in:
lda
2026-08-31 00:15:41 +07:00 Verified
parent ff01609a2e
commit 15d343f637
17 changed files with 537 additions and 19 deletions
+51
View File
@@ -185,6 +185,57 @@ async def test_create_artifact_from_plan_saves_with_observed_node_specs(
assert saved.id == "echo"
@pytest.mark.asyncio
async def test_validate_artifact_plan_does_not_persist(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_validate")
api, _service = _artifact_api(artifact_store)
result = await api.validate_artifact_plan(
plan=_echo_artifact().plan,
outcomes=("completed",),
source_bindings={},
)
assert result["status"] == "valid"
assert result["diagnostics"] == []
assert await api.list_artifacts(query="echo") == {
"nodes": [],
"next_cursor": None,
"total": 0,
}
@pytest.mark.asyncio
async def test_validate_artifact_plan_projects_invalid_plan_diagnostic(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_invalid")
api, _service = _artifact_api(artifact_store)
invalid_plan = {**_echo_artifact().plan, "start": "missing"}
result = await api.validate_artifact_plan(
plan=invalid_plan,
outcomes=("completed",),
source_bindings={},
)
assert result["status"] == "invalid"
assert result["diagnostics"] == [
{
"severity": "error",
"code": "artifact_plan_invalid",
"path": "plan",
"message": "invalid workflow plan: start node 'missing' does not exist",
"repair_hint": None,
}
]
assert await api.list_artifacts(query="echo") == {
"nodes": [],
"next_cursor": None,
"total": 0,
}
@pytest.mark.asyncio
async def test_create_artifact_from_workspace_suggests_exact_available_source_binding(
tmp_path: Path,
+3 -3
View File
@@ -90,9 +90,9 @@ def test_generates_the_complete_real_workflow_contract() -> None:
manifest = generate_manifest()
schemas = manifest["components"]["schemas"]
assert len(manifest["operations"]) == 71
assert len({operation["method"] for operation in manifest["operations"]}) == 71
assert len(schemas) == 140
assert len(manifest["operations"]) == 72
assert len({operation["method"] for operation in manifest["operations"]}) == 72
assert len(schemas) == 142
assert len(manifest["components"]["errors"]) == 1
assert all(
set(operation["result"]["schema"]) == {"$ref"}
+27
View File
@@ -1333,6 +1333,33 @@ async def test_rpc_create_artifact_from_plan(tmp_path) -> None:
assert inspected["result"]["plan"]["name"] == "rpc_constant"
async def test_rpc_validate_artifact_plan_does_not_persist(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
app = create_rpc_app(server)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
validated = await _rpc(
client,
"workflow.artifacts.validate_plan",
{
"plan": _constant_plan().model_dump(mode="json", by_alias=True),
"outcomes": ["ok"],
"source_bindings": {},
},
)
listed = await _rpc(
client, "workflow.artifacts.list", {"query": "rpc_constant"}
)
assert validated["result"]["status"] == "valid"
assert validated["result"]["diagnostics"] == []
assert listed["result"] == {
"nodes": [],
"next_cursor": None,
"total": 0,
}
async def test_rpc_draft_workspace_focused_edit_methods(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
app = create_rpc_app(server)
@@ -695,6 +695,31 @@ async def test_rpc_client_creates_artifact_from_plan(tmp_path) -> None:
assert inspected["id"] == "client_plan"
async def test_rpc_client_validates_artifact_plan_without_persisting(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
app = create_rpc_app(server)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(
transport=transport, base_url="http://test"
) as http_client:
client = RpcWorkflowApiClient(
url="http://test/rpc",
timeout_seconds=5,
http_client=http_client,
)
validated = await client.validate_artifact_plan(
plan=_constant_plan().model_dump(mode="json", by_alias=True),
outcomes=("ok",),
source_bindings={},
)
listed = await client.list_artifacts(query="client_constant")
assert validated["status"] == "valid"
assert validated["diagnostics"] == []
assert listed["nodes"] == []
assert listed["total"] == 0
async def test_rpc_client_set_workflow_output_map(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
app = create_rpc_app(server)
@@ -399,6 +399,11 @@ def test_openrpc_exposes_typed_auth_delete_result(
"SaveArtifactResult",
{"artifact_id", "version", "saved"},
),
(
"workflow.artifacts.validate_plan",
"ValidateArtifactPlanResult",
{"status", "diagnostics", "required_capabilities", "workflow_dependencies"},
),
(
"workflow.artifacts.save",
"SaveArtifactResult",