feat: type stateless draft results

This commit is contained in:
lda
2026-08-01 08:27:47 +07:00 Verified
parent 3891a62138
commit fbcd3a61a5
12 changed files with 338 additions and 63 deletions
+30
View File
@@ -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")
+104 -40
View File
@@ -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: