feat: type stateless draft results
This commit is contained in:
@@ -74,10 +74,10 @@
|
|||||||
- Typed-result slices now give `workflow.health`, all artifact, deployment,
|
- Typed-result slices now give `workflow.health`, all artifact, deployment,
|
||||||
and run operations, every persisted draft-workspace operation, and both the
|
and run operations, every persisted draft-workspace operation, and both the
|
||||||
capability and source-discovery surfaces named transport-neutral result
|
capability and source-discovery surfaces named transport-neutral result
|
||||||
schemas: 53 of 70 methods. The remaining 17 success results still collapse
|
schemas: 55 of 70 methods. The remaining 15 success results still collapse
|
||||||
to generic objects across stateless draft patch/validate and the
|
to generic objects across the source-registry/admin operations. Continue
|
||||||
source-registry/admin operations. Continue introducing operation result DTOs
|
introducing operation result DTOs before adopting generated TypeScript
|
||||||
before adopting generated TypeScript contracts.
|
contracts.
|
||||||
- The stock `@open-rpc/generator` TypeScript client is not suitable here. It
|
- The stock `@open-rpc/generator` TypeScript client is not suitable here. It
|
||||||
exhausted a 4 GB Node heap on the full contract and emitted invalid dotted
|
exhausted a 4 GB Node heap on the full contract and emitted invalid dotted
|
||||||
class members plus `any` results for a minimal `workflow.health` contract.
|
class members plus `any` results for a minimal `workflow.health` contract.
|
||||||
|
|||||||
+39
-13
@@ -60,6 +60,11 @@ from .models import (
|
|||||||
InvalidDraftResult,
|
InvalidDraftResult,
|
||||||
JsonProjector,
|
JsonProjector,
|
||||||
ListDraftWorkspacesResult,
|
ListDraftWorkspacesResult,
|
||||||
|
PatchDraftResult,
|
||||||
|
PatchedDraftInvalidResult,
|
||||||
|
PatchedDraftValidResult,
|
||||||
|
ValidateDraftResult,
|
||||||
|
ValidDraftResult,
|
||||||
)
|
)
|
||||||
from .operation_context import WorkflowOperationContext
|
from .operation_context import WorkflowOperationContext
|
||||||
from .schema_projection import project_property_to_schema_path, schema_path_exists
|
from .schema_projection import project_property_to_schema_path, schema_path_exists
|
||||||
@@ -69,6 +74,23 @@ _PROJECT_DRAFT_WORKSPACE_LIST = JsonProjector(ListDraftWorkspacesResult)
|
|||||||
_PROJECT_DRAFT_WORKSPACE_DELETE = JsonProjector(DeleteDraftWorkspaceResult)
|
_PROJECT_DRAFT_WORKSPACE_DELETE = JsonProjector(DeleteDraftWorkspaceResult)
|
||||||
_PROJECT_DRAFT_COMPILE = JsonProjector(CompileDraftWorkspaceSuccess)
|
_PROJECT_DRAFT_COMPILE = JsonProjector(CompileDraftWorkspaceSuccess)
|
||||||
_PROJECT_INVALID_DRAFT = JsonProjector(InvalidDraftResult)
|
_PROJECT_INVALID_DRAFT = JsonProjector(InvalidDraftResult)
|
||||||
|
_PROJECT_VALID_DRAFT = JsonProjector(ValidDraftResult)
|
||||||
|
_PROJECT_PATCHED_DRAFT_VALID = JsonProjector(PatchedDraftValidResult)
|
||||||
|
_PROJECT_PATCHED_DRAFT_INVALID = JsonProjector(PatchedDraftInvalidResult)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_validate_draft(payload: dict[str, Any]) -> ValidateDraftResult:
|
||||||
|
"""Project one validation result through the matching status variant."""
|
||||||
|
if payload.get("status") == "valid":
|
||||||
|
return _PROJECT_VALID_DRAFT(payload)
|
||||||
|
return _PROJECT_INVALID_DRAFT(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_patch_draft(payload: dict[str, Any]) -> PatchDraftResult:
|
||||||
|
"""Project one patch result while preserving an optional invalid draft."""
|
||||||
|
if payload.get("status") == "valid":
|
||||||
|
return _PROJECT_PATCHED_DRAFT_VALID(payload)
|
||||||
|
return _PROJECT_PATCHED_DRAFT_INVALID(payload)
|
||||||
|
|
||||||
|
|
||||||
def _empty_object_schema() -> dict[str, Any]:
|
def _empty_object_schema() -> dict[str, Any]:
|
||||||
@@ -174,11 +196,13 @@ class WorkflowDraftApi:
|
|||||||
node_defs.append(spec.to_node_def())
|
node_defs.append(spec.to_node_def())
|
||||||
return node_defs
|
return node_defs
|
||||||
|
|
||||||
async def validate_draft(self, *, draft: dict[str, Any]) -> dict[str, Any]:
|
async def validate_draft(self, *, draft: dict[str, Any]) -> ValidateDraftResult:
|
||||||
return validate_workflow_draft(
|
return _project_validate_draft(
|
||||||
draft,
|
validate_workflow_draft(
|
||||||
outcome_lookup=self._outcomes_for_capability,
|
draft,
|
||||||
node_defs=self._node_defs_for_draft(draft),
|
outcome_lookup=self._outcomes_for_capability,
|
||||||
|
node_defs=self._node_defs_for_draft(draft),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def compile_draft(
|
async def compile_draft(
|
||||||
@@ -203,11 +227,13 @@ class WorkflowDraftApi:
|
|||||||
*,
|
*,
|
||||||
draft: dict[str, Any],
|
draft: dict[str, Any],
|
||||||
patch: list[dict[str, Any]],
|
patch: list[dict[str, Any]],
|
||||||
) -> dict[str, Any]:
|
) -> PatchDraftResult:
|
||||||
return patch_workflow_draft(
|
return _project_patch_draft(
|
||||||
draft,
|
patch_workflow_draft(
|
||||||
patch,
|
draft,
|
||||||
node_defs_for_draft=self._node_defs_for_draft,
|
patch,
|
||||||
|
node_defs_for_draft=self._node_defs_for_draft,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def list_draft_workspaces(self) -> ListDraftWorkspacesResult:
|
async def list_draft_workspaces(self) -> ListDraftWorkspacesResult:
|
||||||
@@ -887,14 +913,14 @@ def _path_text(value: Any, *, expected_root: str | None = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _with_workspace_repair_hints(
|
def _with_workspace_repair_hints(
|
||||||
payload: dict[str, Any],
|
payload: Mapping[str, Any],
|
||||||
*,
|
*,
|
||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
revision: int,
|
revision: int,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
diagnostics = payload.get("diagnostics")
|
diagnostics = payload.get("diagnostics")
|
||||||
if not isinstance(diagnostics, list):
|
if not isinstance(diagnostics, list):
|
||||||
return payload
|
return dict(payload)
|
||||||
enriched = []
|
enriched = []
|
||||||
changed = False
|
changed = False
|
||||||
for diagnostic in diagnostics:
|
for diagnostic in diagnostics:
|
||||||
@@ -912,7 +938,7 @@ def _with_workspace_repair_hints(
|
|||||||
changed = True
|
changed = True
|
||||||
enriched.append(repaired)
|
enriched.append(repaired)
|
||||||
if not changed:
|
if not changed:
|
||||||
return payload
|
return dict(payload)
|
||||||
return {**payload, "diagnostics": enriched}
|
return {**payload, "diagnostics": enriched}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,13 @@ from .drafts import (
|
|||||||
DraftWorkspaceSummary,
|
DraftWorkspaceSummary,
|
||||||
InvalidDraftResult,
|
InvalidDraftResult,
|
||||||
ListDraftWorkspacesResult,
|
ListDraftWorkspacesResult,
|
||||||
|
PatchDraftResult,
|
||||||
|
PatchedDraftInvalidResult,
|
||||||
|
PatchedDraftValidResult,
|
||||||
SavedDraftArtifactResult,
|
SavedDraftArtifactResult,
|
||||||
UnsavedDraftArtifactResult,
|
UnsavedDraftArtifactResult,
|
||||||
|
ValidateDraftResult,
|
||||||
|
ValidDraftResult,
|
||||||
WrapperAuthoringHintsPayload,
|
WrapperAuthoringHintsPayload,
|
||||||
WrapperMissingDecisionPayload,
|
WrapperMissingDecisionPayload,
|
||||||
WrapperOutcomeCandidatePayload,
|
WrapperOutcomeCandidatePayload,
|
||||||
@@ -136,6 +141,9 @@ __all__ = [
|
|||||||
"NodeSpecCapabilitySummary",
|
"NodeSpecCapabilitySummary",
|
||||||
"NodeSpecInventoryPayload",
|
"NodeSpecInventoryPayload",
|
||||||
"PageMetadataPayload",
|
"PageMetadataPayload",
|
||||||
|
"PatchDraftResult",
|
||||||
|
"PatchedDraftInvalidResult",
|
||||||
|
"PatchedDraftValidResult",
|
||||||
"RawWorkflowPlan",
|
"RawWorkflowPlan",
|
||||||
"ReducerInventoryPayload",
|
"ReducerInventoryPayload",
|
||||||
"ResumeReadiness",
|
"ResumeReadiness",
|
||||||
@@ -163,7 +171,9 @@ __all__ = [
|
|||||||
"TraceRange",
|
"TraceRange",
|
||||||
"TraceEntryPayload",
|
"TraceEntryPayload",
|
||||||
"UnsavedDraftArtifactResult",
|
"UnsavedDraftArtifactResult",
|
||||||
|
"ValidateDraftResult",
|
||||||
"ValidateDeploymentResult",
|
"ValidateDeploymentResult",
|
||||||
|
"ValidDraftResult",
|
||||||
"WorkflowDeploymentPayload",
|
"WorkflowDeploymentPayload",
|
||||||
"WorkflowArtifactPayload",
|
"WorkflowArtifactPayload",
|
||||||
"WorkflowRefPayload",
|
"WorkflowRefPayload",
|
||||||
|
|||||||
@@ -61,6 +61,34 @@ class InvalidDraftResult(TypedDict):
|
|||||||
diagnostics: list[DraftDiagnosticPayload]
|
diagnostics: list[DraftDiagnosticPayload]
|
||||||
|
|
||||||
|
|
||||||
|
class ValidDraftResult(TypedDict):
|
||||||
|
"""Successful validation of one stateless draft document."""
|
||||||
|
|
||||||
|
status: Literal["valid"]
|
||||||
|
diagnostics: list[DraftDiagnosticPayload]
|
||||||
|
compiled_plan: JsonObject
|
||||||
|
|
||||||
|
|
||||||
|
type ValidateDraftResult = ValidDraftResult | InvalidDraftResult
|
||||||
|
|
||||||
|
|
||||||
|
class PatchedDraftValidResult(ValidDraftResult):
|
||||||
|
"""Valid draft document produced after applying a JSON Patch."""
|
||||||
|
|
||||||
|
draft: JsonObject
|
||||||
|
|
||||||
|
|
||||||
|
class PatchedDraftInvalidResult(InvalidDraftResult):
|
||||||
|
"""Invalid patch result, optionally retaining the applied draft document."""
|
||||||
|
|
||||||
|
# Malformed JSON Patch cannot produce a document. Applied patches retain
|
||||||
|
# their invalid document so a later edit can repair it.
|
||||||
|
draft: NotRequired[JsonObject]
|
||||||
|
|
||||||
|
|
||||||
|
type PatchDraftResult = PatchedDraftValidResult | PatchedDraftInvalidResult
|
||||||
|
|
||||||
|
|
||||||
class CompileDraftWorkspaceSuccess(TypedDict):
|
class CompileDraftWorkspaceSuccess(TypedDict):
|
||||||
"""Compiled raw plan and dependencies for one valid draft workspace."""
|
"""Compiled raw plan and dependencies for one valid draft workspace."""
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from .models import (
|
|||||||
ListDeploymentsResult,
|
ListDeploymentsResult,
|
||||||
ListDraftWorkspacesResult,
|
ListDraftWorkspacesResult,
|
||||||
ListRunsResult,
|
ListRunsResult,
|
||||||
|
PatchDraftResult,
|
||||||
RawWorkflowPlan,
|
RawWorkflowPlan,
|
||||||
RunResult,
|
RunResult,
|
||||||
RunTraceResult,
|
RunTraceResult,
|
||||||
@@ -36,6 +37,7 @@ from .models import (
|
|||||||
SavedDraftArtifactResult,
|
SavedDraftArtifactResult,
|
||||||
SaveDeploymentResult,
|
SaveDeploymentResult,
|
||||||
ValidateDeploymentResult,
|
ValidateDeploymentResult,
|
||||||
|
ValidateDraftResult,
|
||||||
WorkflowArtifactPayload,
|
WorkflowArtifactPayload,
|
||||||
WorkflowDeploymentPayload,
|
WorkflowDeploymentPayload,
|
||||||
)
|
)
|
||||||
@@ -254,7 +256,7 @@ class WorkflowApi:
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
draft: dict[str, Any],
|
draft: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> ValidateDraftResult:
|
||||||
return await self.drafts.validate_draft(draft=draft)
|
return await self.drafts.validate_draft(draft=draft)
|
||||||
|
|
||||||
async def compile_draft(
|
async def compile_draft(
|
||||||
@@ -269,7 +271,7 @@ class WorkflowApi:
|
|||||||
*,
|
*,
|
||||||
draft: dict[str, Any],
|
draft: dict[str, Any],
|
||||||
patch: list[dict[str, Any]],
|
patch: list[dict[str, Any]],
|
||||||
) -> dict[str, Any]:
|
) -> PatchDraftResult:
|
||||||
return await self.drafts.patch_draft(draft=draft, patch=patch)
|
return await self.drafts.patch_draft(draft=draft, patch=patch)
|
||||||
|
|
||||||
# -- draft workspaces --
|
# -- draft workspaces --
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ from .models import (
|
|||||||
ListDraftWorkspacesResult,
|
ListDraftWorkspacesResult,
|
||||||
ListRunsResult,
|
ListRunsResult,
|
||||||
ListSourcesResult,
|
ListSourcesResult,
|
||||||
|
PatchDraftResult,
|
||||||
RunResult,
|
RunResult,
|
||||||
RunTraceResult,
|
RunTraceResult,
|
||||||
SaveArtifactResult,
|
SaveArtifactResult,
|
||||||
SaveDeploymentResult,
|
SaveDeploymentResult,
|
||||||
SourceDiagnosisResult,
|
SourceDiagnosisResult,
|
||||||
ValidateDeploymentResult,
|
ValidateDeploymentResult,
|
||||||
|
ValidateDraftResult,
|
||||||
WorkflowArtifactPayload,
|
WorkflowArtifactPayload,
|
||||||
WorkflowDeploymentPayload,
|
WorkflowDeploymentPayload,
|
||||||
)
|
)
|
||||||
@@ -72,6 +74,19 @@ class WorkflowDraftSurface(Protocol):
|
|||||||
not every same-process authoring helper on ``WorkflowApi``.
|
not every same-process authoring helper on ``WorkflowApi``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
async def validate_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
draft: dict[str, Any],
|
||||||
|
) -> ValidateDraftResult: ...
|
||||||
|
|
||||||
|
async def patch_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
draft: dict[str, Any],
|
||||||
|
patch: list[dict[str, Any]],
|
||||||
|
) -> PatchDraftResult: ...
|
||||||
|
|
||||||
async def list_draft_workspaces(self) -> ListDraftWorkspacesResult: ...
|
async def list_draft_workspaces(self) -> ListDraftWorkspacesResult: ...
|
||||||
|
|
||||||
async def get_draft_workspace(
|
async def get_draft_workspace(
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from wf_api.models import (
|
|||||||
DeleteDraftWorkspaceResult,
|
DeleteDraftWorkspaceResult,
|
||||||
DraftWorkspaceResult,
|
DraftWorkspaceResult,
|
||||||
ListDraftWorkspacesResult,
|
ListDraftWorkspacesResult,
|
||||||
|
PatchDraftResult,
|
||||||
|
ValidateDraftResult,
|
||||||
)
|
)
|
||||||
from wf_api.surface import RouteSource
|
from wf_api.surface import RouteSource
|
||||||
from wf_artifacts.drafts.models import DraftStep
|
from wf_artifacts.drafts.models import DraftStep
|
||||||
@@ -31,6 +33,30 @@ async def _call_draft_workspace(
|
|||||||
class RpcDraftClientMixin:
|
class RpcDraftClientMixin:
|
||||||
"""JSON-RPC implementation of workflow draft workspace surface methods."""
|
"""JSON-RPC implementation of workflow draft workspace surface methods."""
|
||||||
|
|
||||||
|
async def validate_draft(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
draft: dict[str, Any],
|
||||||
|
) -> ValidateDraftResult:
|
||||||
|
return cast(
|
||||||
|
ValidateDraftResult,
|
||||||
|
await self._call("workflow.drafts.validate", {"draft": draft}),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def patch_draft(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
draft: dict[str, Any],
|
||||||
|
patch: list[dict[str, Any]],
|
||||||
|
) -> PatchDraftResult:
|
||||||
|
return cast(
|
||||||
|
PatchDraftResult,
|
||||||
|
await self._call(
|
||||||
|
"workflow.drafts.patch",
|
||||||
|
{"draft": draft, "patch": patch},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
async def list_draft_workspaces(self: RpcCaller) -> ListDraftWorkspacesResult:
|
async def list_draft_workspaces(self: RpcCaller) -> ListDraftWorkspacesResult:
|
||||||
return cast(
|
return cast(
|
||||||
ListDraftWorkspacesResult,
|
ListDraftWorkspacesResult,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ Return annotations stay eagerly evaluated because fastapi-jsonrpc captures them
|
|||||||
while registering nested handlers for response validation and OpenRPC output.
|
while registering nested handlers for response validation and OpenRPC output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import fastapi_jsonrpc as jsonrpc
|
import fastapi_jsonrpc as jsonrpc
|
||||||
|
|
||||||
from wf_api.models import (
|
from wf_api.models import (
|
||||||
@@ -15,6 +13,8 @@ from wf_api.models import (
|
|||||||
DeleteDraftWorkspaceResult,
|
DeleteDraftWorkspaceResult,
|
||||||
DraftWorkspaceResult,
|
DraftWorkspaceResult,
|
||||||
ListDraftWorkspacesResult,
|
ListDraftWorkspacesResult,
|
||||||
|
PatchDraftResult,
|
||||||
|
ValidateDraftResult,
|
||||||
)
|
)
|
||||||
from wf_api.surface import RouteSource
|
from wf_api.surface import RouteSource
|
||||||
from wf_server import WorkflowServer
|
from wf_server import WorkflowServer
|
||||||
@@ -77,7 +77,7 @@ def register_methods(
|
|||||||
@entrypoint.method(name="workflow.drafts.patch", errors=[WorkflowRpcError])
|
@entrypoint.method(name="workflow.drafts.patch", errors=[WorkflowRpcError])
|
||||||
async def workflow_drafts_patch(
|
async def workflow_drafts_patch(
|
||||||
params: PatchDraftParams = RpcParams(),
|
params: PatchDraftParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> PatchDraftResult:
|
||||||
try:
|
try:
|
||||||
return await server.api.patch_draft(draft=params.draft, patch=params.patch)
|
return await server.api.patch_draft(draft=params.draft, patch=params.patch)
|
||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
@@ -86,7 +86,7 @@ def register_methods(
|
|||||||
@entrypoint.method(name="workflow.drafts.validate", errors=[WorkflowRpcError])
|
@entrypoint.method(name="workflow.drafts.validate", errors=[WorkflowRpcError])
|
||||||
async def workflow_drafts_validate(
|
async def workflow_drafts_validate(
|
||||||
params: ValidateDraftParams = RpcParams(),
|
params: ValidateDraftParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> ValidateDraftResult:
|
||||||
try:
|
try:
|
||||||
return await server.api.validate_draft(draft=params.draft)
|
return await server.api.validate_draft(draft=params.draft)
|
||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
|||||||
@@ -740,6 +740,36 @@ async def test_patch_draft_applies_json_patch(tmp_path: Path) -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_patch_draft_projects_valid_applied_document(tmp_path: Path) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_valid")
|
||||||
|
api, _service, _authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
|
||||||
|
result = await api.patch_draft(
|
||||||
|
draft=_echo_draft(),
|
||||||
|
patch=[{"op": "replace", "path": "/name", "value": "renamed_echo"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["status"] == "valid"
|
||||||
|
assert result["draft"]["name"] == "renamed_echo"
|
||||||
|
assert result["compiled_plan"]["name"] == "renamed_echo"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_patch_draft_malformed_patch_has_no_draft(tmp_path: Path) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_malformed")
|
||||||
|
api, _service, _authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
|
||||||
|
result = await api.patch_draft(
|
||||||
|
draft=_echo_draft(),
|
||||||
|
patch=[{"op": "remove", "path": "/missing"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["status"] == "invalid"
|
||||||
|
assert result["diagnostics"][0]["code"] == "patch_invalid"
|
||||||
|
assert "draft" not in result
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_draft_workspace_creates_workspace(tmp_path: Path) -> None:
|
async def test_create_draft_workspace_creates_workspace(tmp_path: Path) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_workspace")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_workspace")
|
||||||
|
|||||||
@@ -31,6 +31,48 @@ async def _rpc(
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def _rpc_constant_draft() -> dict[str, Any]:
|
||||||
|
"""Return the canonical keyed draft shared by stateless RPC tests."""
|
||||||
|
return {
|
||||||
|
"name": "rpc_constant",
|
||||||
|
"input_schema": {"type": "object", "properties": {}},
|
||||||
|
"state_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"result": {"type": "string", "reducer": "wf.std.replace"}},
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"result": {"type": "string"}},
|
||||||
|
"required": ["result"],
|
||||||
|
},
|
||||||
|
"start": "constant",
|
||||||
|
"steps": {
|
||||||
|
"constant": {
|
||||||
|
"use": "wf.std.constant",
|
||||||
|
"input": [
|
||||||
|
{
|
||||||
|
"value": "hello over rpc",
|
||||||
|
"target": {"root": "local", "parts": ["value"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["value"]},
|
||||||
|
"target": {"root": "state", "parts": ["result"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routes": {"constant": {"ok": "__end__"}},
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"path": {"root": "state", "parts": ["result"]},
|
||||||
|
"target": {"root": "local", "parts": ["result"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_update_capability_step_params_preserve_nested_field_presence() -> None:
|
def test_update_capability_step_params_preserve_nested_field_presence() -> None:
|
||||||
params = UpdateCapabilityStepParams.model_validate(
|
params = UpdateCapabilityStepParams.model_validate(
|
||||||
{
|
{
|
||||||
@@ -193,6 +235,67 @@ async def test_rpc_source_discovery_preserves_inventory_contracts(tmp_path) -> N
|
|||||||
assert diagnosed["result"]["status"] == "unknown"
|
assert diagnosed["result"]["status"] == "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_stateless_draft_methods_preserve_result_variants(tmp_path) -> None:
|
||||||
|
app = create_rpc_app(build_local_static_workflow_server(tmp_path / "store"))
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
validated = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.drafts.validate",
|
||||||
|
{"draft": {}},
|
||||||
|
)
|
||||||
|
patched_valid = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.drafts.patch",
|
||||||
|
{
|
||||||
|
"draft": _rpc_constant_draft(),
|
||||||
|
"patch": [
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/name",
|
||||||
|
"value": "renamed_rpc_constant",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
patched_invalid = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.drafts.patch",
|
||||||
|
{
|
||||||
|
"draft": _rpc_constant_draft(),
|
||||||
|
"patch": [
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/routes/constant/ok",
|
||||||
|
"value": "missing_step",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
patched_malformed = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.drafts.patch",
|
||||||
|
{
|
||||||
|
"draft": {},
|
||||||
|
"patch": [{"op": "remove", "path": "/missing"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert validated["result"]["status"] == "invalid"
|
||||||
|
assert validated["result"]["diagnostics"]
|
||||||
|
assert patched_valid["result"]["status"] == "valid"
|
||||||
|
assert patched_valid["result"]["draft"]["name"] == "renamed_rpc_constant"
|
||||||
|
assert patched_valid["result"]["compiled_plan"]["name"] == "renamed_rpc_constant"
|
||||||
|
assert patched_invalid["result"]["status"] == "invalid"
|
||||||
|
assert patched_invalid["result"]["diagnostics"]
|
||||||
|
assert patched_invalid["result"]["draft"]["routes"]["constant"]["ok"] == (
|
||||||
|
"missing_step"
|
||||||
|
)
|
||||||
|
assert patched_malformed["result"]["status"] == "invalid"
|
||||||
|
assert patched_malformed["result"]["diagnostics"][0]["code"] == "patch_invalid"
|
||||||
|
assert "draft" not in patched_malformed["result"]
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_capability_methods_preserve_saved_wrapper_fields(tmp_path) -> None:
|
async def test_rpc_capability_methods_preserve_saved_wrapper_fields(tmp_path) -> None:
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
await server.api.create_artifact_from_plan(
|
await server.api.create_artifact_from_plan(
|
||||||
@@ -285,46 +388,7 @@ async def test_rpc_draft_artifact_deployment_lifecycle(tmp_path) -> None:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
draft = {
|
draft = _rpc_constant_draft()
|
||||||
"name": "rpc_constant",
|
|
||||||
"input_schema": {"type": "object", "properties": {}},
|
|
||||||
"state_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"result": {"type": "string", "reducer": "wf.std.replace"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"output_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"result": {"type": "string"}},
|
|
||||||
"required": ["result"],
|
|
||||||
},
|
|
||||||
"start": "constant",
|
|
||||||
"steps": {
|
|
||||||
"constant": {
|
|
||||||
"use": "wf.std.constant",
|
|
||||||
"input": [
|
|
||||||
{
|
|
||||||
"value": "hello over rpc",
|
|
||||||
"target": {"root": "local", "parts": ["value"]},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"output": [
|
|
||||||
{
|
|
||||||
"source": {"root": "local", "parts": ["value"]},
|
|
||||||
"target": {"root": "state", "parts": ["result"]},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"routes": {"constant": {"ok": "__end__"}},
|
|
||||||
"output": [
|
|
||||||
{
|
|
||||||
"path": {"root": "state", "parts": ["result"]},
|
|
||||||
"target": {"root": "local", "parts": ["result"]},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_draft = await _rpc(
|
validate_draft = await _rpc(
|
||||||
client,
|
client,
|
||||||
|
|||||||
@@ -399,6 +399,33 @@ async def test_rpc_client_sends_exact_draft_lifecycle_payloads() -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_client_sends_exact_stateless_draft_payloads() -> None:
|
||||||
|
calls: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
class Client(RpcDraftClientMixin):
|
||||||
|
async def _call(self, method: str, params: dict[str, object]):
|
||||||
|
calls.append({"method": method, "params": params})
|
||||||
|
return {"status": "invalid", "diagnostics": []}
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
draft = {"name": "report"}
|
||||||
|
patch = [{"op": "replace", "path": "/name", "value": "renamed"}]
|
||||||
|
|
||||||
|
await client.validate_draft(draft=draft)
|
||||||
|
await client.patch_draft(draft=draft, patch=patch)
|
||||||
|
|
||||||
|
assert calls == [
|
||||||
|
{
|
||||||
|
"method": "workflow.drafts.validate",
|
||||||
|
"params": {"draft": draft},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "workflow.drafts.patch",
|
||||||
|
"params": {"draft": draft, "patch": patch},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_client_sends_exact_replace_document_payload() -> None:
|
async def test_rpc_client_sends_exact_replace_document_payload() -> None:
|
||||||
calls: list[dict[str, Any]] = []
|
calls: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
|||||||
@@ -350,6 +350,53 @@ def test_openrpc_exposes_typed_compile_draft_workspace_result(
|
|||||||
assert schemas["InvalidDraftResult"]["properties"]["status"]["const"] == "invalid"
|
assert schemas["InvalidDraftResult"]["properties"]["status"]["const"] == "invalid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_openrpc_exposes_typed_stateless_draft_validation_result(
|
||||||
|
openrpc_document: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
method = _method_by_name(openrpc_document, "workflow.drafts.validate")
|
||||||
|
schemas = openrpc_document["components"]["schemas"]
|
||||||
|
|
||||||
|
assert method["result"]["schema"] == {
|
||||||
|
"$ref": "#/components/schemas/ValidateDraftResult"
|
||||||
|
}
|
||||||
|
assert schemas["ValidateDraftResult"] == {
|
||||||
|
"anyOf": [
|
||||||
|
{"$ref": "#/components/schemas/ValidDraftResult"},
|
||||||
|
{"$ref": "#/components/schemas/InvalidDraftResult"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert schemas["ValidDraftResult"]["required"] == [
|
||||||
|
"status",
|
||||||
|
"diagnostics",
|
||||||
|
"compiled_plan",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_openrpc_exposes_typed_stateless_draft_patch_result(
|
||||||
|
openrpc_document: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
method = _method_by_name(openrpc_document, "workflow.drafts.patch")
|
||||||
|
schemas = openrpc_document["components"]["schemas"]
|
||||||
|
|
||||||
|
assert method["result"]["schema"] == {
|
||||||
|
"$ref": "#/components/schemas/PatchDraftResult"
|
||||||
|
}
|
||||||
|
assert schemas["PatchDraftResult"] == {
|
||||||
|
"anyOf": [
|
||||||
|
{"$ref": "#/components/schemas/PatchedDraftValidResult"},
|
||||||
|
{"$ref": "#/components/schemas/PatchedDraftInvalidResult"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert "draft" in schemas["PatchedDraftValidResult"]["required"]
|
||||||
|
assert "draft" not in schemas["PatchedDraftInvalidResult"]["required"]
|
||||||
|
assert schemas["PatchedDraftValidResult"]["properties"]["draft"] == {
|
||||||
|
"$ref": "#/components/schemas/JsonObject"
|
||||||
|
}
|
||||||
|
assert schemas["PatchedDraftInvalidResult"]["properties"]["draft"] == {
|
||||||
|
"$ref": "#/components/schemas/JsonObject"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_openrpc_exposes_typed_capability_bootstrap_result(
|
def test_openrpc_exposes_typed_capability_bootstrap_result(
|
||||||
openrpc_document: dict[str, Any],
|
openrpc_document: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user