fix: close Python workflow client review

This commit is contained in:
lda
2026-08-31 23:38:03 +07:00 Verified
parent 4f946a35ef
commit 5e3d0b6524
15 changed files with 200 additions and 224 deletions
+58
View File
@@ -236,6 +236,29 @@ async def test_validate_artifact_plan_projects_invalid_plan_diagnostic(
}
@pytest.mark.asyncio
async def test_validate_artifact_plan_roots_capability_diagnostic_at_request_field(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_requirement")
api, _service = _artifact_api(artifact_store)
result = await api.validate_artifact_plan(
plan=_echo_artifact().plan,
outcomes=("completed",),
required_capabilities={
"broken": {
"ref": {"source": "demo", "capability_key": "echo"},
"kind": "not-a-capability-kind",
}
},
source_bindings={},
)
assert result["status"] == "invalid"
assert result["diagnostics"][0]["path"] == "required_capabilities.broken.kind"
@pytest.mark.asyncio
async def test_validate_artifact_plan_propagates_unexpected_value_error(
tmp_path: Path,
@@ -290,6 +313,41 @@ async def test_validate_artifact_plan_derives_saved_workflow_dependencies(
assert result["workflow_dependencies"] == {"child_workflow": 7}
@pytest.mark.asyncio
async def test_validate_artifact_plan_rejects_conflicting_child_version_pins(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_pin_conflict")
api, _service = _artifact_api(artifact_store)
plan = _echo_artifact().plan
plan["start"] = "child_v1"
plan["nodes"] = [
{
"id": node_id,
"type": "subgraph",
"workflow": {"artifact_id": "child_workflow", "version": version},
"input_schema": {"type": "object"},
"output_schema": {"type": "object"},
"outcomes": ["completed"],
}
for node_id, version in (("child_v1", 1), ("child_v2", 2))
]
plan["edges"] = [
{"from": "child_v1", "outcome": "completed", "to": "child_v2"},
{"from": "child_v2", "outcome": "completed", "to": "__end__"},
]
result = await api.validate_artifact_plan(
plan=plan,
outcomes=("completed",),
source_bindings={},
)
assert result["status"] == "invalid"
assert result["diagnostics"][0]["path"] == "plan"
assert "conflicting versions 1 and 2" in result["diagnostics"][0]["message"]
@pytest.mark.asyncio
async def test_create_artifact_from_workspace_suggests_exact_available_source_binding(
tmp_path: Path,
+24 -7
View File
@@ -150,19 +150,25 @@ async def test_http_app_translates_http_and_json_failures(
async def test_http_app_translates_known_workflow_protocol_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def error_post(*args: object, **kwargs: object) -> httpx.Response:
async def error_post(
_client: object,
_url: object,
*,
json: dict[str, object],
**_kwargs: object,
) -> httpx.Response:
return httpx.Response(
200,
request=httpx.Request("POST", "http://test/rpc"),
json={
"jsonrpc": "2.0",
"id": "request",
"id": json["id"],
"error": {
"code": 5000,
"message": "Workflow operation failed",
"data": {
"code": "capability_not_found",
"message": "unknown capability app.default.search",
"code": "KeyError",
"message": "unknown workflow capability 'app.default.search'",
},
},
},
@@ -175,7 +181,12 @@ async def test_http_app_translates_known_workflow_protocol_error(
await app.capability("app.default.search")
assert isinstance(raised.value, CapabilityNotFound)
assert "unknown capability" in str(raised.value)
assert "unknown workflow capability" in str(raised.value)
assert raised.value.code == 5000
assert raised.value.data == {
"code": "KeyError",
"message": "unknown workflow capability 'app.default.search'",
}
@pytest.mark.asyncio
@@ -184,13 +195,19 @@ async def test_http_app_preserves_unknown_protocol_error_details(
) -> None:
data = {"code": "future_workflow_error", "message": "future detail", "retry": 3}
async def error_post(*args: object, **kwargs: object) -> httpx.Response:
async def error_post(
_client: object,
_url: object,
*,
json: dict[str, object],
**_kwargs: object,
) -> httpx.Response:
return httpx.Response(
200,
request=httpx.Request("POST", "http://test/rpc"),
json={
"jsonrpc": "2.0",
"id": "request",
"id": json["id"],
"error": {
"code": 5999,
"message": "Future workflow error",
+28 -1
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import json
from typing import Any
import httpx
@@ -33,11 +34,12 @@ from wf_transport_rpc_http.client.sources import RpcSourceAdminClientMixin
async def test_rpc_client_preserves_structured_jsonrpc_error() -> None:
def handler(request: httpx.Request) -> httpx.Response:
request_id = json.loads(request.content)["id"]
return httpx.Response(
200,
json={
"jsonrpc": "2.0",
"id": "request",
"id": request_id,
"error": {
"code": "missing_source",
"message": "workflow operation failed",
@@ -61,6 +63,31 @@ async def test_rpc_client_preserves_structured_jsonrpc_error() -> None:
assert str(raised.value) == ("workflow operation failed: source is not configured")
@pytest.mark.asyncio
@pytest.mark.parametrize(
("jsonrpc", "response_id"),
[(None, "echo"), ("1.0", "echo"), ("2.0", "wrong")],
)
async def test_rpc_client_rejects_malformed_response_envelope(
jsonrpc: str | None,
response_id: str,
) -> None:
def handler(request: httpx.Request) -> httpx.Response:
request_id = json.loads(request.content)["id"]
payload: dict[str, object] = {
"id": request_id if response_id == "echo" else response_id,
"result": {},
}
if jsonrpc is not None:
payload["jsonrpc"] = jsonrpc
return httpx.Response(200, json=payload)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http_client:
client = RpcWorkflowApiClient(url="http://test/rpc", http_client=http_client)
with pytest.raises(RuntimeError, match="JSON-RPC response"):
await client.list_capabilities()
def _constant_plan() -> RawWorkflowPlan:
return RawWorkflowPlan.model_validate(
{