feat: type stateless draft results
This commit is contained in:
@@ -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
|
||||
async def test_create_draft_workspace_creates_workspace(tmp_path: Path) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_workspace")
|
||||
|
||||
@@ -31,6 +31,48 @@ async def _rpc(
|
||||
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:
|
||||
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"
|
||||
|
||||
|
||||
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:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
await server.api.create_artifact_from_plan(
|
||||
@@ -285,46 +388,7 @@ async def test_rpc_draft_artifact_deployment_lifecycle(tmp_path) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
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"]},
|
||||
}
|
||||
],
|
||||
}
|
||||
draft = _rpc_constant_draft()
|
||||
|
||||
validate_draft = await _rpc(
|
||||
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:
|
||||
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"
|
||||
|
||||
|
||||
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(
|
||||
openrpc_document: dict[str, Any],
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user